@aztec/wallet-sdk 0.0.1-commit.fffb133c → 0.0.1-dev
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/dest/base-wallet/base_wallet.d.ts +83 -35
- package/dest/base-wallet/base_wallet.d.ts.map +1 -1
- package/dest/base-wallet/base_wallet.js +231 -70
- package/dest/base-wallet/index.d.ts +3 -2
- package/dest/base-wallet/index.d.ts.map +1 -1
- package/dest/base-wallet/index.js +1 -0
- package/dest/base-wallet/utils.d.ts +50 -0
- package/dest/base-wallet/utils.d.ts.map +1 -0
- package/dest/base-wallet/utils.js +133 -0
- package/dest/crypto.d.ts +39 -1
- package/dest/crypto.d.ts.map +1 -1
- package/dest/crypto.js +88 -0
- package/dest/extension/handlers/background_connection_handler.d.ts +12 -2
- package/dest/extension/handlers/background_connection_handler.d.ts.map +1 -1
- package/dest/extension/handlers/background_connection_handler.js +44 -8
- package/dest/extension/handlers/content_script_connection_handler.d.ts +2 -1
- package/dest/extension/handlers/content_script_connection_handler.d.ts.map +1 -1
- package/dest/extension/handlers/content_script_connection_handler.js +19 -0
- package/dest/extension/handlers/internal_message_types.d.ts +3 -1
- package/dest/extension/handlers/internal_message_types.d.ts.map +1 -1
- package/dest/extension/handlers/internal_message_types.js +3 -1
- package/dest/extension/provider/extension_wallet.d.ts +27 -6
- package/dest/extension/provider/extension_wallet.d.ts.map +1 -1
- package/dest/extension/provider/extension_wallet.js +87 -9
- package/dest/extension/provider/index.d.ts +2 -2
- package/dest/extension/provider/index.d.ts.map +1 -1
- package/dest/iframe/handlers/iframe_connection_handler.d.ts +122 -0
- package/dest/iframe/handlers/iframe_connection_handler.d.ts.map +1 -0
- package/dest/iframe/handlers/iframe_connection_handler.js +240 -0
- package/dest/iframe/handlers/index.d.ts +2 -0
- package/dest/iframe/handlers/index.d.ts.map +1 -0
- package/dest/iframe/handlers/index.js +1 -0
- package/dest/iframe/provider/iframe_discovery.d.ts +25 -0
- package/dest/iframe/provider/iframe_discovery.d.ts.map +1 -0
- package/dest/iframe/provider/iframe_discovery.js +167 -0
- package/dest/iframe/provider/iframe_provider.d.ts +65 -0
- package/dest/iframe/provider/iframe_provider.d.ts.map +1 -0
- package/dest/iframe/provider/iframe_provider.js +257 -0
- package/dest/iframe/provider/iframe_wallet.d.ts +85 -0
- package/dest/iframe/provider/iframe_wallet.d.ts.map +1 -0
- package/dest/iframe/provider/iframe_wallet.js +269 -0
- package/dest/iframe/provider/index.d.ts +4 -0
- package/dest/iframe/provider/index.d.ts.map +1 -0
- package/dest/iframe/provider/index.js +3 -0
- package/dest/manager/types.d.ts +6 -5
- package/dest/manager/types.d.ts.map +1 -1
- package/dest/manager/wallet_manager.d.ts +1 -1
- package/dest/manager/wallet_manager.d.ts.map +1 -1
- package/dest/manager/wallet_manager.js +48 -18
- package/dest/types.d.ts +64 -2
- package/dest/types.d.ts.map +1 -1
- package/dest/types.js +29 -0
- package/package.json +19 -9
- package/src/base-wallet/base_wallet.ts +318 -124
- package/src/base-wallet/index.ts +7 -1
- package/src/base-wallet/utils.ts +240 -0
- package/src/crypto.ts +104 -0
- package/src/extension/handlers/background_connection_handler.ts +42 -9
- package/src/extension/handlers/content_script_connection_handler.ts +18 -0
- package/src/extension/handlers/internal_message_types.ts +2 -0
- package/src/extension/provider/extension_wallet.ts +104 -15
- package/src/extension/provider/index.ts +1 -1
- package/src/iframe/handlers/iframe_connection_handler.ts +342 -0
- package/src/iframe/handlers/index.ts +7 -0
- package/src/iframe/provider/iframe_discovery.ts +185 -0
- package/src/iframe/provider/iframe_provider.ts +331 -0
- package/src/iframe/provider/iframe_wallet.ts +323 -0
- package/src/iframe/provider/index.ts +3 -0
- package/src/manager/types.ts +5 -4
- package/src/manager/wallet_manager.ts +55 -23
- package/src/types.ts +72 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web wallet discovery — creates {@link IframeWalletProvider} instances from a list of URLs.
|
|
3
|
+
*
|
|
4
|
+
* For each configured URL we probe the wallet by loading a tiny invisible iframe,
|
|
5
|
+
* waiting for WALLET_READY, then sending a DISCOVERY request. On a successful
|
|
6
|
+
* DISCOVERY_RESPONSE we emit an IframeWalletProvider to the caller.
|
|
7
|
+
*
|
|
8
|
+
* This is intentionally lightweight (no key exchange yet) — key exchange happens
|
|
9
|
+
* later when the user selects the wallet and calls `provider.establishSecureChannel()`.
|
|
10
|
+
*/
|
|
11
|
+
import type { ChainInfo } from '@aztec/aztec.js/account';
|
|
12
|
+
import { promiseWithResolvers } from '@aztec/foundation/promise';
|
|
13
|
+
|
|
14
|
+
import type { DiscoverySession, WalletProvider } from '../../manager/types.js';
|
|
15
|
+
import { type WalletInfo, WalletMessageType } from '../../types.js';
|
|
16
|
+
import { IframeWalletProvider } from './iframe_provider.js';
|
|
17
|
+
|
|
18
|
+
const PROBE_TIMEOUT_MS = 10_000;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Probes a list of web wallet URLs and returns a {@link DiscoverySession} compatible
|
|
22
|
+
* with WalletManager's `getAvailableWallets()` interface.
|
|
23
|
+
*
|
|
24
|
+
* Discovered {@link IframeWalletProvider} instances are yielded asynchronously as each
|
|
25
|
+
* wallet responds to the probe.
|
|
26
|
+
*
|
|
27
|
+
* @param walletUrls - URLs of web wallets to probe
|
|
28
|
+
* @param chainInfo - Network information to pass during discovery
|
|
29
|
+
* @returns A cancellable discovery session
|
|
30
|
+
*/
|
|
31
|
+
export function discoverWebWallets(walletUrls: string[], chainInfo: ChainInfo): DiscoverySession {
|
|
32
|
+
const { promise: donePromise, resolve: resolveDone } = promiseWithResolvers<void>();
|
|
33
|
+
|
|
34
|
+
/* eslint-disable jsdoc/require-jsdoc */
|
|
35
|
+
type IteratorState =
|
|
36
|
+
| { status: 'discovering'; resolve: ((result: IteratorResult<WalletProvider>) => void) | null }
|
|
37
|
+
| { status: 'done' };
|
|
38
|
+
/* eslint-enable jsdoc/require-jsdoc */
|
|
39
|
+
|
|
40
|
+
let state: IteratorState = { status: 'discovering', resolve: null };
|
|
41
|
+
const pendingProviders: WalletProvider[] = [];
|
|
42
|
+
|
|
43
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
44
|
+
function emit(provider: WalletProvider) {
|
|
45
|
+
if (state.status !== 'discovering') {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (state.resolve) {
|
|
49
|
+
const resolve = state.resolve;
|
|
50
|
+
state.resolve = null;
|
|
51
|
+
resolve({ value: provider, done: false });
|
|
52
|
+
} else {
|
|
53
|
+
pendingProviders.push(provider);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
58
|
+
function markComplete() {
|
|
59
|
+
if (state.status !== 'discovering') {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const pendingResolve = state.resolve;
|
|
63
|
+
state = { status: 'done' };
|
|
64
|
+
resolveDone();
|
|
65
|
+
if (pendingResolve) {
|
|
66
|
+
pendingResolve({ value: undefined as unknown as WalletProvider, done: true });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Probe all URLs in parallel
|
|
71
|
+
const probes = walletUrls.map(url =>
|
|
72
|
+
probeWallet(url, chainInfo, PROBE_TIMEOUT_MS).then(
|
|
73
|
+
provider => {
|
|
74
|
+
if (provider) {
|
|
75
|
+
emit(provider);
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
() => {
|
|
79
|
+
// ignore probe errors
|
|
80
|
+
},
|
|
81
|
+
),
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
void Promise.all(probes).then(markComplete);
|
|
85
|
+
|
|
86
|
+
const wallets: AsyncIterable<WalletProvider> = {
|
|
87
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
88
|
+
[Symbol.asyncIterator](): AsyncIterator<WalletProvider> {
|
|
89
|
+
return {
|
|
90
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
91
|
+
next(): Promise<IteratorResult<WalletProvider>> {
|
|
92
|
+
if (pendingProviders.length > 0) {
|
|
93
|
+
return Promise.resolve({ value: pendingProviders.shift()!, done: false });
|
|
94
|
+
}
|
|
95
|
+
if (state.status === 'done') {
|
|
96
|
+
return Promise.resolve({ value: undefined as unknown as WalletProvider, done: true });
|
|
97
|
+
}
|
|
98
|
+
return new Promise(resolve => {
|
|
99
|
+
if (state.status === 'discovering') {
|
|
100
|
+
state.resolve = resolve;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
105
|
+
return(): Promise<IteratorResult<WalletProvider>> {
|
|
106
|
+
markComplete();
|
|
107
|
+
return Promise.resolve({ value: undefined as unknown as WalletProvider, done: true });
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
wallets,
|
|
115
|
+
done: donePromise,
|
|
116
|
+
cancel: markComplete,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Probes a single web wallet URL.
|
|
122
|
+
* Creates a temporary hidden iframe, waits for WALLET_READY, sends DISCOVERY_REQUEST.
|
|
123
|
+
* Returns an IframeWalletProvider on success, null on timeout/failure.
|
|
124
|
+
* @internal
|
|
125
|
+
*/
|
|
126
|
+
function probeWallet(walletUrl: string, chainInfo: ChainInfo, timeoutMs: number): Promise<IframeWalletProvider | null> {
|
|
127
|
+
const walletOrigin = new URL(walletUrl).origin;
|
|
128
|
+
const iframe = document.createElement('iframe');
|
|
129
|
+
iframe.src = walletUrl;
|
|
130
|
+
iframe.style.cssText = 'display:none;width:0;height:0;border:none;position:absolute;top:-9999px;';
|
|
131
|
+
iframe.allow = 'storage-access; cross-origin-isolated';
|
|
132
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
133
|
+
|
|
134
|
+
// Register listener BEFORE appending to DOM to avoid race with WALLET_READY
|
|
135
|
+
const result = new Promise<IframeWalletProvider | null>(resolve => {
|
|
136
|
+
const cleanup = () => {
|
|
137
|
+
if (iframe.parentNode) {
|
|
138
|
+
iframe.parentNode.removeChild(iframe);
|
|
139
|
+
}
|
|
140
|
+
window.removeEventListener('message', handler);
|
|
141
|
+
clearTimeout(timer);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
timer = setTimeout(() => {
|
|
145
|
+
cleanup();
|
|
146
|
+
resolve(null);
|
|
147
|
+
}, timeoutMs);
|
|
148
|
+
|
|
149
|
+
let step: 'waiting-ready' | 'waiting-discovery' = 'waiting-ready';
|
|
150
|
+
const requestId = globalThis.crypto.randomUUID();
|
|
151
|
+
|
|
152
|
+
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
153
|
+
function handler(event: MessageEvent) {
|
|
154
|
+
if (event.origin !== walletOrigin) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const msg = event.data;
|
|
158
|
+
if (!msg || typeof msg !== 'object') {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (step === 'waiting-ready' && msg.type === WalletMessageType.WALLET_READY) {
|
|
163
|
+
step = 'waiting-discovery';
|
|
164
|
+
iframe.contentWindow?.postMessage(
|
|
165
|
+
{ type: WalletMessageType.DISCOVERY, requestId, appId: 'discovery-probe' },
|
|
166
|
+
walletOrigin,
|
|
167
|
+
);
|
|
168
|
+
} else if (
|
|
169
|
+
step === 'waiting-discovery' &&
|
|
170
|
+
msg.type === WalletMessageType.DISCOVERY_RESPONSE &&
|
|
171
|
+
msg.requestId === requestId
|
|
172
|
+
) {
|
|
173
|
+
const info = msg.walletInfo as WalletInfo;
|
|
174
|
+
cleanup();
|
|
175
|
+
resolve(new IframeWalletProvider(info.id, info.name, info.icon, walletUrl, chainInfo));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
window.addEventListener('message', handler);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
document.body.appendChild(iframe);
|
|
183
|
+
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IframeWalletProvider — implements {@link WalletProvider} for web wallets loaded in iframes.
|
|
3
|
+
*
|
|
4
|
+
* Flow (mirrors ExtensionProvider):
|
|
5
|
+
* 1. Creates an `<iframe src="walletUrl">` (in app-provided container or floating panel)
|
|
6
|
+
* 2. Waits for WALLET_READY message from the iframe
|
|
7
|
+
* 3. Sends DISCOVERY → waits for DISCOVERY_RESPONSE
|
|
8
|
+
* 4. Sends KEY_EXCHANGE_REQUEST (ECDH public key) → waits for KEY_EXCHANGE_RESPONSE
|
|
9
|
+
* 5. Derives shared session keys, exposes verificationHash
|
|
10
|
+
* 6. On confirm(): returns IframeWallet backed by the established session
|
|
11
|
+
*/
|
|
12
|
+
import type { ChainInfo } from '@aztec/aztec.js/account';
|
|
13
|
+
import type { Wallet } from '@aztec/aztec.js/wallet';
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
type ExportedPublicKey,
|
|
17
|
+
deriveSessionKeys,
|
|
18
|
+
exportPublicKey,
|
|
19
|
+
generateKeyPair,
|
|
20
|
+
importPublicKey,
|
|
21
|
+
} from '../../crypto.js';
|
|
22
|
+
import type { PendingConnection, ProviderDisconnectionCallback, WalletProvider } from '../../manager/types.js';
|
|
23
|
+
import type { WalletInfo } from '../../types.js';
|
|
24
|
+
import { WalletMessageType } from '../../types.js';
|
|
25
|
+
import { IframeWallet } from './iframe_wallet.js';
|
|
26
|
+
|
|
27
|
+
const READY_TIMEOUT_MS = 15_000;
|
|
28
|
+
const DISCOVERY_TIMEOUT_MS = 15_000;
|
|
29
|
+
const KEY_EXCHANGE_TIMEOUT_MS = 15_000;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Options for {@link IframeWalletProvider.establishSecureChannel}.
|
|
33
|
+
*/
|
|
34
|
+
export interface IframeConnectionOptions {
|
|
35
|
+
/** Container element to inject the iframe into. If omitted, a floating panel is created. */
|
|
36
|
+
container?: HTMLElement;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A {@link WalletProvider} that connects to a web wallet loaded in a cross-origin iframe.
|
|
41
|
+
*
|
|
42
|
+
* Discovered via {@link discoverWebWallets} and used through the standard
|
|
43
|
+
* `WalletProvider.establishSecureChannel()` flow.
|
|
44
|
+
*/
|
|
45
|
+
export class IframeWalletProvider implements WalletProvider {
|
|
46
|
+
readonly type = 'web' as const;
|
|
47
|
+
|
|
48
|
+
private iframe: HTMLIFrameElement | null = null;
|
|
49
|
+
private _container: HTMLDivElement | null = null;
|
|
50
|
+
private _appOwnsContainer = false;
|
|
51
|
+
private _dragCleanup: (() => void) | null = null;
|
|
52
|
+
private wallet: IframeWallet | null = null;
|
|
53
|
+
private _disconnected = false;
|
|
54
|
+
private disconnectCallbacks: ProviderDisconnectionCallback[] = [];
|
|
55
|
+
|
|
56
|
+
constructor(
|
|
57
|
+
/** Unique wallet identifier. */
|
|
58
|
+
public readonly id: string,
|
|
59
|
+
/** Display name for the wallet. */
|
|
60
|
+
public readonly name: string,
|
|
61
|
+
/** Optional wallet icon URL. */
|
|
62
|
+
public readonly icon: string | undefined,
|
|
63
|
+
private readonly walletUrl: string,
|
|
64
|
+
private readonly chainInfo: ChainInfo,
|
|
65
|
+
) {}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Establishes a secure encrypted channel with the iframe wallet.
|
|
69
|
+
*
|
|
70
|
+
* @param appId - Application identifier for the requesting dApp
|
|
71
|
+
* @param options - Optional container element for the iframe
|
|
72
|
+
* @returns A {@link PendingConnection} with verification hash and confirm/cancel
|
|
73
|
+
*/
|
|
74
|
+
async establishSecureChannel(appId: string, options?: IframeConnectionOptions): Promise<PendingConnection> {
|
|
75
|
+
const iframe = document.createElement('iframe');
|
|
76
|
+
iframe.src = this.walletUrl;
|
|
77
|
+
iframe.style.cssText = 'flex:1;border:none;width:100%;height:100%;display:block;';
|
|
78
|
+
iframe.allow = 'storage-access; cross-origin-isolated';
|
|
79
|
+
this.iframe = iframe;
|
|
80
|
+
|
|
81
|
+
if (options?.container) {
|
|
82
|
+
this._appOwnsContainer = true;
|
|
83
|
+
options.container.appendChild(iframe);
|
|
84
|
+
} else {
|
|
85
|
+
this.createFloatingPanel(iframe);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const walletOrigin = new URL(this.walletUrl).origin;
|
|
89
|
+
|
|
90
|
+
const post = (msg: object) => {
|
|
91
|
+
if (!iframe.contentWindow) {
|
|
92
|
+
throw new Error('Iframe not ready');
|
|
93
|
+
}
|
|
94
|
+
iframe.contentWindow.postMessage(msg, walletOrigin);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
await waitForMessage(msg => msg.type === WalletMessageType.WALLET_READY, READY_TIMEOUT_MS, walletOrigin);
|
|
98
|
+
|
|
99
|
+
const requestId = globalThis.crypto.randomUUID();
|
|
100
|
+
post({ type: WalletMessageType.DISCOVERY, requestId, appId });
|
|
101
|
+
|
|
102
|
+
const discoveryResp = await waitForMessage(
|
|
103
|
+
msg => msg.type === WalletMessageType.DISCOVERY_RESPONSE && msg.requestId === requestId,
|
|
104
|
+
DISCOVERY_TIMEOUT_MS,
|
|
105
|
+
walletOrigin,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const walletInfo = discoveryResp.walletInfo as WalletInfo;
|
|
109
|
+
|
|
110
|
+
const keyPair = await generateKeyPair();
|
|
111
|
+
const dAppPublicKey = await exportPublicKey(keyPair.publicKey);
|
|
112
|
+
post({ type: WalletMessageType.KEY_EXCHANGE_REQUEST, requestId, publicKey: dAppPublicKey });
|
|
113
|
+
|
|
114
|
+
const keyExchangeResp = await waitForMessage(
|
|
115
|
+
msg => msg.type === WalletMessageType.KEY_EXCHANGE_RESPONSE && msg.requestId === requestId,
|
|
116
|
+
KEY_EXCHANGE_TIMEOUT_MS,
|
|
117
|
+
walletOrigin,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const walletPublicKey = await importPublicKey(keyExchangeResp.publicKey as ExportedPublicKey);
|
|
121
|
+
const sessionKeys = await deriveSessionKeys(keyPair, walletPublicKey, true);
|
|
122
|
+
const { verificationHash, encryptionKey: sharedKey } = sessionKeys;
|
|
123
|
+
|
|
124
|
+
const iframeWallet = IframeWallet.create(
|
|
125
|
+
walletInfo.id,
|
|
126
|
+
requestId, // sessionId
|
|
127
|
+
iframe.contentWindow!,
|
|
128
|
+
walletOrigin,
|
|
129
|
+
sharedKey,
|
|
130
|
+
this.chainInfo,
|
|
131
|
+
appId,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
this.wallet = iframeWallet;
|
|
135
|
+
|
|
136
|
+
iframeWallet.onDisconnect(() => {
|
|
137
|
+
this._disconnected = true;
|
|
138
|
+
for (const cb of this.disconnectCallbacks) {
|
|
139
|
+
try {
|
|
140
|
+
cb();
|
|
141
|
+
} catch {
|
|
142
|
+
// ignore
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
let cancelled = false;
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
verificationHash,
|
|
151
|
+
confirm: (): Promise<Wallet> => {
|
|
152
|
+
if (cancelled) {
|
|
153
|
+
throw new Error('Connection was cancelled');
|
|
154
|
+
}
|
|
155
|
+
return Promise.resolve(iframeWallet.asWallet());
|
|
156
|
+
},
|
|
157
|
+
cancel: () => {
|
|
158
|
+
cancelled = true;
|
|
159
|
+
this.cleanup();
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
disconnect(): Promise<void> {
|
|
165
|
+
if (this.wallet && !this.wallet.isDisconnected()) {
|
|
166
|
+
this.wallet.disconnect();
|
|
167
|
+
}
|
|
168
|
+
this.cleanup();
|
|
169
|
+
return Promise.resolve();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
onDisconnect(callback: ProviderDisconnectionCallback): () => void {
|
|
173
|
+
this.disconnectCallbacks.push(callback);
|
|
174
|
+
return () => {
|
|
175
|
+
const i = this.disconnectCallbacks.indexOf(callback);
|
|
176
|
+
if (i !== -1) {
|
|
177
|
+
this.disconnectCallbacks.splice(i, 1);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
isDisconnected(): boolean {
|
|
183
|
+
return this._disconnected;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ── Floating panel creation ─────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
private createFloatingPanel(iframe: HTMLIFrameElement): void {
|
|
189
|
+
const W = 420,
|
|
190
|
+
H = 500;
|
|
191
|
+
const initLeft = window.innerWidth - W - 24;
|
|
192
|
+
const initTop = window.innerHeight - H - 24;
|
|
193
|
+
|
|
194
|
+
const container = document.createElement('div');
|
|
195
|
+
container.style.cssText = `
|
|
196
|
+
position:fixed;left:${initLeft}px;top:${initTop}px;width:${W}px;height:${H}px;
|
|
197
|
+
border-radius:12px;box-shadow:0 8px 32px rgba(0,0,0,0.4);z-index:999999;
|
|
198
|
+
overflow:hidden;display:flex;flex-direction:column;user-select:none;
|
|
199
|
+
`;
|
|
200
|
+
|
|
201
|
+
const dragHandle = document.createElement('div');
|
|
202
|
+
dragHandle.style.cssText = `
|
|
203
|
+
height:28px;min-height:28px;background:rgba(30,30,30,0.95);cursor:grab;
|
|
204
|
+
display:flex;align-items:center;justify-content:center;
|
|
205
|
+
border-bottom:1px solid rgba(255,255,255,0.08);flex-shrink:0;
|
|
206
|
+
`;
|
|
207
|
+
dragHandle.innerHTML = `<span style="color:rgba(255,255,255,0.3);font-size:14px;letter-spacing:4px">⋮⋮⋮</span>`;
|
|
208
|
+
|
|
209
|
+
const resizeHandle = document.createElement('div');
|
|
210
|
+
resizeHandle.style.cssText =
|
|
211
|
+
'position:absolute;bottom:0;right:0;width:16px;height:16px;cursor:se-resize;z-index:1;';
|
|
212
|
+
resizeHandle.innerHTML = `<svg width="16" height="16" style="opacity:0.3;display:block"><path d="M2 14 L14 2 M6 14 L14 6 M10 14 L14 10" stroke="white" stroke-width="1.5"/></svg>`;
|
|
213
|
+
|
|
214
|
+
container.appendChild(dragHandle);
|
|
215
|
+
container.appendChild(iframe);
|
|
216
|
+
container.appendChild(resizeHandle);
|
|
217
|
+
document.body.appendChild(container);
|
|
218
|
+
this._container = container;
|
|
219
|
+
|
|
220
|
+
let dragging = false;
|
|
221
|
+
let dragOffsetX = 0,
|
|
222
|
+
dragOffsetY = 0;
|
|
223
|
+
|
|
224
|
+
dragHandle.addEventListener('mousedown', (e: MouseEvent) => {
|
|
225
|
+
dragging = true;
|
|
226
|
+
dragHandle.style.cursor = 'grabbing';
|
|
227
|
+
const rect = container.getBoundingClientRect();
|
|
228
|
+
dragOffsetX = e.clientX - rect.left;
|
|
229
|
+
dragOffsetY = e.clientY - rect.top;
|
|
230
|
+
iframe.style.pointerEvents = 'none';
|
|
231
|
+
e.preventDefault();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
let resizing = false;
|
|
235
|
+
let resizeStartX = 0,
|
|
236
|
+
resizeStartY = 0;
|
|
237
|
+
let resizeStartW = 0,
|
|
238
|
+
resizeStartH = 0;
|
|
239
|
+
const MIN_W = 280,
|
|
240
|
+
MIN_H = 320;
|
|
241
|
+
|
|
242
|
+
resizeHandle.addEventListener('mousedown', (e: MouseEvent) => {
|
|
243
|
+
resizing = true;
|
|
244
|
+
resizeStartX = e.clientX;
|
|
245
|
+
resizeStartY = e.clientY;
|
|
246
|
+
resizeStartW = container.offsetWidth;
|
|
247
|
+
resizeStartH = container.offsetHeight;
|
|
248
|
+
iframe.style.pointerEvents = 'none';
|
|
249
|
+
e.preventDefault();
|
|
250
|
+
e.stopPropagation();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const onMouseMove = (e: MouseEvent) => {
|
|
254
|
+
if (dragging) {
|
|
255
|
+
const newLeft = Math.max(0, Math.min(window.innerWidth - container.offsetWidth, e.clientX - dragOffsetX));
|
|
256
|
+
const newTop = Math.max(0, Math.min(window.innerHeight - container.offsetHeight, e.clientY - dragOffsetY));
|
|
257
|
+
container.style.left = `${newLeft}px`;
|
|
258
|
+
container.style.top = `${newTop}px`;
|
|
259
|
+
} else if (resizing) {
|
|
260
|
+
const newW = Math.max(MIN_W, resizeStartW + (e.clientX - resizeStartX));
|
|
261
|
+
const newH = Math.max(MIN_H, resizeStartH + (e.clientY - resizeStartY));
|
|
262
|
+
container.style.width = `${newW}px`;
|
|
263
|
+
container.style.height = `${newH}px`;
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const onMouseUp = () => {
|
|
268
|
+
if (dragging) {
|
|
269
|
+
dragHandle.style.cursor = 'grab';
|
|
270
|
+
}
|
|
271
|
+
dragging = false;
|
|
272
|
+
resizing = false;
|
|
273
|
+
iframe.style.pointerEvents = '';
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
277
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
278
|
+
this._dragCleanup = () => {
|
|
279
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
280
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private cleanup(): void {
|
|
285
|
+
this._dragCleanup?.();
|
|
286
|
+
this._dragCleanup = null;
|
|
287
|
+
|
|
288
|
+
if (this._appOwnsContainer) {
|
|
289
|
+
if (this.iframe && this.iframe.parentNode) {
|
|
290
|
+
this.iframe.parentNode.removeChild(this.iframe);
|
|
291
|
+
}
|
|
292
|
+
} else if (this._container && this._container.parentNode) {
|
|
293
|
+
this._container.parentNode.removeChild(this._container);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
this._container = null;
|
|
297
|
+
this.iframe = null;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** @internal */
|
|
302
|
+
function waitForMessage(
|
|
303
|
+
predicate: (msg: Record<string, unknown>) => boolean,
|
|
304
|
+
timeoutMs: number,
|
|
305
|
+
expectedOrigin: string,
|
|
306
|
+
): Promise<Record<string, unknown>> {
|
|
307
|
+
return new Promise((resolve, reject) => {
|
|
308
|
+
const timer = setTimeout(() => {
|
|
309
|
+
window.removeEventListener('message', handler);
|
|
310
|
+
reject(new Error(`Iframe wallet: timed out waiting for message (${timeoutMs}ms)`));
|
|
311
|
+
}, timeoutMs);
|
|
312
|
+
|
|
313
|
+
/** Handles incoming postMessage events, filtering by origin and predicate. */
|
|
314
|
+
function handler(event: MessageEvent) {
|
|
315
|
+
if (event.origin !== expectedOrigin) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const msg = event.data;
|
|
319
|
+
if (!msg || typeof msg !== 'object') {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (predicate(msg as Record<string, unknown>)) {
|
|
323
|
+
clearTimeout(timer);
|
|
324
|
+
window.removeEventListener('message', handler);
|
|
325
|
+
resolve(msg as Record<string, unknown>);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
window.addEventListener('message', handler);
|
|
330
|
+
});
|
|
331
|
+
}
|