@farmslot/adapter-web 0.1.0 → 0.3.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/CHANGELOG.md +10 -0
- package/README.md +92 -3
- package/dist/dapp/index.d.cts +32 -0
- package/dist/dapp/page-script.d.cts +96 -0
- package/dist/dapp/signature-log.d.cts +119 -0
- package/dist/dapp/strict-wallet.d.cts +27 -0
- package/dist/dapp/wallet-requests.d.cts +48 -0
- package/dist/launch-browser.d.cts +106 -0
- package/dist/origin.d.cts +21 -0
- package/dist/slot-title.d.cts +49 -0
- package/package.json +19 -3
- package/src/dapp/index.cjs +75 -0
- package/src/dapp/page-script.cjs +400 -0
- package/src/dapp/signature-log.cjs +314 -0
- package/src/dapp/strict-wallet.cjs +169 -0
- package/src/dapp/wallet-requests.cjs +321 -0
- package/src/launch-browser.cjs +718 -0
- package/src/origin.cjs +53 -0
- package/src/slot-title.cjs +299 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@farmslot/adapter-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Web platform adapter for Farmslot recipes: resolve and launch an isolated Chromium, own its CDP port, load unpacked extensions, and keep macOS focus.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"exports": {
|
|
@@ -16,14 +16,27 @@
|
|
|
16
16
|
"types": "./dist/chrome-args.d.cts",
|
|
17
17
|
"default": "./src/chrome-args.cjs"
|
|
18
18
|
},
|
|
19
|
+
"./dapp": {
|
|
20
|
+
"types": "./dist/dapp/index.d.cts",
|
|
21
|
+
"default": "./src/dapp/index.cjs"
|
|
22
|
+
},
|
|
19
23
|
"./extension-id": {
|
|
20
24
|
"types": "./dist/extension-id.d.cts",
|
|
21
25
|
"default": "./src/extension-id.cjs"
|
|
22
26
|
},
|
|
27
|
+
"./launch-browser": {
|
|
28
|
+
"types": "./dist/launch-browser.d.cts",
|
|
29
|
+
"default": "./src/launch-browser.cjs"
|
|
30
|
+
},
|
|
23
31
|
"./macos-focus": {
|
|
24
32
|
"types": "./dist/macos-focus.d.cts",
|
|
25
33
|
"default": "./src/macos-focus.cjs"
|
|
26
34
|
},
|
|
35
|
+
"./origin": {
|
|
36
|
+
"types": "./dist/origin.d.cts",
|
|
37
|
+
"default": "./src/origin.cjs"
|
|
38
|
+
},
|
|
39
|
+
"./package.json": "./package.json",
|
|
27
40
|
"./page-target": {
|
|
28
41
|
"types": "./dist/page-target.d.cts",
|
|
29
42
|
"default": "./src/page-target.cjs"
|
|
@@ -32,6 +45,10 @@
|
|
|
32
45
|
"types": "./dist/playwright-cdp.d.cts",
|
|
33
46
|
"default": "./src/playwright-cdp.cjs"
|
|
34
47
|
},
|
|
48
|
+
"./slot-title": {
|
|
49
|
+
"types": "./dist/slot-title.d.cts",
|
|
50
|
+
"default": "./src/slot-title.cjs"
|
|
51
|
+
},
|
|
35
52
|
"./validation-launch-supervisor": {
|
|
36
53
|
"types": "./dist/validation-launch-supervisor.d.cts",
|
|
37
54
|
"default": "./src/validation-launch-supervisor.cjs"
|
|
@@ -39,8 +56,7 @@
|
|
|
39
56
|
"./validation-process-ownership": {
|
|
40
57
|
"types": "./dist/validation-process-ownership.d.cts",
|
|
41
58
|
"default": "./src/validation-process-ownership.cjs"
|
|
42
|
-
}
|
|
43
|
-
"./package.json": "./package.json"
|
|
59
|
+
}
|
|
44
60
|
},
|
|
45
61
|
"scripts": {
|
|
46
62
|
"typecheck": "tsc --noEmit --project tsconfig.json && yarn build && tsc --project tsconfig.consumer.json",
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The web-dapp layer of the web adapter: a strict EIP-1193 test wallet, the
|
|
4
|
+
// page script that wraps the app's provider and logs its wallet requests (or
|
|
5
|
+
// injects a provider the host answers), the host's side of that binding, and
|
|
6
|
+
// the request log that wallet assertions read. Product policy (typed data to
|
|
7
|
+
// refuse, the injected wallet's identity, forbidden log entries) is an input.
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
DEFAULT_KNOWN_CHAINS,
|
|
11
|
+
READ_ONLY_RPC_METHODS,
|
|
12
|
+
createStrictWallet,
|
|
13
|
+
} = require('./strict-wallet.cjs');
|
|
14
|
+
const {
|
|
15
|
+
ERROR_CATEGORIES,
|
|
16
|
+
LOGGED_METHODS,
|
|
17
|
+
LOG_BINDING,
|
|
18
|
+
PAGE_MARKER,
|
|
19
|
+
REQUEST_BINDING,
|
|
20
|
+
RESOLVE_FN,
|
|
21
|
+
pageReadyExpression,
|
|
22
|
+
pageScriptSource,
|
|
23
|
+
} = require('./page-script.cjs');
|
|
24
|
+
const { PENDING_CALL_TTL_MS, createWalletRequestBinding } = require('./wallet-requests.cjs');
|
|
25
|
+
const {
|
|
26
|
+
awaitSignatureLog,
|
|
27
|
+
evaluateSignatureLog,
|
|
28
|
+
isTypedDataRequest,
|
|
29
|
+
parseLog,
|
|
30
|
+
readCursor,
|
|
31
|
+
readLog,
|
|
32
|
+
resetWindow,
|
|
33
|
+
shortPrimaryType,
|
|
34
|
+
summarize,
|
|
35
|
+
windowSinceCursor,
|
|
36
|
+
writeCursor,
|
|
37
|
+
writeLogArtifact,
|
|
38
|
+
} = require('./signature-log.cjs');
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {import('./page-script.cjs').TypedDataRefusal} TypedDataRefusal
|
|
42
|
+
* @typedef {import('./page-script.cjs').InjectedWallet} InjectedWallet
|
|
43
|
+
* @typedef {import('./signature-log.cjs').SignatureLog} SignatureLog
|
|
44
|
+
* @typedef {import('./signature-log.cjs').SignaturePolicy} SignaturePolicy
|
|
45
|
+
* @typedef {import('./signature-log.cjs').SignatureSummary} SignatureSummary
|
|
46
|
+
* @typedef {import('./signature-log.cjs').LogEntry} LogEntry
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
DEFAULT_KNOWN_CHAINS,
|
|
51
|
+
ERROR_CATEGORIES,
|
|
52
|
+
LOGGED_METHODS,
|
|
53
|
+
LOG_BINDING,
|
|
54
|
+
PAGE_MARKER,
|
|
55
|
+
PENDING_CALL_TTL_MS,
|
|
56
|
+
READ_ONLY_RPC_METHODS,
|
|
57
|
+
REQUEST_BINDING,
|
|
58
|
+
RESOLVE_FN,
|
|
59
|
+
awaitSignatureLog,
|
|
60
|
+
createStrictWallet,
|
|
61
|
+
createWalletRequestBinding,
|
|
62
|
+
evaluateSignatureLog,
|
|
63
|
+
isTypedDataRequest,
|
|
64
|
+
pageReadyExpression,
|
|
65
|
+
pageScriptSource,
|
|
66
|
+
parseLog,
|
|
67
|
+
readCursor,
|
|
68
|
+
readLog,
|
|
69
|
+
resetWindow,
|
|
70
|
+
shortPrimaryType,
|
|
71
|
+
summarize,
|
|
72
|
+
windowSinceCursor,
|
|
73
|
+
writeCursor,
|
|
74
|
+
writeLogArtifact,
|
|
75
|
+
};
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Source of the script a wallet host evaluates in every app document before
|
|
4
|
+
// page scripts. It reports each wallet request the app makes (method, EIP-712
|
|
5
|
+
// primary type, domain/active chain, outcome; no params or signatures) through
|
|
6
|
+
// a CDP binding, and with the injected signer also provides an EIP-1193
|
|
7
|
+
// provider, announced over EIP-6963, whose requests the host answers. It
|
|
8
|
+
// installs itself only in the top frame of a document on the exact app origin:
|
|
9
|
+
// the CDP preload also runs in iframes and after the tab navigates elsewhere.
|
|
10
|
+
// Errors are logged as a code and a fixed category, never as provider text.
|
|
11
|
+
//
|
|
12
|
+
// Policy is an input: `refuseTypedData` names the typed data the run must never
|
|
13
|
+
// sign (its reason function runs in the page), `injectedWallet` the identity
|
|
14
|
+
// the injected provider announces.
|
|
15
|
+
|
|
16
|
+
const LOG_BINDING = '__farmslotWalletLog';
|
|
17
|
+
const REQUEST_BINDING = '__farmslotWalletRequest';
|
|
18
|
+
const RESOLVE_FN = '__farmslotWalletResolve';
|
|
19
|
+
// The marker the preload leaves on the window; pageReadyExpression reads it.
|
|
20
|
+
const PAGE_MARKER = '__farmslotDapp';
|
|
21
|
+
|
|
22
|
+
const LOGGED_METHODS = Object.freeze([
|
|
23
|
+
'eth_requestAccounts',
|
|
24
|
+
'wallet_requestPermissions',
|
|
25
|
+
'wallet_revokePermissions',
|
|
26
|
+
'wallet_switchEthereumChain',
|
|
27
|
+
'wallet_addEthereumChain',
|
|
28
|
+
'eth_signTypedData',
|
|
29
|
+
'eth_signTypedData_v3',
|
|
30
|
+
'eth_signTypedData_v4',
|
|
31
|
+
'personal_sign',
|
|
32
|
+
'eth_sign',
|
|
33
|
+
'eth_sendTransaction',
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
const ERROR_CATEGORIES = Object.freeze({
|
|
37
|
+
4001: 'user-rejected',
|
|
38
|
+
4100: 'unauthorized',
|
|
39
|
+
4200: 'unsupported-method',
|
|
40
|
+
4900: 'disconnected',
|
|
41
|
+
4901: 'chain-disconnected',
|
|
42
|
+
4902: 'unrecognized-chain',
|
|
43
|
+
[-32602]: 'invalid-params',
|
|
44
|
+
[-32603]: 'internal',
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
function pageMain(config, refusalReason) {
|
|
48
|
+
if (window.top !== window || location.origin !== config.appOrigin) return;
|
|
49
|
+
if (window[config.marker]) return;
|
|
50
|
+
// The wallet host's ready check reads this marker on the committed document:
|
|
51
|
+
// the preload ran, and the page's provider is wrapped (covers).
|
|
52
|
+
const wrapped = new WeakSet();
|
|
53
|
+
const harness = {
|
|
54
|
+
signer: config.signer,
|
|
55
|
+
refusesTypedData: Boolean(config.refusal),
|
|
56
|
+
wrapped: 0,
|
|
57
|
+
wrapFailures: [],
|
|
58
|
+
covers: (provider) => Boolean(provider) && wrapped.has(provider),
|
|
59
|
+
};
|
|
60
|
+
window[config.marker] = harness;
|
|
61
|
+
const logged = new Set(config.loggedMethods);
|
|
62
|
+
// Without the host's binding a wallet request would go unrecorded, so it is
|
|
63
|
+
// refused (fail closed) rather than passed through.
|
|
64
|
+
const unrecorded = () =>
|
|
65
|
+
Object.assign(
|
|
66
|
+
new Error(
|
|
67
|
+
'The harness wallet host is not attached to this document; the request was refused so it cannot go unrecorded.',
|
|
68
|
+
),
|
|
69
|
+
{ code: 4100 },
|
|
70
|
+
);
|
|
71
|
+
const hasBinding = (binding) => typeof window[binding] === 'function';
|
|
72
|
+
const report = (entry) => {
|
|
73
|
+
try {
|
|
74
|
+
window[config.logBinding](JSON.stringify(entry));
|
|
75
|
+
} catch {
|
|
76
|
+
// Binding unavailable (host detached): the request still proceeds.
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const describe = (method, params) => {
|
|
80
|
+
const entry = { kind: 'request', method, t: Date.now(), url: location.pathname };
|
|
81
|
+
if (/^eth_signTypedData/.test(method)) {
|
|
82
|
+
try {
|
|
83
|
+
const raw = params?.[1];
|
|
84
|
+
const data = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
85
|
+
entry.primaryType = data?.primaryType ?? null;
|
|
86
|
+
entry.domainName = data?.domain?.name ?? null;
|
|
87
|
+
entry.domainChainId =
|
|
88
|
+
data?.domain?.chainId == null ? null : Number(BigInt(data.domain.chainId));
|
|
89
|
+
} catch {
|
|
90
|
+
entry.primaryType = null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (method === 'wallet_switchEthereumChain' || method === 'wallet_addEthereumChain') {
|
|
94
|
+
try {
|
|
95
|
+
entry.toChainId = Number.parseInt(params[0].chainId, 16);
|
|
96
|
+
} catch {
|
|
97
|
+
entry.toChainId = null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return entry;
|
|
101
|
+
};
|
|
102
|
+
const wrap = (provider, source) => {
|
|
103
|
+
if (!provider || typeof provider.request !== 'function' || wrapped.has(provider))
|
|
104
|
+
return provider;
|
|
105
|
+
const original = provider.request.bind(provider);
|
|
106
|
+
const request = async (args) => {
|
|
107
|
+
const method = args?.method;
|
|
108
|
+
if (!logged.has(method)) return original(args);
|
|
109
|
+
if (!hasBinding(config.logBinding)) throw unrecorded();
|
|
110
|
+
const entry = { ...describe(method, args?.params), source };
|
|
111
|
+
// Typed data the run must never sign never reaches the signer. Data
|
|
112
|
+
// that does not parse is left to the signer to reject; a check that
|
|
113
|
+
// throws refuses.
|
|
114
|
+
if (config.refusal && /^eth_signTypedData/.test(method)) {
|
|
115
|
+
let data;
|
|
116
|
+
let parsed = true;
|
|
117
|
+
try {
|
|
118
|
+
const raw = args?.params?.[1];
|
|
119
|
+
data = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
120
|
+
} catch {
|
|
121
|
+
parsed = false;
|
|
122
|
+
}
|
|
123
|
+
let reason = null;
|
|
124
|
+
if (parsed) {
|
|
125
|
+
try {
|
|
126
|
+
reason = refusalReason(data);
|
|
127
|
+
} catch {
|
|
128
|
+
reason = config.refusal.checkFailed;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (reason) {
|
|
132
|
+
report({
|
|
133
|
+
...entry,
|
|
134
|
+
kind: config.refusal.kind,
|
|
135
|
+
reason,
|
|
136
|
+
outcome: 'refused',
|
|
137
|
+
errorCode: 4100,
|
|
138
|
+
});
|
|
139
|
+
throw Object.assign(new Error(config.refusal.message), { code: 4100 });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
entry.activeChainId = Number.parseInt(await original({ method: 'eth_chainId' }), 16);
|
|
144
|
+
} catch {
|
|
145
|
+
entry.activeChainId = null;
|
|
146
|
+
}
|
|
147
|
+
const started = Date.now();
|
|
148
|
+
try {
|
|
149
|
+
const result = await original(args);
|
|
150
|
+
entry.outcome = /sign/i.test(method) ? 'signed' : 'approved';
|
|
151
|
+
entry.durationMs = Date.now() - started;
|
|
152
|
+
report(entry);
|
|
153
|
+
return result;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
const code = Number.isInteger(error?.code) ? error.code : null;
|
|
156
|
+
entry.outcome = code === 4001 ? 'rejected' : 'error';
|
|
157
|
+
entry.errorCode = code;
|
|
158
|
+
entry.errorCategory = config.errorCategories[code] ?? 'other';
|
|
159
|
+
entry.durationMs = Date.now() - started;
|
|
160
|
+
report(entry);
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
try {
|
|
165
|
+
provider.request = request;
|
|
166
|
+
} catch {
|
|
167
|
+
// Read-only property: define it instead (below).
|
|
168
|
+
}
|
|
169
|
+
if (provider.request !== request) {
|
|
170
|
+
try {
|
|
171
|
+
Object.defineProperty(provider, 'request', {
|
|
172
|
+
value: request,
|
|
173
|
+
configurable: true,
|
|
174
|
+
writable: true,
|
|
175
|
+
});
|
|
176
|
+
} catch {
|
|
177
|
+
// Frozen provider: recorded below.
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// A provider whose request could not be replaced would sign unrecorded.
|
|
181
|
+
if (provider.request !== request) {
|
|
182
|
+
harness.wrapFailures.push(source);
|
|
183
|
+
return provider;
|
|
184
|
+
}
|
|
185
|
+
wrapped.add(provider);
|
|
186
|
+
harness.wrapped += 1;
|
|
187
|
+
return provider;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
if (config.signer === 'injected') {
|
|
191
|
+
const listeners = {};
|
|
192
|
+
const emit = (event, ...args) =>
|
|
193
|
+
(listeners[event] ?? []).forEach((fn) => {
|
|
194
|
+
try {
|
|
195
|
+
fn(...args);
|
|
196
|
+
} catch {
|
|
197
|
+
/* listener errors stay in the app */
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
const pending = new Map();
|
|
201
|
+
let nextId = 1;
|
|
202
|
+
window[config.resolveFn] = (id, outcome) => {
|
|
203
|
+
const entry = pending.get(id);
|
|
204
|
+
if (!entry) return;
|
|
205
|
+
pending.delete(id);
|
|
206
|
+
if (outcome.error)
|
|
207
|
+
entry.reject(Object.assign(new Error(outcome.error.message), { code: outcome.error.code }));
|
|
208
|
+
else entry.resolve(outcome.result);
|
|
209
|
+
};
|
|
210
|
+
let lastChain = null;
|
|
211
|
+
const provider = {
|
|
212
|
+
...(config.injectedWallet.isMetaMask
|
|
213
|
+
? { isMetaMask: true, _metamask: { isUnlocked: async () => true } }
|
|
214
|
+
: {}),
|
|
215
|
+
request: ({ method, params }) =>
|
|
216
|
+
new Promise((resolve, reject) => {
|
|
217
|
+
if (!hasBinding(config.requestBinding)) {
|
|
218
|
+
reject(unrecorded());
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const id = nextId++;
|
|
222
|
+
pending.set(id, { resolve, reject });
|
|
223
|
+
window[config.requestBinding](JSON.stringify({ id, method, params }));
|
|
224
|
+
}).then((result) => {
|
|
225
|
+
if (method === 'wallet_switchEthereumChain' && params?.[0]?.chainId !== lastChain) {
|
|
226
|
+
lastChain = params[0].chainId;
|
|
227
|
+
emit('chainChanged', lastChain);
|
|
228
|
+
}
|
|
229
|
+
if (method === 'eth_chainId') lastChain = result;
|
|
230
|
+
if (method === 'eth_requestAccounts') emit('connect', { chainId: lastChain ?? '0x1' });
|
|
231
|
+
return result;
|
|
232
|
+
}),
|
|
233
|
+
on: (event, fn) => {
|
|
234
|
+
(listeners[event] ??= []).push(fn);
|
|
235
|
+
return provider;
|
|
236
|
+
},
|
|
237
|
+
removeListener: (event, fn) => {
|
|
238
|
+
listeners[event] = (listeners[event] ?? []).filter((f) => f !== fn);
|
|
239
|
+
return provider;
|
|
240
|
+
},
|
|
241
|
+
enable: () => provider.request({ method: 'eth_requestAccounts' }),
|
|
242
|
+
};
|
|
243
|
+
wrap(provider, 'injected-strict-wallet');
|
|
244
|
+
window.ethereum = provider;
|
|
245
|
+
const info = config.injectedWallet.info;
|
|
246
|
+
const announce = () =>
|
|
247
|
+
window.dispatchEvent(
|
|
248
|
+
new CustomEvent('eip6963:announceProvider', { detail: Object.freeze({ info, provider }) }),
|
|
249
|
+
);
|
|
250
|
+
window.addEventListener('eip6963:requestProvider', announce);
|
|
251
|
+
announce();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Extension mode: wrap whatever provider the wallet extension injects,
|
|
256
|
+
// however the app reaches it (window.ethereum or an EIP-6963 announcement).
|
|
257
|
+
let current = window.ethereum ? wrap(window.ethereum, 'window.ethereum') : undefined;
|
|
258
|
+
try {
|
|
259
|
+
Object.defineProperty(window, 'ethereum', {
|
|
260
|
+
configurable: true,
|
|
261
|
+
get: () => current,
|
|
262
|
+
set: (value) => {
|
|
263
|
+
current = wrap(value, 'window.ethereum');
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
} catch {
|
|
267
|
+
// A non-configurable window.ethereum is still wrapped by the poll below.
|
|
268
|
+
}
|
|
269
|
+
window.addEventListener(
|
|
270
|
+
'eip6963:announceProvider',
|
|
271
|
+
(event) => {
|
|
272
|
+
wrap(event.detail?.provider, `eip6963:${event.detail?.info?.rdns ?? 'unknown'}`);
|
|
273
|
+
},
|
|
274
|
+
true,
|
|
275
|
+
);
|
|
276
|
+
let polls = 0;
|
|
277
|
+
const poll = setInterval(() => {
|
|
278
|
+
if (window.ethereum) wrap(window.ethereum, 'window.ethereum');
|
|
279
|
+
if (++polls > 200) clearInterval(poll);
|
|
280
|
+
}, 50);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// The reason recorded when a refusal check throws: the request is refused.
|
|
284
|
+
const REFUSAL_CHECK_FAILED = 'the refusal check threw';
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Refuse a policy the page or the host could not apply: kind and message must
|
|
288
|
+
* be non-empty strings, and reason a function whose source is an expression
|
|
289
|
+
* in the page (a function declaration or an arrow; not a method shorthand, a
|
|
290
|
+
* bound or a native function).
|
|
291
|
+
* @param {TypedDataRefusal | null | undefined} refuseTypedData
|
|
292
|
+
*/
|
|
293
|
+
function assertTypedDataRefusal(refuseTypedData) {
|
|
294
|
+
if (!refuseTypedData) return;
|
|
295
|
+
if (typeof refuseTypedData.reason !== 'function') {
|
|
296
|
+
throw new Error('refuseTypedData.reason must be a function.');
|
|
297
|
+
}
|
|
298
|
+
for (const field of ['kind', 'message']) {
|
|
299
|
+
if (typeof refuseTypedData[field] !== 'string' || !refuseTypedData[field].trim()) {
|
|
300
|
+
throw new Error(`refuseTypedData.${field} must be a non-empty string.`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
try {
|
|
304
|
+
new Function(`return (${refuseTypedData.reason.toString()});`);
|
|
305
|
+
} catch {
|
|
306
|
+
throw new Error(
|
|
307
|
+
'refuseTypedData.reason must be a function declaration or an arrow function; a method shorthand, bound or native function does not serialize into the page.',
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* @typedef {object} TypedDataRefusal
|
|
314
|
+
* @property {(typedData: any) => string | null} reason Why the run must not sign this typed data, or null. Runs in the page too: self-contained (no closure, no imports).
|
|
315
|
+
* @property {string} kind The log entry kind of a refusal.
|
|
316
|
+
* @property {string} message The refusal's error message (code 4100).
|
|
317
|
+
*/
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* @typedef {object} InjectedWallet
|
|
321
|
+
* @property {{ uuid: string, name: string, icon: string, rdns: string }} info EIP-6963 provider info to announce.
|
|
322
|
+
* @property {boolean} [isMetaMask] Present the provider as MetaMask (`isMetaMask`, `_metamask.isUnlocked`).
|
|
323
|
+
*/
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* The preload for one app origin.
|
|
327
|
+
* - `signer`: 'extension' wraps the provider a wallet extension injects;
|
|
328
|
+
* 'injected' provides one whose requests go to the host (REQUEST_BINDING).
|
|
329
|
+
* - `refuseTypedData`: `{ reason, kind, message }` or null. `reason(typedData)`
|
|
330
|
+
* returns why the run must not sign it, or null; it runs in the page, so it
|
|
331
|
+
* must be a self-contained function (no closure, no imports). A refusal is
|
|
332
|
+
* logged as `kind` and answered with code 4100 and `message`.
|
|
333
|
+
* - `injectedWallet` (injected signer): `{ info, isMetaMask }`, the EIP-6963
|
|
334
|
+
* provider info to announce and whether the provider presents as MetaMask.
|
|
335
|
+
*
|
|
336
|
+
* @param {{ signer: 'extension' | 'injected', appOrigin: string, refuseTypedData?: TypedDataRefusal | null, injectedWallet?: InjectedWallet }} options
|
|
337
|
+
* @returns {string}
|
|
338
|
+
*/
|
|
339
|
+
function pageScriptSource({ signer, appOrigin, refuseTypedData = null, injectedWallet }) {
|
|
340
|
+
if (signer !== 'extension' && signer !== 'injected') {
|
|
341
|
+
throw new Error(`signer must be extension or injected, got ${JSON.stringify(signer)}.`);
|
|
342
|
+
}
|
|
343
|
+
assertTypedDataRefusal(refuseTypedData);
|
|
344
|
+
if (signer === 'injected' && !injectedWallet?.info?.rdns) {
|
|
345
|
+
throw new Error('the injected signer needs injectedWallet.info (EIP-6963 provider info).');
|
|
346
|
+
}
|
|
347
|
+
const config = {
|
|
348
|
+
signer,
|
|
349
|
+
appOrigin: new URL(appOrigin).origin,
|
|
350
|
+
refusal: refuseTypedData
|
|
351
|
+
? {
|
|
352
|
+
kind: refuseTypedData.kind,
|
|
353
|
+
message: refuseTypedData.message,
|
|
354
|
+
checkFailed: REFUSAL_CHECK_FAILED,
|
|
355
|
+
}
|
|
356
|
+
: null,
|
|
357
|
+
injectedWallet:
|
|
358
|
+
signer === 'injected'
|
|
359
|
+
? { info: injectedWallet.info, isMetaMask: injectedWallet.isMetaMask === true }
|
|
360
|
+
: null,
|
|
361
|
+
errorCategories: ERROR_CATEGORIES,
|
|
362
|
+
loggedMethods: LOGGED_METHODS,
|
|
363
|
+
marker: PAGE_MARKER,
|
|
364
|
+
logBinding: LOG_BINDING,
|
|
365
|
+
requestBinding: REQUEST_BINDING,
|
|
366
|
+
resolveFn: RESOLVE_FN,
|
|
367
|
+
};
|
|
368
|
+
const reason = refuseTypedData ? refuseTypedData.reason.toString() : 'null';
|
|
369
|
+
return `(${pageMain.toString()})(${JSON.stringify(config)}, ${reason});`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* An expression that is true on a document whose preload is installed for
|
|
374
|
+
* this signer and policy, whose bindings are attached, and whose provider the
|
|
375
|
+
* app sees (window.ethereum) is wrapped with no wrap failure.
|
|
376
|
+
*
|
|
377
|
+
* @param {{ signer: 'extension' | 'injected', refusesTypedData: boolean }} options
|
|
378
|
+
* @returns {string}
|
|
379
|
+
*/
|
|
380
|
+
function pageReadyExpression({ signer, refusesTypedData }) {
|
|
381
|
+
return [
|
|
382
|
+
`typeof window.${LOG_BINDING} === 'function'`,
|
|
383
|
+
...(signer === 'injected' ? [`typeof window.${REQUEST_BINDING} === 'function'`] : []),
|
|
384
|
+
`Boolean(window.${PAGE_MARKER} && window.${PAGE_MARKER}.signer === ${JSON.stringify(signer)} && window.${PAGE_MARKER}.refusesTypedData === ${refusesTypedData === true})`,
|
|
385
|
+
`window.${PAGE_MARKER}.wrapFailures.length === 0 && window.${PAGE_MARKER}.covers(window.ethereum)`,
|
|
386
|
+
].join(' && ');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
module.exports = {
|
|
390
|
+
ERROR_CATEGORIES,
|
|
391
|
+
LOGGED_METHODS,
|
|
392
|
+
LOG_BINDING,
|
|
393
|
+
PAGE_MARKER,
|
|
394
|
+
REQUEST_BINDING,
|
|
395
|
+
REFUSAL_CHECK_FAILED,
|
|
396
|
+
RESOLVE_FN,
|
|
397
|
+
assertTypedDataRefusal,
|
|
398
|
+
pageReadyExpression,
|
|
399
|
+
pageScriptSource,
|
|
400
|
+
};
|