@flayerlabs/gamemode-client 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/README.md +8 -12
- package/dist/embed.d.ts +28 -39
- package/dist/embed.d.ts.map +1 -1
- package/dist/embed.js +163 -314
- package/dist/embed.js.map +1 -1
- package/dist/host.d.ts +48 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +142 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +5 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/package.json +6 -2
- package/src/embed.ts +198 -367
- package/src/host.ts +178 -0
- package/src/index.ts +8 -13
package/README.md
CHANGED
|
@@ -45,23 +45,21 @@ and real spending. Call `unsubscribe()`, then `room.dispose()` when the game clo
|
|
|
45
45
|
An embedded game first receives its context and restricted host services from the trusted parent:
|
|
46
46
|
|
|
47
47
|
```ts
|
|
48
|
-
import {
|
|
48
|
+
import { connectHost, joinRoom } from '@flayerlabs/gamemode-client'
|
|
49
49
|
|
|
50
|
-
const embedded = await
|
|
51
|
-
|
|
52
|
-
})
|
|
50
|
+
const embedded = await connectHost()
|
|
51
|
+
if (!embedded) throw new Error('open this game from its coin page')
|
|
53
52
|
|
|
54
53
|
const room = await joinRoom({
|
|
55
54
|
gateUrl: embedded.context.gateUrl,
|
|
56
55
|
roundId: embedded.context.roundId,
|
|
57
56
|
launch: embedded.context.launch,
|
|
58
57
|
host: embedded.host,
|
|
59
|
-
platform: embedded.platform,
|
|
60
58
|
})
|
|
61
59
|
```
|
|
62
60
|
|
|
63
61
|
Creator code sends game actions and reads room state. It cannot send a wallet transaction or create
|
|
64
|
-
Flaunch calldata. The trusted parent uses `
|
|
62
|
+
Flaunch calldata. The trusted parent uses `attachGameHost` to provide the permitted wallet,
|
|
65
63
|
market and identity operations to one known iframe.
|
|
66
64
|
|
|
67
65
|
Call `room.dispose()` and `embedded.dispose()` when the iframe closes.
|
|
@@ -71,18 +69,16 @@ Call `room.dispose()` and `embedded.dispose()` when the iframe closes.
|
|
|
71
69
|
Use `joinEconomy()` when your existing server owns gameplay and scoring:
|
|
72
70
|
|
|
73
71
|
```ts
|
|
74
|
-
import {
|
|
72
|
+
import { connectHost, joinEconomy } from '@flayerlabs/gamemode-client'
|
|
75
73
|
|
|
76
|
-
const embedded = await
|
|
77
|
-
|
|
78
|
-
})
|
|
74
|
+
const embedded = await connectHost()
|
|
75
|
+
if (!embedded) throw new Error('open this game from its coin page')
|
|
79
76
|
|
|
80
77
|
const gameMode = await joinEconomy({
|
|
81
78
|
gateUrl: embedded.context.gateUrl,
|
|
82
79
|
roundId: embedded.context.roundId,
|
|
83
80
|
launch: embedded.context.launch,
|
|
84
81
|
host: embedded.host,
|
|
85
|
-
platform: embedded.platform,
|
|
86
82
|
})
|
|
87
83
|
|
|
88
84
|
const stop = gameMode.economy.subscribe(renderAllowance)
|
|
@@ -129,7 +125,7 @@ to build market presentation without a live chain.
|
|
|
129
125
|
## Integrate the trusted parent
|
|
130
126
|
|
|
131
127
|
Read the [trusted platform integration guide](https://github.com/flayerlabs/gamemode-sdk/blob/main/docs/guides/integrate-the-platform.md)
|
|
132
|
-
before implementing `
|
|
128
|
+
before implementing `attachGameHost`, `Host` or a production `joinRoom` call.
|
|
133
129
|
|
|
134
130
|
The [quiz example](https://github.com/flayerlabs/gamemode-sdk/tree/main/examples/quiz) shows a live
|
|
135
131
|
rules room. The [external-server example](https://github.com/flayerlabs/gamemode-sdk/tree/main/examples/external-server)
|
package/dist/embed.d.ts
CHANGED
|
@@ -1,45 +1,34 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import type { Host } from './live.js';
|
|
2
|
+
import { type EmbedContext } from '@flayerlabs/gamemode-spec/frame';
|
|
3
|
+
/**
|
|
4
|
+
* Where frames come from and go to. The default is the real window; tests inject a pair of these
|
|
5
|
+
* to run both ends of the protocol in one process with no DOM.
|
|
6
|
+
*/
|
|
7
|
+
export interface FrameEndpoint {
|
|
8
|
+
/** Deliver every incoming message with its origin and source; returns an unlisten. */
|
|
9
|
+
listen(handler: (data: unknown, origin: string, source: unknown) => void): () => void;
|
|
10
|
+
post(message: unknown, targetOrigin: string): void;
|
|
11
|
+
/** What a trusted message's source must equal. Null when there is no parent to talk to. */
|
|
12
|
+
parentSource(): unknown;
|
|
7
13
|
}
|
|
8
|
-
export interface
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export interface EmbeddedGameConnection {
|
|
12
|
-
context: EmbeddedContext;
|
|
13
|
-
host: Host;
|
|
14
|
-
platform: ClientPlatform;
|
|
15
|
-
dispose(): void;
|
|
16
|
-
}
|
|
17
|
-
export interface ConnectEmbeddedGameOptions {
|
|
18
|
-
/** Fixed in trusted game code, never taken from a query string supplied by the parent. */
|
|
19
|
-
parentOrigin: string;
|
|
14
|
+
export interface ConnectHostOptions {
|
|
15
|
+
/** Default: the `parentOrigin` query parameter of the page's own URL. */
|
|
16
|
+
parentOrigin?: string;
|
|
20
17
|
timeoutMs?: number;
|
|
21
|
-
|
|
22
|
-
parentWindow?: EmbedTargetWindow;
|
|
18
|
+
endpoint?: FrameEndpoint;
|
|
23
19
|
}
|
|
24
|
-
|
|
25
|
-
export declare function connectEmbeddedGame(options: ConnectEmbeddedGameOptions): Promise<EmbeddedGameConnection>;
|
|
26
|
-
export interface ServeEmbeddedGameOptions {
|
|
27
|
-
currentWindow?: EmbedEventWindow;
|
|
28
|
-
frameWindow: EmbedTargetWindow;
|
|
29
|
-
gameOrigin: string;
|
|
30
|
-
context: EmbeddedContext;
|
|
20
|
+
export interface EmbeddedHost {
|
|
31
21
|
host: Host;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
*
|
|
35
|
-
* The bridge itself checks the complete ABI encoding, including the required zero referrer,
|
|
36
|
-
* before calling this seam. A false result fails closed before any wallet UI can open.
|
|
37
|
-
*/
|
|
38
|
-
validateAuthorisation(authorisation: Authorisation, context: EmbeddedContext, signal?: AbortSignal): boolean | Promise<boolean>;
|
|
39
|
-
platform?: ClientPlatform;
|
|
40
|
-
/** Display domain used by the gate's canonical wallet challenge. */
|
|
41
|
-
signInDomain?: string;
|
|
22
|
+
context: EmbedContext;
|
|
23
|
+
dispose(): void;
|
|
42
24
|
}
|
|
43
|
-
/**
|
|
44
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Connect to the embedding page, or learn that there is none.
|
|
27
|
+
*
|
|
28
|
+
* Resolves null when the game is not in an iframe, when no `parentOrigin` was provided, or when
|
|
29
|
+
* the page does not answer within the timeout — a page that answers late was never going to
|
|
30
|
+
* service a buy either. After it resolves, individual Host calls have no timeout of their own:
|
|
31
|
+
* sign-in and buy legitimately wait on a human, and the page always answers, refusals included.
|
|
32
|
+
*/
|
|
33
|
+
export declare function connectHost(options?: ConnectHostOptions): Promise<EmbeddedHost | null>;
|
|
45
34
|
//# sourceMappingURL=embed.d.ts.map
|
package/dist/embed.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"embed.d.ts","sourceRoot":"","sources":["../src/embed.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"embed.d.ts","sourceRoot":"","sources":["../src/embed.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAmB,MAAM,WAAW,CAAC;AAEvD,OAAO,EAIL,KAAK,YAAY,EAGlB,MAAM,iCAAiC,CAAC;AAuBzC;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,sFAAsF;IACtF,MAAM,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACtF,IAAI,CAAC,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACnD,2FAA2F;IAC3F,YAAY,IAAI,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC,yEAAyE;IACzE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,aAAa,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,IAAI,CAAC;IACX,OAAO,EAAE,YAAY,CAAC;IACtB,OAAO,IAAI,IAAI,CAAC;CACjB;AAqCD;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CA6HhG"}
|
package/dist/embed.js
CHANGED
|
@@ -1,329 +1,178 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import { FRAME_PROTOCOL_VERSION, hostFrameFrom, } from '@flayerlabs/gamemode-spec/frame';
|
|
2
|
+
/**
|
|
3
|
+
* The game's end of the frame protocol: a {@link Host} reconstructed over `postMessage`.
|
|
4
|
+
*
|
|
5
|
+
* A game on flaunch.gg runs in a cross-origin iframe, so the page cannot hand it a Host object —
|
|
6
|
+
* the three Host calls travel as `gm:` frames instead (see `@flayerlabs/gamemode-spec/frame` for the
|
|
7
|
+
* shapes and the trust story). This file exists so a game never touches `postMessage` itself: it
|
|
8
|
+
* calls {@link connectHost} once and receives either a working Host plus the round to join, or
|
|
9
|
+
* `null` — which is an answer, not an error. Null means "not embedded", and the caller's right
|
|
10
|
+
* move is a mock room, so `pnpm dev` and the real page run the same entry file.
|
|
11
|
+
*
|
|
12
|
+
* Trust is decided once, before any message is read: the parent's origin comes from the one query
|
|
13
|
+
* parameter the page stamps on the iframe URL, and every frame is checked against both that origin
|
|
14
|
+
* and the parent window itself. A frame from anywhere else is silently ignored — this listener
|
|
15
|
+
* shares the window with whatever else the page runs.
|
|
16
|
+
*/
|
|
17
|
+
/** How often the game announces itself until the page answers. */
|
|
18
|
+
const HELLO_INTERVAL_MS = 250;
|
|
19
|
+
/** How long to wait for the page before concluding this is not an embed. */
|
|
20
|
+
const DEFAULT_TIMEOUT_MS = 3_000;
|
|
21
|
+
function windowEndpoint() {
|
|
22
|
+
if (typeof window === 'undefined')
|
|
23
|
+
return null;
|
|
24
|
+
return {
|
|
25
|
+
listen(handler) {
|
|
26
|
+
const onMessage = (event) => handler(event.data, event.origin, event.source);
|
|
27
|
+
window.addEventListener('message', onMessage);
|
|
28
|
+
return () => window.removeEventListener('message', onMessage);
|
|
29
|
+
},
|
|
30
|
+
post(message, targetOrigin) {
|
|
31
|
+
window.parent.postMessage(message, targetOrigin);
|
|
32
|
+
},
|
|
33
|
+
parentSource() {
|
|
34
|
+
return window.parent === window ? null : window.parent;
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function parentOriginFromLocation() {
|
|
39
|
+
if (typeof window === 'undefined')
|
|
40
|
+
return null;
|
|
41
|
+
const origin = new URLSearchParams(window.location.search).get('parentOrigin');
|
|
42
|
+
return origin && origin.length > 0 ? origin : null;
|
|
43
|
+
}
|
|
44
|
+
/** What the game is told when the page refuses a call outright rather than failing a buy. */
|
|
45
|
+
const failureFor = (code) => (code === 'declined' ? 'declined' : 'try-again');
|
|
46
|
+
/**
|
|
47
|
+
* Connect to the embedding page, or learn that there is none.
|
|
48
|
+
*
|
|
49
|
+
* Resolves null when the game is not in an iframe, when no `parentOrigin` was provided, or when
|
|
50
|
+
* the page does not answer within the timeout — a page that answers late was never going to
|
|
51
|
+
* service a buy either. After it resolves, individual Host calls have no timeout of their own:
|
|
52
|
+
* sign-in and buy legitimately wait on a human, and the page always answers, refusals included.
|
|
53
|
+
*/
|
|
54
|
+
export async function connectHost(options = {}) {
|
|
55
|
+
const endpoint = options.endpoint ?? windowEndpoint();
|
|
56
|
+
if (!endpoint)
|
|
57
|
+
return null;
|
|
58
|
+
const parentOrigin = options.parentOrigin ?? parentOriginFromLocation();
|
|
59
|
+
if (!parentOrigin)
|
|
60
|
+
return null;
|
|
61
|
+
const parent = endpoint.parentSource();
|
|
62
|
+
if (parent === null)
|
|
63
|
+
return null;
|
|
13
64
|
const pending = new Map();
|
|
65
|
+
let unlisten = () => { };
|
|
14
66
|
let disposed = false;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
context = Object.freeze({ ...message.context, launch: immutableLaunch(message.context.launch) });
|
|
46
|
-
resolveContext(context);
|
|
47
|
-
}
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
if (message.type === 'market') {
|
|
51
|
-
const parsed = marketFrom(message.market);
|
|
52
|
-
if (parsed)
|
|
53
|
-
market.set(parsed);
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
const waiting = pending.get(message.id);
|
|
57
|
-
if (!waiting)
|
|
58
|
-
return;
|
|
59
|
-
if (message.type === 'buy-progress') {
|
|
60
|
-
waiting.progress?.(message.transactionHash
|
|
61
|
-
? { state: 'pending', transactionHash: message.transactionHash }
|
|
62
|
-
: { state: 'pending' });
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
pending.delete(message.id);
|
|
66
|
-
clearTimeout(waiting.timer);
|
|
67
|
-
if (!message.ok) {
|
|
68
|
-
if (waiting.method === 'buy') {
|
|
69
|
-
const reason = message.error === 'no-wallet' ? 'try-again' : message.error;
|
|
70
|
-
waiting.resolve({ failed: { bought: false, reason } });
|
|
67
|
+
const send = (frame) => endpoint.post(frame, parentOrigin);
|
|
68
|
+
const context = await new Promise((resolveContext) => {
|
|
69
|
+
let settled = false;
|
|
70
|
+
const hello = setInterval(() => send({ v: FRAME_PROTOCOL_VERSION, type: 'gm:hello' }), HELLO_INTERVAL_MS);
|
|
71
|
+
const deadline = setTimeout(() => {
|
|
72
|
+
if (settled)
|
|
73
|
+
return;
|
|
74
|
+
settled = true;
|
|
75
|
+
clearInterval(hello);
|
|
76
|
+
unlisten();
|
|
77
|
+
resolveContext(null);
|
|
78
|
+
}, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
79
|
+
unlisten = endpoint.listen((data, origin, source) => {
|
|
80
|
+
// Both checks, always: the origin says who wrote the frame, the source says which window
|
|
81
|
+
// sent it, and either alone can be satisfied by a page we never agreed to trust.
|
|
82
|
+
if (origin !== parentOrigin || source !== parent)
|
|
83
|
+
return;
|
|
84
|
+
const frame = hostFrameFrom(data);
|
|
85
|
+
if (!frame)
|
|
86
|
+
return;
|
|
87
|
+
if (frame.type === 'gm:context') {
|
|
88
|
+
// First context wins; the page re-sends it for every hello and nothing may move the game
|
|
89
|
+
// to a different round mid-flight.
|
|
90
|
+
if (settled)
|
|
91
|
+
return;
|
|
92
|
+
settled = true;
|
|
93
|
+
clearInterval(hello);
|
|
94
|
+
clearTimeout(deadline);
|
|
95
|
+
resolveContext(frame.context);
|
|
96
|
+
return;
|
|
71
97
|
}
|
|
72
|
-
|
|
73
|
-
|
|
98
|
+
if (frame.type === 'gm:progress') {
|
|
99
|
+
const request = pending.get(frame.id);
|
|
100
|
+
if (request?.kind === 'buy') {
|
|
101
|
+
request.progress?.(frame.progress.transactionHash !== undefined
|
|
102
|
+
? { state: 'pending', transactionHash: frame.progress.transactionHash }
|
|
103
|
+
: { state: 'pending' });
|
|
104
|
+
}
|
|
105
|
+
return;
|
|
74
106
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
rejectContext(new Error('the trusted parent did not provide game context'));
|
|
84
|
-
}, timeoutMs);
|
|
85
|
-
post({ channel: EMBED_CHANNEL, v: EMBED_PROTOCOL_VERSION, type: 'ready' });
|
|
86
|
-
return ready.then((trustedContext) => {
|
|
87
|
-
clearTimeout(contextTimer);
|
|
88
|
-
const host = {
|
|
89
|
-
address: async () => {
|
|
90
|
-
const value = await request({ method: 'address' });
|
|
91
|
-
return typeof value === 'string' || value === null ? value : null;
|
|
92
|
-
},
|
|
93
|
-
signIn: async (message) => {
|
|
94
|
-
const value = await request({ method: 'sign-in', message });
|
|
95
|
-
if (typeof value !== 'string' || !/^0x[0-9a-fA-F]+$/.test(value))
|
|
96
|
-
throw new Error('invalid signature response');
|
|
97
|
-
return value;
|
|
98
|
-
},
|
|
99
|
-
sessionEvidence: () => request({ method: 'session-evidence' }),
|
|
100
|
-
buy: async (authorisation, progress) => {
|
|
101
|
-
const value = await request({ method: 'buy', authorisation }, progress);
|
|
102
|
-
if (typeof value !== 'object' || value === null)
|
|
103
|
-
return { failed: { bought: false, reason: 'try-again' } };
|
|
104
|
-
const spent = value.spentWei;
|
|
105
|
-
try {
|
|
106
|
-
const spentWei = BigInt(String(spent));
|
|
107
|
-
return spentWei >= 0n ? { spentWei } : { failed: { bought: false, reason: 'try-again' } };
|
|
107
|
+
// gm:res — terminal, exactly once per id. A late or repeated answer finds nothing here.
|
|
108
|
+
const request = pending.get(frame.id);
|
|
109
|
+
if (!request)
|
|
110
|
+
return;
|
|
111
|
+
pending.delete(frame.id);
|
|
112
|
+
if (request.kind === 'buy') {
|
|
113
|
+
if (!frame.ok) {
|
|
114
|
+
request.resolve({ failed: { bought: false, reason: failureFor(frame.error.code) } });
|
|
108
115
|
}
|
|
109
|
-
|
|
110
|
-
|
|
116
|
+
else if (frame.result.method !== 'buy') {
|
|
117
|
+
request.resolve({ failed: { bought: false, reason: 'try-again' } });
|
|
111
118
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const identity = {
|
|
115
|
-
resolve: async (players) => {
|
|
116
|
-
const value = await request({ method: 'identity', players });
|
|
117
|
-
return isPlayerIdentities(value) ? value : [];
|
|
118
|
-
},
|
|
119
|
-
};
|
|
120
|
-
const marketReadable = {
|
|
121
|
-
current: () => market.current(),
|
|
122
|
-
subscribe: (listener) => market.subscribe(listener),
|
|
123
|
-
};
|
|
124
|
-
return {
|
|
125
|
-
context: trustedContext,
|
|
126
|
-
host,
|
|
127
|
-
platform: { identity, market: marketReadable },
|
|
128
|
-
dispose: () => {
|
|
129
|
-
if (disposed)
|
|
130
|
-
return;
|
|
131
|
-
disposed = true;
|
|
132
|
-
current.removeEventListener('message', onMessage);
|
|
133
|
-
for (const waiting of pending.values()) {
|
|
134
|
-
clearTimeout(waiting.timer);
|
|
135
|
-
waiting.reject(new Error('the embedded connection is closed'));
|
|
119
|
+
else if ('spentWei' in frame.result.outcome) {
|
|
120
|
+
request.resolve({ spentWei: BigInt(frame.result.outcome.spentWei) });
|
|
136
121
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
},
|
|
140
|
-
};
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
function wireMarket(state) {
|
|
144
|
-
return { ...state, trades: state.trades.map((trade) => ({ ...trade, spendWei: trade.spendWei.toString() })) };
|
|
145
|
-
}
|
|
146
|
-
function failure(reason) {
|
|
147
|
-
return ['nothing-to-spend', 'declined', 'not-enough-for-fees', 'window-closed'].includes(reason)
|
|
148
|
-
? reason
|
|
149
|
-
: 'try-again';
|
|
150
|
-
}
|
|
151
|
-
/** Serve one known iframe. Both its origin and WindowProxy are required for every request. */
|
|
152
|
-
export function serveEmbeddedGame(options) {
|
|
153
|
-
const current = options.currentWindow ?? window;
|
|
154
|
-
const active = new Set();
|
|
155
|
-
const acceptedRequestIds = new Set();
|
|
156
|
-
const usedAuthorisations = new Set();
|
|
157
|
-
const requestTimes = [];
|
|
158
|
-
let buyActive = false;
|
|
159
|
-
let signInActive = false;
|
|
160
|
-
let evidenceActive = false;
|
|
161
|
-
let childReady = false;
|
|
162
|
-
let stopped = false;
|
|
163
|
-
const lifetime = new AbortController();
|
|
164
|
-
let latestMarket = options.platform?.market?.current() ?? null;
|
|
165
|
-
const post = (message) => {
|
|
166
|
-
if (!stopped)
|
|
167
|
-
options.frameWindow.postMessage(message, options.gameOrigin);
|
|
168
|
-
};
|
|
169
|
-
const answer = (id, value) => post({ channel: EMBED_CHANNEL, v: EMBED_PROTOCOL_VERSION, type: 'response', id, ok: true, value });
|
|
170
|
-
const refuse = (id, error) => post({ channel: EMBED_CHANNEL, v: EMBED_PROTOCOL_VERSION, type: 'response', id, ok: false, error });
|
|
171
|
-
const run = async (id, request) => {
|
|
172
|
-
if (stopped || acceptedRequestIds.has(id) || acceptedRequestIds.size >= MAX_ACCEPTED_REQUEST_IDS)
|
|
173
|
-
return;
|
|
174
|
-
const now = Date.now();
|
|
175
|
-
while (requestTimes[0] !== undefined && requestTimes[0] <= now - 1_000)
|
|
176
|
-
requestTimes.shift();
|
|
177
|
-
if (active.size >= 16 || requestTimes.length >= 64)
|
|
178
|
-
return;
|
|
179
|
-
requestTimes.push(now);
|
|
180
|
-
acceptedRequestIds.add(id);
|
|
181
|
-
active.add(id);
|
|
182
|
-
try {
|
|
183
|
-
switch (request.method) {
|
|
184
|
-
case 'address':
|
|
185
|
-
answer(id, await options.host.address(lifetime.signal));
|
|
186
|
-
break;
|
|
187
|
-
case 'sign-in':
|
|
188
|
-
if (signInActive) {
|
|
189
|
-
refuse(id, 'try-again');
|
|
190
|
-
break;
|
|
191
|
-
}
|
|
192
|
-
signInActive = true;
|
|
193
|
-
try {
|
|
194
|
-
const address = await options.host.address(lifetime.signal);
|
|
195
|
-
if (stopped)
|
|
196
|
-
break;
|
|
197
|
-
if (!address ||
|
|
198
|
-
!isSessionChallenge(request.message, address, options.context.gateUrl, options.signInDomain)) {
|
|
199
|
-
refuse(id, address ? 'try-again' : 'no-wallet');
|
|
200
|
-
break;
|
|
201
|
-
}
|
|
202
|
-
const signature = await options.host.signIn(request.message, lifetime.signal);
|
|
203
|
-
if (stopped)
|
|
204
|
-
break;
|
|
205
|
-
answer(id, signature);
|
|
206
|
-
}
|
|
207
|
-
finally {
|
|
208
|
-
signInActive = false;
|
|
209
|
-
}
|
|
210
|
-
break;
|
|
211
|
-
case 'session-evidence':
|
|
212
|
-
if (evidenceActive) {
|
|
213
|
-
refuse(id, 'try-again');
|
|
214
|
-
break;
|
|
215
|
-
}
|
|
216
|
-
evidenceActive = true;
|
|
217
|
-
try {
|
|
218
|
-
const evidence = await options.host.sessionEvidence?.(lifetime.signal);
|
|
219
|
-
if (!stopped)
|
|
220
|
-
answer(id, evidence);
|
|
221
|
-
}
|
|
222
|
-
finally {
|
|
223
|
-
evidenceActive = false;
|
|
224
|
-
}
|
|
225
|
-
break;
|
|
226
|
-
case 'identity':
|
|
227
|
-
answer(id, (await options.platform?.identity?.resolve(request.players)) ?? []);
|
|
228
|
-
break;
|
|
229
|
-
case 'buy': {
|
|
230
|
-
if (buyActive) {
|
|
231
|
-
refuse(id, 'try-again');
|
|
232
|
-
break;
|
|
233
|
-
}
|
|
234
|
-
buyActive = true;
|
|
235
|
-
try {
|
|
236
|
-
const address = await options.host.address(lifetime.signal);
|
|
237
|
-
if (stopped)
|
|
238
|
-
break;
|
|
239
|
-
if (!address ||
|
|
240
|
-
address.toLowerCase() !== request.authorisation.buyer.toLowerCase() ||
|
|
241
|
-
request.authorisation.poolId.toLowerCase() !== options.context.launch.poolId.toLowerCase()) {
|
|
242
|
-
refuse(id, address ? 'try-again' : 'no-wallet');
|
|
243
|
-
break;
|
|
244
|
-
}
|
|
245
|
-
if (!isCanonicalSpendHookData(request.authorisation) ||
|
|
246
|
-
!(await options.validateAuthorisation(request.authorisation, options.context, lifetime.signal))) {
|
|
247
|
-
refuse(id, 'try-again');
|
|
248
|
-
break;
|
|
249
|
-
}
|
|
250
|
-
if (stopped)
|
|
251
|
-
break;
|
|
252
|
-
const authorisationId = [
|
|
253
|
-
request.authorisation.buyer.toLowerCase(),
|
|
254
|
-
request.authorisation.poolId.toLowerCase(),
|
|
255
|
-
request.authorisation.nonce,
|
|
256
|
-
].join(':');
|
|
257
|
-
if (usedAuthorisations.has(authorisationId) || usedAuthorisations.size >= 256) {
|
|
258
|
-
refuse(id, 'try-again');
|
|
259
|
-
break;
|
|
260
|
-
}
|
|
261
|
-
usedAuthorisations.add(authorisationId);
|
|
262
|
-
const result = await options.host.buy(request.authorisation, (event) => {
|
|
263
|
-
post({
|
|
264
|
-
channel: EMBED_CHANNEL,
|
|
265
|
-
v: EMBED_PROTOCOL_VERSION,
|
|
266
|
-
type: 'buy-progress',
|
|
267
|
-
id,
|
|
268
|
-
...(event.transactionHash ? { transactionHash: event.transactionHash } : {}),
|
|
269
|
-
});
|
|
270
|
-
}, lifetime.signal);
|
|
271
|
-
if (stopped)
|
|
272
|
-
break;
|
|
273
|
-
if ('failed' in result) {
|
|
274
|
-
refuse(id, result.failed.bought ? 'try-again' : failure(result.failed.reason));
|
|
275
|
-
}
|
|
276
|
-
else {
|
|
277
|
-
answer(id, { spentWei: result.spentWei.toString() });
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
finally {
|
|
281
|
-
buyActive = false;
|
|
282
|
-
}
|
|
283
|
-
break;
|
|
122
|
+
else {
|
|
123
|
+
request.resolve({ failed: frame.result.outcome.failed });
|
|
284
124
|
}
|
|
125
|
+
return;
|
|
285
126
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
active.delete(id);
|
|
292
|
-
}
|
|
293
|
-
};
|
|
294
|
-
const onMessage = (rawEvent) => {
|
|
295
|
-
const event = rawEvent;
|
|
296
|
-
if (event.origin !== options.gameOrigin || event.source !== options.frameWindow)
|
|
297
|
-
return;
|
|
298
|
-
const message = parseGameToHostMessage(event.data);
|
|
299
|
-
if (!message)
|
|
300
|
-
return;
|
|
301
|
-
if (message.type === 'ready') {
|
|
302
|
-
if (childReady)
|
|
127
|
+
if (request.kind === 'address') {
|
|
128
|
+
if (frame.ok && frame.result.method === 'address')
|
|
129
|
+
request.resolve(frame.result.address);
|
|
130
|
+
else
|
|
131
|
+
request.reject(new Error(frame.ok ? 'the page answered the wrong call' : frame.error.message));
|
|
303
132
|
return;
|
|
304
|
-
childReady = true;
|
|
305
|
-
post({ channel: EMBED_CHANNEL, v: EMBED_PROTOCOL_VERSION, type: 'context', context: options.context });
|
|
306
|
-
if (latestMarket) {
|
|
307
|
-
post({ channel: EMBED_CHANNEL, v: EMBED_PROTOCOL_VERSION, type: 'market', market: wireMarket(latestMarket) });
|
|
308
133
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
134
|
+
if (frame.ok && frame.result.method === 'signIn')
|
|
135
|
+
request.resolve(frame.result.signature);
|
|
136
|
+
else
|
|
137
|
+
request.reject(new Error(frame.ok ? 'the page answered the wrong call' : frame.error.message));
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
if (!context)
|
|
141
|
+
return null;
|
|
142
|
+
const requestId = () => globalThis.crypto.randomUUID();
|
|
143
|
+
const host = {
|
|
144
|
+
address: () => new Promise((resolve, reject) => {
|
|
145
|
+
if (disposed)
|
|
146
|
+
return resolve(null);
|
|
147
|
+
const id = requestId();
|
|
148
|
+
pending.set(id, { kind: 'address', resolve, reject });
|
|
149
|
+
send({ v: FRAME_PROTOCOL_VERSION, type: 'gm:req', id, call: { method: 'address' } });
|
|
150
|
+
}),
|
|
151
|
+
signIn: (message) => new Promise((resolve, reject) => {
|
|
152
|
+
if (disposed)
|
|
153
|
+
return reject(new Error('this embed has been disposed'));
|
|
154
|
+
const id = requestId();
|
|
155
|
+
pending.set(id, { kind: 'signIn', resolve, reject });
|
|
156
|
+
send({ v: FRAME_PROTOCOL_VERSION, type: 'gm:req', id, call: { method: 'signIn', message } });
|
|
157
|
+
}),
|
|
158
|
+
buy: (authorisation, progress) => new Promise((resolve) => {
|
|
159
|
+
if (disposed)
|
|
160
|
+
return resolve({ failed: { bought: false, reason: 'try-again' } });
|
|
161
|
+
const id = requestId();
|
|
162
|
+
pending.set(id, { kind: 'buy', resolve, progress });
|
|
163
|
+
// The client's Authorisation is already all strings plus optional hookData, which is
|
|
164
|
+
// exactly the wire shape — nothing to convert, nothing to get wrong.
|
|
165
|
+
send({ v: FRAME_PROTOCOL_VERSION, type: 'gm:req', id, call: { method: 'buy', authorisation } });
|
|
166
|
+
}),
|
|
312
167
|
};
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if (stopped)
|
|
322
|
-
return;
|
|
323
|
-
stopped = true;
|
|
324
|
-
lifetime.abort();
|
|
325
|
-
current.removeEventListener('message', onMessage);
|
|
326
|
-
stopMarket();
|
|
168
|
+
return {
|
|
169
|
+
host,
|
|
170
|
+
context,
|
|
171
|
+
dispose() {
|
|
172
|
+
disposed = true;
|
|
173
|
+
unlisten();
|
|
174
|
+
pending.clear();
|
|
175
|
+
},
|
|
327
176
|
};
|
|
328
177
|
}
|
|
329
178
|
//# sourceMappingURL=embed.js.map
|