Skip to content

Commit 4aee222

Browse files
committed
feat(dapp-browser-eip1193)_: js object wrappers (eip1193, eip-6963)
1 parent 2097d2e commit 4aee222

File tree

4 files changed

+836
-0
lines changed

4 files changed

+836
-0
lines changed

ui/app/AppLayouts/DAppBrowser/core/js/eip6963_announcer.js

Lines changed: 50 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// WebChannel integration with retry mechanism
2+
function initializeWebChannel() {
3+
if (typeof qt !== 'undefined' && qt.webChannelTransport) {
4+
console.log("[Ethereum Injector] WebChannel transport available, initializing...");
5+
6+
try {
7+
new QWebChannel(qt.webChannelTransport, setupEthereumProvider);
8+
} catch (error) {
9+
console.error("[Ethereum Injector] Error initializing WebChannel:", error);
10+
}
11+
} else {
12+
// console.log("[Ethereum Injector] WebChannel transport not available, retrying...");
13+
// Retry after a short delay
14+
// setTimeout(initializeWebChannel, 100);
15+
}
16+
}
17+
18+
// Start initialization
19+
initializeWebChannel();
20+
21+
// Setup Ethereum provider
22+
function setupEthereumProvider(channel) {
23+
// Get the EIP-1193 provider QtObject object (WebChannel.id = "ethereumProvider")
24+
window.ethereumProvider = channel.objects.ethereumProvider;
25+
26+
if (!window.ethereumProvider) {
27+
console.error("[Ethereum Injector] ethereumProvider not found in channel.objects");
28+
return;
29+
}
30+
31+
console.log("[Ethereum Injector] ethereumProvider exposed to window");
32+
33+
// Install the EIP-1193 js wrapper
34+
if (typeof EthereumWrapper !== 'undefined' && EthereumWrapper.install) {
35+
EthereumWrapper.install();
36+
} else {
37+
console.error("[Ethereum Injector] EthereumWrapper not available");
38+
}
39+
}
40+
41+
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
"use strict";
2+
3+
// IIFE start (https://developer.mozilla.org/ru/docs/Glossary/IIFE)
4+
// Guard against multiple script loads
5+
const EthereumWrapper = (function() {
6+
// If already loaded, return existing instance
7+
if (window.__ETHEREUM_WRAPPER_INSTANCE__) {
8+
return window.__ETHEREUM_WRAPPER_INSTANCE__;
9+
}
10+
11+
// Manages EIP-1193 provider wrapper around Qml ethereum object (EIP1193ProviderAdapter.qml)
12+
class EthereumProvider extends EventTarget {
13+
constructor(nativeEthereum) {
14+
super();
15+
16+
if (!nativeEthereum) {
17+
console.error("[Ethereum Wrapper] nativeEthereum is not available");
18+
return null;
19+
}
20+
21+
this.listeners = new Map(); // event -> Set<handler>
22+
this.nativeEthereum = nativeEthereum;
23+
this.requestIdCounter = 1; // async requests
24+
this.pendingRequests = new Map(); // requestId -> { resolve, reject }
25+
26+
// Wire native signals to events
27+
this._wireSignals();
28+
29+
// Set up EIP-1193 properties
30+
this.isStatus = true;
31+
this.isMetaMask = false;
32+
this.chainId = null; // Will be set on first eth_chainId request or chainChanged event
33+
this._connected = false;
34+
}
35+
36+
_connectSignal(eventName, handler) {
37+
const event = this.nativeEthereum[eventName];
38+
if (event && event.connect) {
39+
event.connect(handler);
40+
return true;
41+
}
42+
return false;
43+
}
44+
45+
46+
_wireSignals() {
47+
this._connectSignal('connectEvent', (info) => {
48+
this._connected = true;
49+
if (info && info.chainId) {
50+
this.chainId = info.chainId;
51+
}
52+
this._emit('connect', info);
53+
});
54+
55+
this._connectSignal('disconnectEvent', (error) => {
56+
this._connected = false;
57+
this._emit('disconnect', error);
58+
});
59+
60+
this._connectSignal('messageEvent', (message) => {
61+
this._emit('message', message);
62+
});
63+
64+
this._connectSignal('chainChangedEvent', (chainId) => {
65+
this.chainId = chainId;
66+
this._emit('chainChanged', chainId);
67+
});
68+
69+
this._connectSignal('accountsChangedEvent', (accounts) => {
70+
this._emit('accountsChanged', accounts);
71+
});
72+
73+
const hasAsyncEvents = this._connectSignal('requestCompletedEvent',
74+
this.handleRequestCompleted.bind(this)
75+
);
76+
77+
if (!hasAsyncEvents) {
78+
console.warn('[Ethereum Wrapper] requestCompletedEvent not available on native provider');
79+
}
80+
81+
this._hasAsyncEvents = hasAsyncEvents;
82+
}
83+
84+
_emit(event, ...args) {
85+
const set = this.listeners.get(event);
86+
if (!set) return;
87+
for (const handler of set) {
88+
try {
89+
handler(...args);
90+
} catch (e) {
91+
console.error("[Ethereum Wrapper] handler error", e);
92+
}
93+
}
94+
}
95+
96+
request(args) {
97+
if (!args || typeof args !== 'object' || !args.method) {
98+
return Promise.reject(new Error('Invalid request: missing method'));
99+
}
100+
const requestId = this.requestIdCounter++;
101+
const payload = Object.assign({}, args, { requestId });
102+
103+
return new Promise((resolve, reject) => {
104+
this.pendingRequests.set(requestId, { resolve, reject, method: args.method });
105+
106+
try {
107+
const nativeResp = this.nativeEthereum.request(payload);
108+
if (nativeResp && typeof nativeResp === 'object' && nativeResp.error) {
109+
this.pendingRequests.delete(requestId);
110+
reject(nativeResp.error);
111+
} else if (nativeResp && nativeResp.result !== undefined && !this._hasAsyncEvents) {
112+
this.pendingRequests.delete(requestId);
113+
resolve(nativeResp.result);
114+
}
115+
} catch (e) {
116+
this.pendingRequests.delete(requestId);
117+
reject(e);
118+
}
119+
});
120+
}
121+
122+
_updateStateFromResponse(method, result) {
123+
if (method === 'eth_chainId' && result && this.chainId !== result) {
124+
this.chainId = result;
125+
}
126+
}
127+
128+
_processResponse(resp, method, entry) {
129+
if (resp && typeof resp === 'string') {
130+
try {
131+
const parsed = JSON.parse(resp);
132+
resp = parsed;
133+
} catch (e) {
134+
entry.resolve(resp);
135+
return;
136+
}
137+
}
138+
139+
if (resp && resp.error) {
140+
entry.reject(resp.error);
141+
} else if (resp && resp.result !== undefined) {
142+
this._updateStateFromResponse(method, resp.result);
143+
entry.resolve(resp.result);
144+
} else {
145+
entry.resolve(resp);
146+
}
147+
}
148+
149+
handleRequestCompleted(payload) {
150+
try {
151+
const requestId = payload && (payload.requestId || (payload.response && payload.response.id)) || 0;
152+
const entry = this.pendingRequests.get(requestId);
153+
154+
if (!entry) {
155+
console.warn("[Ethereum Wrapper] No pending request found for ID:", requestId);
156+
return;
157+
}
158+
159+
this.pendingRequests.delete(requestId);
160+
this._processResponse(payload && payload.response, entry.method, entry);
161+
} catch (e) {
162+
console.error('[Ethereum Wrapper] requestCompletedEvent handler error', e);
163+
}
164+
}
165+
166+
on(event, handler) {
167+
if (typeof handler !== 'function') return this;
168+
const set = this.listeners.get(event) || new Set();
169+
set.add(handler);
170+
this.listeners.set(event, set);
171+
return this;
172+
}
173+
174+
once(event, handler) {
175+
if (typeof handler !== 'function') return this;
176+
const self = this;
177+
function onceHandler() {
178+
try {
179+
handler.apply(null, arguments);
180+
} finally {
181+
self.removeListener(event, onceHandler);
182+
}
183+
}
184+
return this.on(event, onceHandler);
185+
}
186+
187+
removeListener(event, handler) {
188+
const set = this.listeners.get(event);
189+
if (!set) return this;
190+
set.delete(handler);
191+
if (set.size === 0) this.listeners.delete(event);
192+
return this;
193+
}
194+
195+
removeAllListeners(event) {
196+
if (event) {
197+
this.listeners.delete(event);
198+
} else {
199+
this.listeners.clear();
200+
}
201+
return this;
202+
}
203+
204+
// Deprecated aliases for compatibility
205+
addListener(event, handler) {
206+
return this.on(event, handler);
207+
}
208+
209+
off(event, handler) {
210+
return this.removeListener(event, handler);
211+
}
212+
}
213+
214+
function install() {
215+
if (!window.ethereumProvider) {
216+
return false;
217+
}
218+
219+
if (window.__ETHEREUM_INSTALLED__) {
220+
return true;
221+
}
222+
223+
const provider = new EthereumProvider(window.ethereumProvider);
224+
if (!provider) {
225+
console.error('[Ethereum Wrapper] Failed to create EthereumProvider');
226+
return false;
227+
}
228+
229+
if (!window.ethereum) {
230+
Object.defineProperty(window, 'ethereum', {
231+
value: provider,
232+
writable: false,
233+
configurable: false,
234+
enumerable: true
235+
});
236+
window.__ETHEREUM_INSTALLED__ = true;
237+
window.dispatchEvent(new Event('ethereum#initialized'));
238+
return true;
239+
} else {
240+
console.warn('[Ethereum Wrapper] window.ethereum already present; skipping install');
241+
return false;
242+
}
243+
}
244+
245+
function tryInstall() {
246+
if (install()) {
247+
return;
248+
}
249+
250+
let attempts = 0;
251+
const maxAttempts = 20;
252+
const retryInterval = 50;
253+
254+
const retry = () => {
255+
attempts++;
256+
if (install()) {
257+
return;
258+
}
259+
260+
if (attempts < maxAttempts) {
261+
setTimeout(retry, retryInterval * Math.min(attempts, 5));
262+
} else {
263+
console.error('[Ethereum Wrapper] Failed to install after', maxAttempts, 'attempts');
264+
}
265+
};
266+
267+
setTimeout(retry, retryInterval);
268+
}
269+
270+
// Return public API if needed
271+
const instance = {
272+
EthereumProvider: EthereumProvider,
273+
install: install,
274+
tryInstall: tryInstall
275+
};
276+
277+
// Store instance globally to prevent duplicate loading
278+
window.__ETHEREUM_WRAPPER_INSTANCE__ = instance;
279+
280+
return instance;
281+
282+
})(); // IIFE end
283+
284+
// Auto-install on script load (only if this is the first instance)
285+
if (!window.__ETHEREUM_AUTO_INSTALL_CALLED__) {
286+
window.__ETHEREUM_AUTO_INSTALL_CALLED__ = true;
287+
EthereumWrapper.tryInstall();
288+
}
289+
290+

0 commit comments

Comments
 (0)