@flayerlabs/gamemode-spec 0.3.0 → 0.4.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/dist/frame.d.ts +127 -0
- package/dist/frame.d.ts.map +1 -0
- package/dist/frame.js +175 -0
- package/dist/frame.js.map +1 -0
- package/package.json +5 -1
- package/src/frame.ts +244 -0
package/dist/frame.d.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { LaunchContext } from './live.js';
|
|
2
|
+
/**
|
|
3
|
+
* The wire contract between a game iframe and the page embedding it.
|
|
4
|
+
*
|
|
5
|
+
* A game and its embedding page are different origins, so the `Host` object the client needs
|
|
6
|
+
* cannot be handed across directly — it is reconstructed over `postMessage` using these frames.
|
|
7
|
+
* The shapes live in spec because BOTH ends must agree while sharing no runtime: the game side is
|
|
8
|
+
* `@flayerlabs/gamemode-client`, and an embedding page may implement the parent side from these
|
|
9
|
+
* definitions alone.
|
|
10
|
+
*
|
|
11
|
+
* Two ground rules, both inherited from the live-socket protocol:
|
|
12
|
+
* - every frame carries `v`, and an end that sees a version it does not speak ignores the frame;
|
|
13
|
+
* - bigints cross as decimal strings, because they do not survive JSON and structured clone is not
|
|
14
|
+
* guaranteed by every path a frame may take.
|
|
15
|
+
*
|
|
16
|
+
* The trust story is deliberately asymmetric. The game learns exactly one thing from its URL — the
|
|
17
|
+
* embedding page's origin, `?parentOrigin=` — because it needs that BEFORE any message can be
|
|
18
|
+
* trusted. Everything else (which gate, which round, which coin) arrives in `gm:context`, so there
|
|
19
|
+
* is one source of truth and it is the page the player is actually looking at.
|
|
20
|
+
*/
|
|
21
|
+
export declare const FRAME_PROTOCOL_VERSION: 1;
|
|
22
|
+
/**
|
|
23
|
+
* Why a buy did not happen, in terms a player can be told about.
|
|
24
|
+
*
|
|
25
|
+
* Deliberately a short list of plain outcomes rather than anything from the chain. A game should
|
|
26
|
+
* never render a revert string or an error code at a player: the client maps each of these to its
|
|
27
|
+
* own copy, and the copy is free to change without the meaning moving.
|
|
28
|
+
*
|
|
29
|
+
* Lives here rather than in the client because the embedding page produces these values and the
|
|
30
|
+
* game consumes them — the one enum both ends of the frame protocol must share.
|
|
31
|
+
*/
|
|
32
|
+
import type { BuyFailure } from './live.js';
|
|
33
|
+
export type { BuyFailure };
|
|
34
|
+
/** Everything a game needs to join its round, told to it by the page rather than its URL. */
|
|
35
|
+
export interface EmbedContext {
|
|
36
|
+
readonly gateUrl: string;
|
|
37
|
+
readonly roundId: string;
|
|
38
|
+
readonly coinAddress: string;
|
|
39
|
+
readonly chainId: number;
|
|
40
|
+
/** Optional — a live gate sends it in the first snapshot anyway. */
|
|
41
|
+
readonly launch?: LaunchContext;
|
|
42
|
+
}
|
|
43
|
+
/** Exactly what the gate signed, all strings because bigints do not survive JSON. */
|
|
44
|
+
export interface WireAuthorisation {
|
|
45
|
+
buyer: string;
|
|
46
|
+
poolId: string;
|
|
47
|
+
deadline: string;
|
|
48
|
+
maxSpendWei: string;
|
|
49
|
+
nonce: string;
|
|
50
|
+
signature: string;
|
|
51
|
+
signer: string;
|
|
52
|
+
/** Opaque proof the page passes to the Flaunch buy call. Never calldata. */
|
|
53
|
+
hookData?: `0x${string}`;
|
|
54
|
+
}
|
|
55
|
+
export type HostCall = {
|
|
56
|
+
method: 'address';
|
|
57
|
+
} | {
|
|
58
|
+
method: 'signIn';
|
|
59
|
+
message: string;
|
|
60
|
+
} | {
|
|
61
|
+
method: 'buy';
|
|
62
|
+
authorisation: WireAuthorisation;
|
|
63
|
+
};
|
|
64
|
+
export type GameToHostFrame = {
|
|
65
|
+
v: typeof FRAME_PROTOCOL_VERSION;
|
|
66
|
+
type: 'gm:hello';
|
|
67
|
+
} | {
|
|
68
|
+
v: typeof FRAME_PROTOCOL_VERSION;
|
|
69
|
+
type: 'gm:req';
|
|
70
|
+
id: string;
|
|
71
|
+
call: HostCall;
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* What a refusal means to the game: `declined` and `no-wallet` are player states a game can name;
|
|
75
|
+
* `not-allowed` is a call the page refuses to service; `failed` is everything else.
|
|
76
|
+
*/
|
|
77
|
+
export type HostErrorCode = 'declined' | 'no-wallet' | 'not-allowed' | 'failed';
|
|
78
|
+
export type WireHostResult = {
|
|
79
|
+
method: 'address';
|
|
80
|
+
address: string | null;
|
|
81
|
+
} | {
|
|
82
|
+
method: 'signIn';
|
|
83
|
+
signature: `0x${string}`;
|
|
84
|
+
} | {
|
|
85
|
+
method: 'buy';
|
|
86
|
+
outcome: {
|
|
87
|
+
spentWei: string;
|
|
88
|
+
} | {
|
|
89
|
+
failed: {
|
|
90
|
+
bought: false;
|
|
91
|
+
reason: BuyFailure;
|
|
92
|
+
};
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
export type HostToGameFrame = {
|
|
96
|
+
v: typeof FRAME_PROTOCOL_VERSION;
|
|
97
|
+
type: 'gm:context';
|
|
98
|
+
context: EmbedContext;
|
|
99
|
+
} | {
|
|
100
|
+
v: typeof FRAME_PROTOCOL_VERSION;
|
|
101
|
+
type: 'gm:progress';
|
|
102
|
+
id: string;
|
|
103
|
+
progress: {
|
|
104
|
+
state: 'pending';
|
|
105
|
+
transactionHash?: string;
|
|
106
|
+
};
|
|
107
|
+
} | {
|
|
108
|
+
v: typeof FRAME_PROTOCOL_VERSION;
|
|
109
|
+
type: 'gm:res';
|
|
110
|
+
id: string;
|
|
111
|
+
ok: true;
|
|
112
|
+
result: WireHostResult;
|
|
113
|
+
} | {
|
|
114
|
+
v: typeof FRAME_PROTOCOL_VERSION;
|
|
115
|
+
type: 'gm:res';
|
|
116
|
+
id: string;
|
|
117
|
+
ok: false;
|
|
118
|
+
error: {
|
|
119
|
+
code: HostErrorCode;
|
|
120
|
+
message: string;
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
/** A frame from the game, or null for anything malformed or from another protocol version. */
|
|
124
|
+
export declare function gameFrameFrom(input: unknown): GameToHostFrame | null;
|
|
125
|
+
/** A frame from the page, or null for anything malformed or from another protocol version. */
|
|
126
|
+
export declare function hostFrameFrom(input: unknown): HostToGameFrame | null;
|
|
127
|
+
//# sourceMappingURL=frame.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frame.d.ts","sourceRoot":"","sources":["../src/frame.ts"],"names":[],"mappings":"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"}
|
package/dist/frame.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire contract between a game iframe and the page embedding it.
|
|
3
|
+
*
|
|
4
|
+
* A game and its embedding page are different origins, so the `Host` object the client needs
|
|
5
|
+
* cannot be handed across directly — it is reconstructed over `postMessage` using these frames.
|
|
6
|
+
* The shapes live in spec because BOTH ends must agree while sharing no runtime: the game side is
|
|
7
|
+
* `@flayerlabs/gamemode-client`, and an embedding page may implement the parent side from these
|
|
8
|
+
* definitions alone.
|
|
9
|
+
*
|
|
10
|
+
* Two ground rules, both inherited from the live-socket protocol:
|
|
11
|
+
* - every frame carries `v`, and an end that sees a version it does not speak ignores the frame;
|
|
12
|
+
* - bigints cross as decimal strings, because they do not survive JSON and structured clone is not
|
|
13
|
+
* guaranteed by every path a frame may take.
|
|
14
|
+
*
|
|
15
|
+
* The trust story is deliberately asymmetric. The game learns exactly one thing from its URL — the
|
|
16
|
+
* embedding page's origin, `?parentOrigin=` — because it needs that BEFORE any message can be
|
|
17
|
+
* trusted. Everything else (which gate, which round, which coin) arrives in `gm:context`, so there
|
|
18
|
+
* is one source of truth and it is the page the player is actually looking at.
|
|
19
|
+
*/
|
|
20
|
+
export const FRAME_PROTOCOL_VERSION = 1;
|
|
21
|
+
const BUY_FAILURES = [
|
|
22
|
+
'nothing-to-spend',
|
|
23
|
+
'declined',
|
|
24
|
+
'not-enough-for-fees',
|
|
25
|
+
'window-closed',
|
|
26
|
+
'try-again',
|
|
27
|
+
];
|
|
28
|
+
const HOST_ERROR_CODES = ['declined', 'no-wallet', 'not-allowed', 'failed'];
|
|
29
|
+
const isRecord = (value) => typeof value === 'object' && value !== null;
|
|
30
|
+
const isNonEmptyString = (value) => typeof value === 'string' && value.length > 0;
|
|
31
|
+
function authorisationFrom(value) {
|
|
32
|
+
if (!isRecord(value))
|
|
33
|
+
return null;
|
|
34
|
+
const { buyer, poolId, deadline, maxSpendWei, nonce, signature, signer, hookData } = value;
|
|
35
|
+
for (const field of [buyer, poolId, deadline, maxSpendWei, nonce, signature, signer]) {
|
|
36
|
+
if (!isNonEmptyString(field))
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
if (hookData !== undefined && !(typeof hookData === 'string' && hookData.startsWith('0x')))
|
|
40
|
+
return null;
|
|
41
|
+
return {
|
|
42
|
+
buyer: buyer,
|
|
43
|
+
poolId: poolId,
|
|
44
|
+
deadline: deadline,
|
|
45
|
+
maxSpendWei: maxSpendWei,
|
|
46
|
+
nonce: nonce,
|
|
47
|
+
signature: signature,
|
|
48
|
+
signer: signer,
|
|
49
|
+
...(hookData !== undefined ? { hookData: hookData } : {}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function callFrom(value) {
|
|
53
|
+
if (!isRecord(value))
|
|
54
|
+
return null;
|
|
55
|
+
if (value.method === 'address')
|
|
56
|
+
return { method: 'address' };
|
|
57
|
+
if (value.method === 'signIn') {
|
|
58
|
+
return isNonEmptyString(value.message) ? { method: 'signIn', message: value.message } : null;
|
|
59
|
+
}
|
|
60
|
+
if (value.method === 'buy') {
|
|
61
|
+
const authorisation = authorisationFrom(value.authorisation);
|
|
62
|
+
return authorisation ? { method: 'buy', authorisation } : null;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
/** A frame from the game, or null for anything malformed or from another protocol version. */
|
|
67
|
+
export function gameFrameFrom(input) {
|
|
68
|
+
if (!isRecord(input) || input.v !== FRAME_PROTOCOL_VERSION)
|
|
69
|
+
return null;
|
|
70
|
+
if (input.type === 'gm:hello')
|
|
71
|
+
return { v: FRAME_PROTOCOL_VERSION, type: 'gm:hello' };
|
|
72
|
+
if (input.type === 'gm:req') {
|
|
73
|
+
if (!isNonEmptyString(input.id))
|
|
74
|
+
return null;
|
|
75
|
+
const call = callFrom(input.call);
|
|
76
|
+
return call ? { v: FRAME_PROTOCOL_VERSION, type: 'gm:req', id: input.id, call } : null;
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
function contextFrom(value) {
|
|
81
|
+
if (!isRecord(value))
|
|
82
|
+
return null;
|
|
83
|
+
const { gateUrl, roundId, coinAddress, chainId, launch } = value;
|
|
84
|
+
if (!isNonEmptyString(gateUrl) || !isNonEmptyString(roundId) || !isNonEmptyString(coinAddress))
|
|
85
|
+
return null;
|
|
86
|
+
if (typeof chainId !== 'number' || !Number.isSafeInteger(chainId) || chainId <= 0)
|
|
87
|
+
return null;
|
|
88
|
+
// Launch context is validated shallowly: it is display data a live gate re-sends authoritatively
|
|
89
|
+
// in its first snapshot, so a malformed one costs a blank header, not a wrong decision.
|
|
90
|
+
if (launch !== undefined && !isRecord(launch))
|
|
91
|
+
return null;
|
|
92
|
+
return {
|
|
93
|
+
gateUrl,
|
|
94
|
+
roundId,
|
|
95
|
+
coinAddress,
|
|
96
|
+
chainId,
|
|
97
|
+
...(launch !== undefined ? { launch: launch } : {}),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function resultFrom(value) {
|
|
101
|
+
if (!isRecord(value))
|
|
102
|
+
return null;
|
|
103
|
+
if (value.method === 'address') {
|
|
104
|
+
return value.address === null || isNonEmptyString(value.address)
|
|
105
|
+
? { method: 'address', address: value.address }
|
|
106
|
+
: null;
|
|
107
|
+
}
|
|
108
|
+
if (value.method === 'signIn') {
|
|
109
|
+
return typeof value.signature === 'string' && value.signature.startsWith('0x')
|
|
110
|
+
? { method: 'signIn', signature: value.signature }
|
|
111
|
+
: null;
|
|
112
|
+
}
|
|
113
|
+
if (value.method === 'buy') {
|
|
114
|
+
const outcome = value.outcome;
|
|
115
|
+
if (!isRecord(outcome))
|
|
116
|
+
return null;
|
|
117
|
+
if (isNonEmptyString(outcome.spentWei))
|
|
118
|
+
return { method: 'buy', outcome: { spentWei: outcome.spentWei } };
|
|
119
|
+
const failed = outcome.failed;
|
|
120
|
+
if (isRecord(failed) && failed.bought === false && BUY_FAILURES.includes(failed.reason)) {
|
|
121
|
+
return { method: 'buy', outcome: { failed: { bought: false, reason: failed.reason } } };
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
/** A frame from the page, or null for anything malformed or from another protocol version. */
|
|
128
|
+
export function hostFrameFrom(input) {
|
|
129
|
+
if (!isRecord(input) || input.v !== FRAME_PROTOCOL_VERSION)
|
|
130
|
+
return null;
|
|
131
|
+
if (input.type === 'gm:context') {
|
|
132
|
+
const context = contextFrom(input.context);
|
|
133
|
+
return context ? { v: FRAME_PROTOCOL_VERSION, type: 'gm:context', context } : null;
|
|
134
|
+
}
|
|
135
|
+
if (input.type === 'gm:progress') {
|
|
136
|
+
if (!isNonEmptyString(input.id) || !isRecord(input.progress))
|
|
137
|
+
return null;
|
|
138
|
+
const { state, transactionHash } = input.progress;
|
|
139
|
+
if (state !== 'pending')
|
|
140
|
+
return null;
|
|
141
|
+
if (transactionHash !== undefined && !isNonEmptyString(transactionHash))
|
|
142
|
+
return null;
|
|
143
|
+
return {
|
|
144
|
+
v: FRAME_PROTOCOL_VERSION,
|
|
145
|
+
type: 'gm:progress',
|
|
146
|
+
id: input.id,
|
|
147
|
+
progress: { state: 'pending', ...(transactionHash !== undefined ? { transactionHash } : {}) },
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (input.type === 'gm:res') {
|
|
151
|
+
if (!isNonEmptyString(input.id))
|
|
152
|
+
return null;
|
|
153
|
+
if (input.ok === true) {
|
|
154
|
+
const result = resultFrom(input.result);
|
|
155
|
+
return result ? { v: FRAME_PROTOCOL_VERSION, type: 'gm:res', id: input.id, ok: true, result } : null;
|
|
156
|
+
}
|
|
157
|
+
if (input.ok === false) {
|
|
158
|
+
const error = input.error;
|
|
159
|
+
if (!isRecord(error))
|
|
160
|
+
return null;
|
|
161
|
+
if (!HOST_ERROR_CODES.includes(error.code) || typeof error.message !== 'string')
|
|
162
|
+
return null;
|
|
163
|
+
return {
|
|
164
|
+
v: FRAME_PROTOCOL_VERSION,
|
|
165
|
+
type: 'gm:res',
|
|
166
|
+
id: input.id,
|
|
167
|
+
ok: false,
|
|
168
|
+
error: { code: error.code, message: error.message },
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=frame.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frame.js","sourceRoot":"","sources":["../src/frame.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flayerlabs/gamemode-spec",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Pure rules, round, scheduling, live-room and embed contracts for Flaunch Game Modes",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Flayer Labs",
|
|
@@ -36,6 +36,10 @@
|
|
|
36
36
|
"types": "./dist/live.d.ts",
|
|
37
37
|
"default": "./dist/live.js"
|
|
38
38
|
},
|
|
39
|
+
"./frame": {
|
|
40
|
+
"types": "./dist/frame.d.ts",
|
|
41
|
+
"default": "./dist/frame.js"
|
|
42
|
+
},
|
|
39
43
|
"./embed": {
|
|
40
44
|
"types": "./dist/embed.d.ts",
|
|
41
45
|
"default": "./dist/embed.js"
|
package/src/frame.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import type { LaunchContext } from './live.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The wire contract between a game iframe and the page embedding it.
|
|
5
|
+
*
|
|
6
|
+
* A game and its embedding page are different origins, so the `Host` object the client needs
|
|
7
|
+
* cannot be handed across directly — it is reconstructed over `postMessage` using these frames.
|
|
8
|
+
* The shapes live in spec because BOTH ends must agree while sharing no runtime: the game side is
|
|
9
|
+
* `@flayerlabs/gamemode-client`, and an embedding page may implement the parent side from these
|
|
10
|
+
* definitions alone.
|
|
11
|
+
*
|
|
12
|
+
* Two ground rules, both inherited from the live-socket protocol:
|
|
13
|
+
* - every frame carries `v`, and an end that sees a version it does not speak ignores the frame;
|
|
14
|
+
* - bigints cross as decimal strings, because they do not survive JSON and structured clone is not
|
|
15
|
+
* guaranteed by every path a frame may take.
|
|
16
|
+
*
|
|
17
|
+
* The trust story is deliberately asymmetric. The game learns exactly one thing from its URL — the
|
|
18
|
+
* embedding page's origin, `?parentOrigin=` — because it needs that BEFORE any message can be
|
|
19
|
+
* trusted. Everything else (which gate, which round, which coin) arrives in `gm:context`, so there
|
|
20
|
+
* is one source of truth and it is the page the player is actually looking at.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export const FRAME_PROTOCOL_VERSION = 1 as const;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Why a buy did not happen, in terms a player can be told about.
|
|
27
|
+
*
|
|
28
|
+
* Deliberately a short list of plain outcomes rather than anything from the chain. A game should
|
|
29
|
+
* never render a revert string or an error code at a player: the client maps each of these to its
|
|
30
|
+
* own copy, and the copy is free to change without the meaning moving.
|
|
31
|
+
*
|
|
32
|
+
* Lives here rather than in the client because the embedding page produces these values and the
|
|
33
|
+
* game consumes them — the one enum both ends of the frame protocol must share.
|
|
34
|
+
*/
|
|
35
|
+
// One definition for every end: the live room owns BuyFailure, the frame re-exports it.
|
|
36
|
+
import type { BuyFailure } from './live.js';
|
|
37
|
+
export type { BuyFailure };
|
|
38
|
+
|
|
39
|
+
/** Everything a game needs to join its round, told to it by the page rather than its URL. */
|
|
40
|
+
export interface EmbedContext {
|
|
41
|
+
readonly gateUrl: string;
|
|
42
|
+
readonly roundId: string;
|
|
43
|
+
readonly coinAddress: string;
|
|
44
|
+
readonly chainId: number;
|
|
45
|
+
/** Optional — a live gate sends it in the first snapshot anyway. */
|
|
46
|
+
readonly launch?: LaunchContext;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Exactly what the gate signed, all strings because bigints do not survive JSON. */
|
|
50
|
+
export interface WireAuthorisation {
|
|
51
|
+
buyer: string;
|
|
52
|
+
poolId: string;
|
|
53
|
+
deadline: string;
|
|
54
|
+
maxSpendWei: string;
|
|
55
|
+
nonce: string;
|
|
56
|
+
signature: string;
|
|
57
|
+
signer: string;
|
|
58
|
+
/** Opaque proof the page passes to the Flaunch buy call. Never calldata. */
|
|
59
|
+
hookData?: `0x${string}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type HostCall =
|
|
63
|
+
| { method: 'address' }
|
|
64
|
+
| { method: 'signIn'; message: string }
|
|
65
|
+
| { method: 'buy'; authorisation: WireAuthorisation };
|
|
66
|
+
|
|
67
|
+
export type GameToHostFrame =
|
|
68
|
+
| { v: typeof FRAME_PROTOCOL_VERSION; type: 'gm:hello' }
|
|
69
|
+
| { v: typeof FRAME_PROTOCOL_VERSION; type: 'gm:req'; id: string; call: HostCall };
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* What a refusal means to the game: `declined` and `no-wallet` are player states a game can name;
|
|
73
|
+
* `not-allowed` is a call the page refuses to service; `failed` is everything else.
|
|
74
|
+
*/
|
|
75
|
+
export type HostErrorCode = 'declined' | 'no-wallet' | 'not-allowed' | 'failed';
|
|
76
|
+
|
|
77
|
+
export type WireHostResult =
|
|
78
|
+
| { method: 'address'; address: string | null }
|
|
79
|
+
| { method: 'signIn'; signature: `0x${string}` }
|
|
80
|
+
| { method: 'buy'; outcome: { spentWei: string } | { failed: { bought: false; reason: BuyFailure } } };
|
|
81
|
+
|
|
82
|
+
export type HostToGameFrame =
|
|
83
|
+
| { v: typeof FRAME_PROTOCOL_VERSION; type: 'gm:context'; context: EmbedContext }
|
|
84
|
+
| {
|
|
85
|
+
v: typeof FRAME_PROTOCOL_VERSION;
|
|
86
|
+
type: 'gm:progress';
|
|
87
|
+
id: string;
|
|
88
|
+
progress: { state: 'pending'; transactionHash?: string };
|
|
89
|
+
}
|
|
90
|
+
| { v: typeof FRAME_PROTOCOL_VERSION; type: 'gm:res'; id: string; ok: true; result: WireHostResult }
|
|
91
|
+
| {
|
|
92
|
+
v: typeof FRAME_PROTOCOL_VERSION;
|
|
93
|
+
type: 'gm:res';
|
|
94
|
+
id: string;
|
|
95
|
+
ok: false;
|
|
96
|
+
error: { code: HostErrorCode; message: string };
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const BUY_FAILURES: readonly BuyFailure[] = [
|
|
100
|
+
'nothing-to-spend',
|
|
101
|
+
'declined',
|
|
102
|
+
'not-enough-for-fees',
|
|
103
|
+
'window-closed',
|
|
104
|
+
'try-again',
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
const HOST_ERROR_CODES: readonly HostErrorCode[] = ['declined', 'no-wallet', 'not-allowed', 'failed'];
|
|
108
|
+
|
|
109
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
110
|
+
typeof value === 'object' && value !== null;
|
|
111
|
+
|
|
112
|
+
const isNonEmptyString = (value: unknown): value is string => typeof value === 'string' && value.length > 0;
|
|
113
|
+
|
|
114
|
+
function authorisationFrom(value: unknown): WireAuthorisation | null {
|
|
115
|
+
if (!isRecord(value)) return null;
|
|
116
|
+
const { buyer, poolId, deadline, maxSpendWei, nonce, signature, signer, hookData } = value;
|
|
117
|
+
for (const field of [buyer, poolId, deadline, maxSpendWei, nonce, signature, signer]) {
|
|
118
|
+
if (!isNonEmptyString(field)) return null;
|
|
119
|
+
}
|
|
120
|
+
if (hookData !== undefined && !(typeof hookData === 'string' && hookData.startsWith('0x'))) return null;
|
|
121
|
+
return {
|
|
122
|
+
buyer: buyer as string,
|
|
123
|
+
poolId: poolId as string,
|
|
124
|
+
deadline: deadline as string,
|
|
125
|
+
maxSpendWei: maxSpendWei as string,
|
|
126
|
+
nonce: nonce as string,
|
|
127
|
+
signature: signature as string,
|
|
128
|
+
signer: signer as string,
|
|
129
|
+
...(hookData !== undefined ? { hookData: hookData as `0x${string}` } : {}),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function callFrom(value: unknown): HostCall | null {
|
|
134
|
+
if (!isRecord(value)) return null;
|
|
135
|
+
if (value.method === 'address') return { method: 'address' };
|
|
136
|
+
if (value.method === 'signIn') {
|
|
137
|
+
return isNonEmptyString(value.message) ? { method: 'signIn', message: value.message } : null;
|
|
138
|
+
}
|
|
139
|
+
if (value.method === 'buy') {
|
|
140
|
+
const authorisation = authorisationFrom(value.authorisation);
|
|
141
|
+
return authorisation ? { method: 'buy', authorisation } : null;
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** A frame from the game, or null for anything malformed or from another protocol version. */
|
|
147
|
+
export function gameFrameFrom(input: unknown): GameToHostFrame | null {
|
|
148
|
+
if (!isRecord(input) || input.v !== FRAME_PROTOCOL_VERSION) return null;
|
|
149
|
+
if (input.type === 'gm:hello') return { v: FRAME_PROTOCOL_VERSION, type: 'gm:hello' };
|
|
150
|
+
if (input.type === 'gm:req') {
|
|
151
|
+
if (!isNonEmptyString(input.id)) return null;
|
|
152
|
+
const call = callFrom(input.call);
|
|
153
|
+
return call ? { v: FRAME_PROTOCOL_VERSION, type: 'gm:req', id: input.id, call } : null;
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function contextFrom(value: unknown): EmbedContext | null {
|
|
159
|
+
if (!isRecord(value)) return null;
|
|
160
|
+
const { gateUrl, roundId, coinAddress, chainId, launch } = value;
|
|
161
|
+
if (!isNonEmptyString(gateUrl) || !isNonEmptyString(roundId) || !isNonEmptyString(coinAddress)) return null;
|
|
162
|
+
if (typeof chainId !== 'number' || !Number.isSafeInteger(chainId) || chainId <= 0) return null;
|
|
163
|
+
// Launch context is validated shallowly: it is display data a live gate re-sends authoritatively
|
|
164
|
+
// in its first snapshot, so a malformed one costs a blank header, not a wrong decision.
|
|
165
|
+
if (launch !== undefined && !isRecord(launch)) return null;
|
|
166
|
+
return {
|
|
167
|
+
gateUrl,
|
|
168
|
+
roundId,
|
|
169
|
+
coinAddress,
|
|
170
|
+
chainId,
|
|
171
|
+
...(launch !== undefined ? { launch: launch as unknown as LaunchContext } : {}),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function resultFrom(value: unknown): WireHostResult | null {
|
|
176
|
+
if (!isRecord(value)) return null;
|
|
177
|
+
if (value.method === 'address') {
|
|
178
|
+
return value.address === null || isNonEmptyString(value.address)
|
|
179
|
+
? { method: 'address', address: value.address as string | null }
|
|
180
|
+
: null;
|
|
181
|
+
}
|
|
182
|
+
if (value.method === 'signIn') {
|
|
183
|
+
return typeof value.signature === 'string' && value.signature.startsWith('0x')
|
|
184
|
+
? { method: 'signIn', signature: value.signature as `0x${string}` }
|
|
185
|
+
: null;
|
|
186
|
+
}
|
|
187
|
+
if (value.method === 'buy') {
|
|
188
|
+
const outcome = value.outcome;
|
|
189
|
+
if (!isRecord(outcome)) return null;
|
|
190
|
+
if (isNonEmptyString(outcome.spentWei)) return { method: 'buy', outcome: { spentWei: outcome.spentWei } };
|
|
191
|
+
const failed = outcome.failed;
|
|
192
|
+
if (isRecord(failed) && failed.bought === false && BUY_FAILURES.includes(failed.reason as BuyFailure)) {
|
|
193
|
+
return { method: 'buy', outcome: { failed: { bought: false, reason: failed.reason as BuyFailure } } };
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** A frame from the page, or null for anything malformed or from another protocol version. */
|
|
201
|
+
export function hostFrameFrom(input: unknown): HostToGameFrame | null {
|
|
202
|
+
if (!isRecord(input) || input.v !== FRAME_PROTOCOL_VERSION) return null;
|
|
203
|
+
|
|
204
|
+
if (input.type === 'gm:context') {
|
|
205
|
+
const context = contextFrom(input.context);
|
|
206
|
+
return context ? { v: FRAME_PROTOCOL_VERSION, type: 'gm:context', context } : null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (input.type === 'gm:progress') {
|
|
210
|
+
if (!isNonEmptyString(input.id) || !isRecord(input.progress)) return null;
|
|
211
|
+
const { state, transactionHash } = input.progress;
|
|
212
|
+
if (state !== 'pending') return null;
|
|
213
|
+
if (transactionHash !== undefined && !isNonEmptyString(transactionHash)) return null;
|
|
214
|
+
return {
|
|
215
|
+
v: FRAME_PROTOCOL_VERSION,
|
|
216
|
+
type: 'gm:progress',
|
|
217
|
+
id: input.id,
|
|
218
|
+
progress: { state: 'pending', ...(transactionHash !== undefined ? { transactionHash } : {}) },
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (input.type === 'gm:res') {
|
|
223
|
+
if (!isNonEmptyString(input.id)) return null;
|
|
224
|
+
if (input.ok === true) {
|
|
225
|
+
const result = resultFrom(input.result);
|
|
226
|
+
return result ? { v: FRAME_PROTOCOL_VERSION, type: 'gm:res', id: input.id, ok: true, result } : null;
|
|
227
|
+
}
|
|
228
|
+
if (input.ok === false) {
|
|
229
|
+
const error = input.error;
|
|
230
|
+
if (!isRecord(error)) return null;
|
|
231
|
+
if (!HOST_ERROR_CODES.includes(error.code as HostErrorCode) || typeof error.message !== 'string') return null;
|
|
232
|
+
return {
|
|
233
|
+
v: FRAME_PROTOCOL_VERSION,
|
|
234
|
+
type: 'gm:res',
|
|
235
|
+
id: input.id,
|
|
236
|
+
ok: false,
|
|
237
|
+
error: { code: error.code as HostErrorCode, message: error.message },
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return null;
|
|
244
|
+
}
|