accounts 0.4.0 → 0.4.2
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 +13 -0
- package/README.md +38 -7
- package/dist/cli/Provider.d.ts +12 -0
- package/dist/cli/Provider.d.ts.map +1 -0
- package/dist/cli/Provider.js +19 -0
- package/dist/cli/Provider.js.map +1 -0
- package/dist/cli/adapter.d.ts +24 -0
- package/dist/cli/adapter.d.ts.map +1 -0
- package/dist/cli/adapter.js +173 -0
- package/dist/cli/adapter.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/Dialog.d.ts.map +1 -1
- package/dist/core/Dialog.js +25 -1
- package/dist/core/Dialog.js.map +1 -1
- package/dist/core/IntersectionObserver.d.ts +3 -0
- package/dist/core/IntersectionObserver.d.ts.map +1 -0
- package/dist/core/IntersectionObserver.js +6 -0
- package/dist/core/IntersectionObserver.js.map +1 -0
- package/dist/core/Messenger.d.ts +14 -3
- package/dist/core/Messenger.d.ts.map +1 -1
- package/dist/core/Messenger.js +4 -4
- package/dist/core/Messenger.js.map +1 -1
- package/dist/core/Remote.d.ts +6 -3
- package/dist/core/Remote.d.ts.map +1 -1
- package/dist/core/Remote.js +3 -6
- package/dist/core/Remote.js.map +1 -1
- package/dist/core/adapters/local.d.ts.map +1 -1
- package/dist/core/adapters/local.js +2 -2
- package/dist/core/adapters/local.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/react/Remote.d.ts +21 -0
- package/dist/react/Remote.d.ts.map +1 -0
- package/dist/react/Remote.js +51 -0
- package/dist/react/Remote.js.map +1 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +2 -0
- package/dist/react/index.js.map +1 -0
- package/dist/server/CliAuth.d.ts +553 -0
- package/dist/server/CliAuth.d.ts.map +1 -0
- package/dist/server/CliAuth.js +446 -0
- package/dist/server/CliAuth.js.map +1 -0
- package/dist/server/Handler.d.ts +36 -2
- package/dist/server/Handler.d.ts.map +1 -1
- package/dist/server/Handler.js +84 -0
- package/dist/server/Handler.js.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/package.json +16 -54
- package/src/cli/Provider.test-d.ts +28 -0
- package/src/cli/Provider.test.ts +235 -0
- package/src/cli/Provider.ts +26 -0
- package/src/cli/adapter.ts +229 -0
- package/src/cli/index.ts +2 -0
- package/src/core/Dialog.ts +31 -1
- package/src/core/IntersectionObserver.ts +6 -0
- package/src/core/Messenger.ts +18 -8
- package/src/core/Provider.test.ts +12 -2
- package/src/core/Remote.ts +9 -10
- package/src/core/adapters/local.ts +7 -2
- package/src/index.ts +1 -0
- package/src/react/Remote.ts +94 -0
- package/src/react/index.ts +1 -0
- package/src/server/CliAuth.test-d.ts +56 -0
- package/src/server/CliAuth.test.ts +800 -0
- package/src/server/CliAuth.ts +634 -0
- package/src/server/Handler.ts +123 -1
- package/src/server/index.ts +1 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import { setTimeout as sleep } from 'node:timers/promises'
|
|
3
|
+
import { Base64, Hash, Hex, Provider as core_Provider, RpcResponse } from 'ox'
|
|
4
|
+
import * as z from 'zod/mini'
|
|
5
|
+
|
|
6
|
+
import * as Adapter from '../core/Adapter.js'
|
|
7
|
+
import * as CliAuth from '../server/CliAuth.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Creates a CLI bootstrap adapter backed by the device-code protocol.
|
|
11
|
+
*
|
|
12
|
+
* Only `wallet_connect` is supported in v1.
|
|
13
|
+
*/
|
|
14
|
+
export function cli(options: cli.Options): Adapter.Adapter {
|
|
15
|
+
const { name = 'Tempo CLI', rdns = 'xyz.tempo.cli' } = options
|
|
16
|
+
|
|
17
|
+
return Adapter.define({ name, rdns }, () => ({
|
|
18
|
+
actions: {
|
|
19
|
+
async createAccount(params, request) {
|
|
20
|
+
return this.loadAccounts(params, request)
|
|
21
|
+
},
|
|
22
|
+
async loadAccounts(parameters) {
|
|
23
|
+
const {
|
|
24
|
+
host,
|
|
25
|
+
open = defaultOpen,
|
|
26
|
+
pollIntervalMs = 2_000,
|
|
27
|
+
timeoutMs = 5 * 60 * 1_000,
|
|
28
|
+
} = options
|
|
29
|
+
const authorizeAccessKey = parameters?.authorizeAccessKey
|
|
30
|
+
|
|
31
|
+
if (!authorizeAccessKey?.publicKey)
|
|
32
|
+
throw new RpcResponse.InvalidParamsError({
|
|
33
|
+
message:
|
|
34
|
+
'`wallet_connect` on the CLI adapter requires `capabilities.authorizeAccessKey.publicKey`.',
|
|
35
|
+
})
|
|
36
|
+
if (parameters?.digest)
|
|
37
|
+
throw unsupported('`wallet_connect` digest signing not supported by CLI adapter.')
|
|
38
|
+
|
|
39
|
+
const codeVerifier = createCodeVerifier()
|
|
40
|
+
const codeChallenge = createCodeChallenge(codeVerifier)
|
|
41
|
+
const created = await post({
|
|
42
|
+
body: {
|
|
43
|
+
codeChallenge,
|
|
44
|
+
...(typeof authorizeAccessKey.expiry !== 'undefined'
|
|
45
|
+
? { expiry: authorizeAccessKey.expiry }
|
|
46
|
+
: {}),
|
|
47
|
+
...(authorizeAccessKey.keyType ? { keyType: authorizeAccessKey.keyType } : {}),
|
|
48
|
+
...(authorizeAccessKey.limits ? { limits: authorizeAccessKey.limits } : {}),
|
|
49
|
+
pubKey: authorizeAccessKey.publicKey,
|
|
50
|
+
} satisfies z.output<typeof CliAuth.createRequest>,
|
|
51
|
+
request: CliAuth.createRequest,
|
|
52
|
+
response: CliAuth.createResponse,
|
|
53
|
+
url: getApiUrl(host, 'code'),
|
|
54
|
+
})
|
|
55
|
+
const url = getBrowserUrl(host, created.code)
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
await open(url)
|
|
59
|
+
} catch (error) {
|
|
60
|
+
throw new OpenError(url, created.code, error)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const startedAt = Date.now()
|
|
64
|
+
|
|
65
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
66
|
+
const result = await post({
|
|
67
|
+
body: {
|
|
68
|
+
codeVerifier,
|
|
69
|
+
} satisfies z.output<typeof CliAuth.pollRequest>,
|
|
70
|
+
request: CliAuth.pollRequest,
|
|
71
|
+
response: CliAuth.pollResponse,
|
|
72
|
+
url: getApiUrl(host, `poll/${created.code}`),
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
if (result.status === 'pending') {
|
|
76
|
+
await sleep(pollIntervalMs)
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
if (result.status === 'expired')
|
|
80
|
+
throw new Error('Device code expired before authorization completed.')
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
accounts: [
|
|
84
|
+
{
|
|
85
|
+
address: result.accountAddress,
|
|
86
|
+
capabilities: {},
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
keyAuthorization: z.encode(CliAuth.keyAuthorization, result.keyAuthorization),
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
throw new TimeoutError(url, created.code)
|
|
94
|
+
},
|
|
95
|
+
async revokeAccessKey() {
|
|
96
|
+
throw unsupported('`wallet_revokeAccessKey` not supported by CLI adapter.')
|
|
97
|
+
},
|
|
98
|
+
async sendTransaction() {
|
|
99
|
+
throw unsupported('`eth_sendTransaction` not supported by CLI adapter.')
|
|
100
|
+
},
|
|
101
|
+
async sendTransactionSync() {
|
|
102
|
+
throw unsupported('`eth_sendTransactionSync` not supported by CLI adapter.')
|
|
103
|
+
},
|
|
104
|
+
async signPersonalMessage() {
|
|
105
|
+
throw unsupported('`personal_sign` not supported by CLI adapter.')
|
|
106
|
+
},
|
|
107
|
+
async signTransaction() {
|
|
108
|
+
throw unsupported('`eth_signTransaction` not supported by CLI adapter.')
|
|
109
|
+
},
|
|
110
|
+
async signTypedData() {
|
|
111
|
+
throw unsupported('`eth_signTypedData_v4` not supported by CLI adapter.')
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
}))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export declare namespace cli {
|
|
118
|
+
export type Options = {
|
|
119
|
+
/** Host URL for the device-code flow. API calls are made under the same base path. */
|
|
120
|
+
host: string
|
|
121
|
+
/** Provider display name. @default "Tempo CLI" */
|
|
122
|
+
name?: string | undefined
|
|
123
|
+
/** Browser opener override. */
|
|
124
|
+
open?: ((url: string) => Promise<void> | void) | undefined
|
|
125
|
+
/** Poll interval in milliseconds. @default 2000 */
|
|
126
|
+
pollIntervalMs?: number | undefined
|
|
127
|
+
/** Reverse-DNS provider identifier. @default "xyz.tempo.cli" */
|
|
128
|
+
rdns?: string | undefined
|
|
129
|
+
/** Poll timeout in milliseconds. @default 300000 */
|
|
130
|
+
timeoutMs?: number | undefined
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
class OpenError extends Error {
|
|
135
|
+
code: string
|
|
136
|
+
cause?: unknown | undefined
|
|
137
|
+
url: string
|
|
138
|
+
|
|
139
|
+
constructor(url: string, code: string, cause?: unknown) {
|
|
140
|
+
super(`Failed to open browser for device code ${formatCode(code)}. Open ${url} manually.`)
|
|
141
|
+
this.name = 'OpenError'
|
|
142
|
+
this.code = code
|
|
143
|
+
this.cause = cause
|
|
144
|
+
this.url = url
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
class TimeoutError extends Error {
|
|
149
|
+
code: string
|
|
150
|
+
url: string
|
|
151
|
+
|
|
152
|
+
constructor(url: string, code: string) {
|
|
153
|
+
super(`Timed out waiting for device code ${formatCode(code)}. Continue at ${url}.`)
|
|
154
|
+
this.name = 'TimeoutError'
|
|
155
|
+
this.code = code
|
|
156
|
+
this.url = url
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function createCodeChallenge(codeVerifier: string) {
|
|
161
|
+
return Base64.fromBytes(Hash.sha256(Hex.fromString(codeVerifier), { as: 'Bytes' }), {
|
|
162
|
+
pad: false,
|
|
163
|
+
url: true,
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function createCodeVerifier() {
|
|
168
|
+
return Base64.fromBytes(Hex.toBytes(Hex.random(32)), { pad: false, url: true })
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function formatCode(code: string) {
|
|
172
|
+
return code.length === 8 ? `${code.slice(0, 4)}-${code.slice(4)}` : code
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function defaultOpen(url: string) {
|
|
176
|
+
const command =
|
|
177
|
+
process.platform === 'darwin'
|
|
178
|
+
? { command: 'open', args: [url] }
|
|
179
|
+
: process.platform === 'win32'
|
|
180
|
+
? { command: 'cmd', args: ['/c', 'start', '', url] }
|
|
181
|
+
: { command: 'xdg-open', args: [url] }
|
|
182
|
+
|
|
183
|
+
const child = spawn(command.command, command.args, {
|
|
184
|
+
detached: true,
|
|
185
|
+
stdio: 'ignore',
|
|
186
|
+
})
|
|
187
|
+
child.unref()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function getApiUrl(serviceUrl: string, path: string) {
|
|
191
|
+
const url = new URL(serviceUrl)
|
|
192
|
+
url.pathname = `${url.pathname.replace(/\/$/, '')}/${path.replace(/^\//, '')}`
|
|
193
|
+
url.search = ''
|
|
194
|
+
return url.toString()
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function getBrowserUrl(serviceUrl: string, code: string) {
|
|
198
|
+
const url = new URL(serviceUrl)
|
|
199
|
+
url.searchParams.set('code', code)
|
|
200
|
+
return url.toString()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function post<
|
|
204
|
+
const request extends z.ZodMiniType,
|
|
205
|
+
const response extends z.ZodMiniType,
|
|
206
|
+
>(options: {
|
|
207
|
+
body: z.output<request>
|
|
208
|
+
request: request
|
|
209
|
+
response: response
|
|
210
|
+
url: string
|
|
211
|
+
}): Promise<z.output<response>> {
|
|
212
|
+
const result = await fetch(options.url, {
|
|
213
|
+
body: JSON.stringify(z.encode(options.request, options.body)),
|
|
214
|
+
headers: { 'content-type': 'application/json' },
|
|
215
|
+
method: 'POST',
|
|
216
|
+
})
|
|
217
|
+
const json = (await result.json().catch(() => ({}))) as z.input<response>
|
|
218
|
+
|
|
219
|
+
if (!result.ok) {
|
|
220
|
+
const error = (json as { error?: unknown }).error
|
|
221
|
+
throw new Error(typeof error === 'string' ? error : `Request failed: ${result.status}`)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return z.decode(options.response, json)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function unsupported(message: string) {
|
|
228
|
+
return new core_Provider.UnsupportedMethodError({ message })
|
|
229
|
+
}
|
package/src/cli/index.ts
ADDED
package/src/core/Dialog.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as IO from './IntersectionObserver.js'
|
|
1
2
|
import * as Messenger from './Messenger.js'
|
|
2
3
|
import type * as Store from './Store.js'
|
|
3
4
|
|
|
@@ -207,6 +208,19 @@ export function iframe(): Dialog {
|
|
|
207
208
|
}
|
|
208
209
|
}
|
|
209
210
|
|
|
211
|
+
messenger.on('switch-mode', () => {
|
|
212
|
+
hideDialog()
|
|
213
|
+
activatePage()
|
|
214
|
+
open = false
|
|
215
|
+
|
|
216
|
+
const pending = store
|
|
217
|
+
.getState()
|
|
218
|
+
.requestQueue.filter(
|
|
219
|
+
(x): x is Store.QueuedRequest & { status: 'pending' } => x.status === 'pending',
|
|
220
|
+
)
|
|
221
|
+
if (pending.length > 0) fallback.syncRequests(pending)
|
|
222
|
+
})
|
|
223
|
+
|
|
210
224
|
return {
|
|
211
225
|
close() {
|
|
212
226
|
fallback.close()
|
|
@@ -241,7 +255,23 @@ export function iframe(): Dialog {
|
|
|
241
255
|
isSafari() &&
|
|
242
256
|
requests.some((x) => ['wallet_connect', 'eth_requestAccounts'].includes(x.request.method))
|
|
243
257
|
|
|
244
|
-
|
|
258
|
+
const secure = await (async () => {
|
|
259
|
+
const { trustedHosts } = await messenger.waitForReady()
|
|
260
|
+
const ioSupported = IO.supported()
|
|
261
|
+
const trusted = Boolean(trustedHosts?.includes(window.location.hostname))
|
|
262
|
+
return ioSupported || trusted
|
|
263
|
+
})()
|
|
264
|
+
|
|
265
|
+
if (unsupported || !secure) {
|
|
266
|
+
if (!secure)
|
|
267
|
+
console.warn(
|
|
268
|
+
[
|
|
269
|
+
`[accounts] Browser does not support IntersectionObserver v2 and "${window.location.hostname}" is not a trusted host.`,
|
|
270
|
+
'Falling back to popup dialog.',
|
|
271
|
+
'',
|
|
272
|
+
'To enable the iframe dialog, add your hostname to the trusted hosts list.',
|
|
273
|
+
].join('\n'),
|
|
274
|
+
)
|
|
245
275
|
fallback.syncRequests(requests)
|
|
246
276
|
} else {
|
|
247
277
|
const requiresConfirm = requests.some((x) => x.status === 'pending')
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Whether IntersectionObserver v2 (with `isVisible`) is supported. */
|
|
2
|
+
export const supported = () =>
|
|
3
|
+
'IntersectionObserver' in window &&
|
|
4
|
+
'IntersectionObserverEntry' in window &&
|
|
5
|
+
'intersectionRatio' in IntersectionObserverEntry.prototype &&
|
|
6
|
+
'isVisible' in IntersectionObserverEntry.prototype
|
package/src/core/Messenger.ts
CHANGED
|
@@ -20,19 +20,25 @@ export type Messenger = {
|
|
|
20
20
|
) => Promise<{ id: string; topic: topic; payload: Payload<topic> }>
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** Options sent with the `ready` signal from the remote frame. */
|
|
24
|
+
export type ReadyOptions = {
|
|
25
|
+
/** Hostnames trusted by the remote embed to render in an iframe. */
|
|
26
|
+
trustedHosts?: string[] | undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
/** Bridge messenger that waits for a `ready` signal from the remote frame. */
|
|
24
30
|
export type Bridge = Messenger & {
|
|
25
31
|
/** Signal readiness (called by the remote frame). */
|
|
26
|
-
ready: () => void
|
|
32
|
+
ready: (options?: ReadyOptions | undefined) => void
|
|
27
33
|
/** Promise that resolves when the remote frame signals ready. */
|
|
28
|
-
waitForReady: () => Promise<
|
|
34
|
+
waitForReady: () => Promise<ReadyOptions>
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
/** Message schema for cross-frame communication. */
|
|
32
38
|
export type Schema = [
|
|
33
39
|
{
|
|
34
40
|
topic: 'ready'
|
|
35
|
-
payload:
|
|
41
|
+
payload: ReadyOptions
|
|
36
42
|
},
|
|
37
43
|
{
|
|
38
44
|
topic: 'rpc-requests'
|
|
@@ -52,6 +58,10 @@ export type Schema = [
|
|
|
52
58
|
topic: 'close'
|
|
53
59
|
payload: undefined
|
|
54
60
|
},
|
|
61
|
+
{
|
|
62
|
+
topic: 'switch-mode'
|
|
63
|
+
payload: { mode: 'popup' }
|
|
64
|
+
},
|
|
55
65
|
]
|
|
56
66
|
|
|
57
67
|
/** Union of all topic strings. */
|
|
@@ -116,8 +126,8 @@ export function bridge(parameters: bridge.Parameters): Bridge {
|
|
|
116
126
|
|
|
117
127
|
let pending = false
|
|
118
128
|
|
|
119
|
-
const ready = withResolvers<
|
|
120
|
-
from_.on('ready', ready.resolve)
|
|
129
|
+
const ready = withResolvers<ReadyOptions>()
|
|
130
|
+
from_.on('ready', (payload) => ready.resolve(payload ?? {}))
|
|
121
131
|
|
|
122
132
|
const messenger = from({
|
|
123
133
|
destroy() {
|
|
@@ -137,8 +147,8 @@ export function bridge(parameters: bridge.Parameters): Bridge {
|
|
|
137
147
|
|
|
138
148
|
return {
|
|
139
149
|
...messenger,
|
|
140
|
-
ready() {
|
|
141
|
-
void messenger.send('ready',
|
|
150
|
+
ready(options) {
|
|
151
|
+
void messenger.send('ready', options ?? {})
|
|
142
152
|
},
|
|
143
153
|
waitForReady() {
|
|
144
154
|
return ready.promise
|
|
@@ -169,7 +179,7 @@ export function noop(): Bridge {
|
|
|
169
179
|
},
|
|
170
180
|
ready() {},
|
|
171
181
|
waitForReady() {
|
|
172
|
-
return Promise.resolve()
|
|
182
|
+
return Promise.resolve({})
|
|
173
183
|
},
|
|
174
184
|
}
|
|
175
185
|
}
|
|
@@ -1482,10 +1482,15 @@ describe.each(adapters)('$name', ({ adapter }: (typeof adapters)[number]) => {
|
|
|
1482
1482
|
|
|
1483
1483
|
const connected = await connect(provider)
|
|
1484
1484
|
await fund(connected)
|
|
1485
|
+
const syncTransferCall = Actions.token.transfer.call({
|
|
1486
|
+
to: '0x0000000000000000000000000000000000000001',
|
|
1487
|
+
token: Addresses.pathUsd,
|
|
1488
|
+
amount: parseUnits('2', 6),
|
|
1489
|
+
})
|
|
1485
1490
|
|
|
1486
1491
|
const receipt = await provider.request({
|
|
1487
1492
|
method: 'eth_sendTransactionSync',
|
|
1488
|
-
params: [{ calls: [
|
|
1493
|
+
params: [{ calls: [syncTransferCall], feePayer: server.url }],
|
|
1489
1494
|
})
|
|
1490
1495
|
|
|
1491
1496
|
expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
|
|
@@ -1536,10 +1541,15 @@ describe.each(adapters)('$name', ({ adapter }: (typeof adapters)[number]) => {
|
|
|
1536
1541
|
|
|
1537
1542
|
const connected = await connect(provider)
|
|
1538
1543
|
await fund(connected)
|
|
1544
|
+
const syncTransferCall = Actions.token.transfer.call({
|
|
1545
|
+
to: '0x0000000000000000000000000000000000000001',
|
|
1546
|
+
token: Addresses.pathUsd,
|
|
1547
|
+
amount: parseUnits('3', 6),
|
|
1548
|
+
})
|
|
1539
1549
|
|
|
1540
1550
|
const receipt = await provider.request({
|
|
1541
1551
|
method: 'eth_sendTransactionSync',
|
|
1542
|
-
params: [{ calls: [
|
|
1552
|
+
params: [{ calls: [syncTransferCall], feePayer: true }],
|
|
1543
1553
|
})
|
|
1544
1554
|
|
|
1545
1555
|
expect(receipt.feePayer).toBe(feePayerAccount.address.toLowerCase())
|
package/src/core/Remote.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Hex } from 'ox'
|
|
2
2
|
import * as Provider from 'ox/Provider'
|
|
3
3
|
import * as RpcResponse from 'ox/RpcResponse'
|
|
4
|
-
import { useStore } from 'zustand'
|
|
5
4
|
import type { StoreApi } from 'zustand/vanilla'
|
|
6
5
|
import { createStore } from 'zustand/vanilla'
|
|
7
6
|
|
|
@@ -35,6 +34,10 @@ export type Remote = {
|
|
|
35
34
|
* Remote context store.
|
|
36
35
|
*/
|
|
37
36
|
store: StoreApi<State>
|
|
37
|
+
/**
|
|
38
|
+
* Hostnames trusted to render the embed in an iframe.
|
|
39
|
+
*/
|
|
40
|
+
trustedHosts: readonly string[]
|
|
38
41
|
/**
|
|
39
42
|
* Subscribes to user-facing RPC requests from the parent context.
|
|
40
43
|
*
|
|
@@ -109,7 +112,7 @@ export declare namespace respond {
|
|
|
109
112
|
|
|
110
113
|
/** Creates a remote context for the dialog app. */
|
|
111
114
|
export function create(options: create.Options): Remote {
|
|
112
|
-
const { messenger, provider } = options
|
|
115
|
+
const { messenger, provider, trustedHosts } = options
|
|
113
116
|
const ready =
|
|
114
117
|
typeof window !== 'undefined' && !new URLSearchParams(window.location.search).get('mode')
|
|
115
118
|
const store = createStore<State>(() => ({
|
|
@@ -123,6 +126,7 @@ export function create(options: create.Options): Remote {
|
|
|
123
126
|
messenger,
|
|
124
127
|
provider,
|
|
125
128
|
store,
|
|
129
|
+
trustedHosts: trustedHosts ?? [],
|
|
126
130
|
|
|
127
131
|
onUserRequest(cb) {
|
|
128
132
|
return this.onRequests(async (requests, event, { account }) => {
|
|
@@ -171,7 +175,7 @@ export function create(options: create.Options): Remote {
|
|
|
171
175
|
},
|
|
172
176
|
|
|
173
177
|
ready() {
|
|
174
|
-
messenger.ready()
|
|
178
|
+
messenger.ready({ trustedHosts })
|
|
175
179
|
|
|
176
180
|
if (typeof window !== 'undefined') {
|
|
177
181
|
const params = new URLSearchParams(window.location.search)
|
|
@@ -251,12 +255,7 @@ export declare namespace create {
|
|
|
251
255
|
messenger: Messenger.Bridge
|
|
252
256
|
/** Provider to execute RPC requests against. */
|
|
253
257
|
provider: CoreProvider.Provider
|
|
258
|
+
/** Hostnames trusted to render the embed in an iframe. */
|
|
259
|
+
trustedHosts?: string[] | undefined
|
|
254
260
|
}
|
|
255
261
|
}
|
|
256
|
-
|
|
257
|
-
/** React hook to select state from a remote context's store. */
|
|
258
|
-
export function useState(remote: Remote): State
|
|
259
|
-
export function useState<selected>(remote: Remote, selector: (state: State) => selected): selected
|
|
260
|
-
export function useState(remote: Remote, selector?: (state: State) => unknown) {
|
|
261
|
-
return useStore(remote.store, selector as never)
|
|
262
|
-
}
|
|
@@ -207,8 +207,13 @@ export function local(options: local.Options): Adapter.Adapter {
|
|
|
207
207
|
},
|
|
208
208
|
async signTypedData({ data, address }) {
|
|
209
209
|
const account = getAccount({ address, signable: true })
|
|
210
|
-
const
|
|
211
|
-
|
|
210
|
+
const parsed = JSON.parse(data) as {
|
|
211
|
+
domain: Record<string, unknown>
|
|
212
|
+
message: Record<string, unknown>
|
|
213
|
+
primaryType: string
|
|
214
|
+
types: Record<string, unknown>
|
|
215
|
+
}
|
|
216
|
+
return await account.signTypedData(parsed)
|
|
212
217
|
},
|
|
213
218
|
async sendTransaction(parameters) {
|
|
214
219
|
const { feePayer, ...rest } = parameters
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState as react_useState } from 'react'
|
|
2
|
+
import { useStore } from 'zustand'
|
|
3
|
+
|
|
4
|
+
import * as IO from '../core/IntersectionObserver.js'
|
|
5
|
+
import type * as CoreRemote from '../core/Remote.js'
|
|
6
|
+
|
|
7
|
+
/** Monitors element visibility using IntersectionObserver v2. */
|
|
8
|
+
export function useEnsureVisibility(
|
|
9
|
+
remote: CoreRemote.Remote,
|
|
10
|
+
options: useEnsureVisibility.Options = {},
|
|
11
|
+
): useEnsureVisibility.ReturnType {
|
|
12
|
+
const { enabled = true } = options
|
|
13
|
+
|
|
14
|
+
const origin = useState(remote, (s) => s.origin)
|
|
15
|
+
|
|
16
|
+
const trusted = useMemo(() => {
|
|
17
|
+
if (!origin) return false
|
|
18
|
+
try {
|
|
19
|
+
const hostname = new URL(origin).hostname
|
|
20
|
+
return remote.trustedHosts.includes(hostname)
|
|
21
|
+
} catch {
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
}, [origin, remote.trustedHosts])
|
|
25
|
+
|
|
26
|
+
const active = enabled && !trusted
|
|
27
|
+
|
|
28
|
+
const ref = useRef<HTMLDivElement>(null)
|
|
29
|
+
const [visible, setVisible] = react_useState(true)
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!active) return
|
|
33
|
+
if (!ref.current) return
|
|
34
|
+
|
|
35
|
+
if (!IO.supported()) {
|
|
36
|
+
setVisible(false)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const observer = new IntersectionObserver(
|
|
41
|
+
(entries) => {
|
|
42
|
+
const entry = entries[0]
|
|
43
|
+
if (!entry) return
|
|
44
|
+
const isVisible =
|
|
45
|
+
(entry as unknown as { isVisible: boolean | undefined }).isVisible || false
|
|
46
|
+
setVisible(isVisible)
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
delay: 100,
|
|
50
|
+
threshold: [0.99],
|
|
51
|
+
trackVisibility: true,
|
|
52
|
+
} as IntersectionObserverInit,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
observer.observe(ref.current)
|
|
56
|
+
return () => observer.disconnect()
|
|
57
|
+
}, [active])
|
|
58
|
+
|
|
59
|
+
const invokePopup = useCallback(
|
|
60
|
+
() => remote.messenger.send('switch-mode', { mode: 'popup' }),
|
|
61
|
+
[remote],
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return { invokePopup, ref, visible }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** React hook to select state from a remote context's store. */
|
|
68
|
+
export function useState(remote: CoreRemote.Remote): CoreRemote.State
|
|
69
|
+
export function useState<selected>(
|
|
70
|
+
remote: CoreRemote.Remote,
|
|
71
|
+
selector: (state: CoreRemote.State) => selected,
|
|
72
|
+
): selected
|
|
73
|
+
export function useState(
|
|
74
|
+
remote: CoreRemote.Remote,
|
|
75
|
+
selector?: (state: CoreRemote.State) => unknown,
|
|
76
|
+
) {
|
|
77
|
+
return useStore(remote.store, selector as never)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export declare namespace useEnsureVisibility {
|
|
81
|
+
type Options = {
|
|
82
|
+
/** Whether visibility monitoring is enabled. @default true */
|
|
83
|
+
enabled?: boolean | undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
type ReturnType = {
|
|
87
|
+
/** Requests the host switch to a popup dialog. */
|
|
88
|
+
invokePopup: () => void
|
|
89
|
+
/** Ref to attach to the element being monitored. */
|
|
90
|
+
ref: React.RefObject<HTMLDivElement | null>
|
|
91
|
+
/** Whether the element is currently visible. */
|
|
92
|
+
visible: boolean
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * as Remote from './Remote.js'
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Hex } from 'viem'
|
|
2
|
+
import { describe, expectTypeOf, test } from 'vp/test'
|
|
3
|
+
import * as z from 'zod/mini'
|
|
4
|
+
|
|
5
|
+
import * as CliAuth from './CliAuth.js'
|
|
6
|
+
|
|
7
|
+
describe('createRequest', () => {
|
|
8
|
+
test('includes the v1 device-code request fields', () => {
|
|
9
|
+
expectTypeOf<z.output<typeof CliAuth.createRequest>>().toMatchTypeOf<{
|
|
10
|
+
account?: Hex | undefined
|
|
11
|
+
codeChallenge: string
|
|
12
|
+
expiry?: number | undefined
|
|
13
|
+
keyType?: 'secp256k1' | 'p256' | 'webAuthn' | undefined
|
|
14
|
+
limits?: readonly { token: Hex; limit: bigint }[] | undefined
|
|
15
|
+
pubKey: Hex
|
|
16
|
+
}>()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('does not include scopes in v1', () => {
|
|
20
|
+
type Request = z.output<typeof CliAuth.createRequest>
|
|
21
|
+
expectTypeOf<Request>().not.toHaveProperty('scopes')
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('pollResponse', () => {
|
|
26
|
+
test('authorized responses carry the normal keyAuthorization shape', () => {
|
|
27
|
+
type Response = Extract<z.output<typeof CliAuth.pollResponse>, { status: 'authorized' }>
|
|
28
|
+
expectTypeOf<Response>().toMatchTypeOf<{
|
|
29
|
+
accountAddress: Hex
|
|
30
|
+
keyAuthorization: z.output<typeof CliAuth.keyAuthorization>
|
|
31
|
+
status: 'authorized'
|
|
32
|
+
}>()
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('pendingResponse', () => {
|
|
37
|
+
test('pending responses expose the browser approval payload', () => {
|
|
38
|
+
expectTypeOf<z.output<typeof CliAuth.pendingResponse>>().toMatchTypeOf<{
|
|
39
|
+
accessKeyAddress: Hex
|
|
40
|
+
account?: Hex | undefined
|
|
41
|
+
chainId: bigint
|
|
42
|
+
code: string
|
|
43
|
+
expiry: number
|
|
44
|
+
keyType: 'secp256k1' | 'p256' | 'webAuthn'
|
|
45
|
+
limits?: readonly { token: Hex; limit: bigint }[] | undefined
|
|
46
|
+
pubKey: Hex
|
|
47
|
+
status: 'pending'
|
|
48
|
+
}>()
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('Store', () => {
|
|
53
|
+
test('memory helper satisfies the shared store contract', () => {
|
|
54
|
+
expectTypeOf(CliAuth.Store.memory).returns.toMatchTypeOf<CliAuth.Store>()
|
|
55
|
+
})
|
|
56
|
+
})
|