accounts 0.16.2 → 0.17.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 +33 -0
- package/README.md +3 -4
- package/dist/cli/Provider.d.ts +1 -1
- package/dist/cli/Provider.d.ts.map +1 -1
- package/dist/cli/Provider.js +3 -3
- package/dist/cli/Provider.js.map +1 -1
- package/dist/cli/adapter.d.ts +13 -8
- package/dist/cli/adapter.d.ts.map +1 -1
- package/dist/cli/adapter.js +92 -303
- package/dist/cli/adapter.js.map +1 -1
- package/dist/core/adapters/deviceCode/deviceCode.d.ts +59 -0
- package/dist/core/adapters/deviceCode/deviceCode.d.ts.map +1 -0
- package/dist/core/adapters/deviceCode/deviceCode.js +93 -0
- package/dist/core/adapters/deviceCode/deviceCode.js.map +1 -0
- package/dist/core/adapters/deviceCode/index.d.ts +3 -0
- package/dist/core/adapters/deviceCode/index.d.ts.map +1 -0
- package/dist/core/adapters/deviceCode/index.js +3 -0
- package/dist/core/adapters/deviceCode/index.js.map +1 -0
- package/dist/core/adapters/deviceCode/tempoWallet.d.ts +9 -0
- package/dist/core/adapters/deviceCode/tempoWallet.d.ts.map +1 -0
- package/dist/core/adapters/deviceCode/tempoWallet.js +11 -0
- package/dist/core/adapters/deviceCode/tempoWallet.js.map +1 -0
- package/dist/core/adapters/internal/fromRequest.d.ts +10 -0
- package/dist/core/adapters/internal/fromRequest.d.ts.map +1 -1
- package/dist/core/adapters/internal/fromRequest.js +4 -2
- package/dist/core/adapters/internal/fromRequest.js.map +1 -1
- package/dist/server/CliAuth.d.ts +69 -1
- package/dist/server/CliAuth.d.ts.map +1 -1
- package/dist/server/CliAuth.js +6 -1
- package/dist/server/CliAuth.js.map +1 -1
- package/dist/server/Handler.d.ts +2 -1
- package/dist/server/Handler.d.ts.map +1 -1
- package/dist/server/Handler.js +2 -1
- 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/dist/server/internal/handlers/codeAuth.d.ts +1 -0
- package/dist/server/internal/handlers/codeAuth.d.ts.map +1 -1
- package/dist/server/internal/handlers/codeAuth.js +1 -0
- package/dist/server/internal/handlers/codeAuth.js.map +1 -1
- package/dist/server/internal/handlers/deviceCode.d.ts +60 -0
- package/dist/server/internal/handlers/deviceCode.d.ts.map +1 -0
- package/dist/server/internal/handlers/deviceCode.js +218 -0
- package/dist/server/internal/handlers/deviceCode.js.map +1 -0
- package/package.json +7 -1
- package/src/cli/Provider.localnet.test.ts +414 -470
- package/src/cli/Provider.test-d.ts +4 -3
- package/src/cli/Provider.ts +6 -6
- package/src/cli/adapter.ts +111 -380
- package/src/core/adapters/deviceCode/deviceCode.localnet.test.ts +226 -0
- package/src/core/adapters/deviceCode/deviceCode.test-d.ts +19 -0
- package/src/core/adapters/deviceCode/deviceCode.ts +157 -0
- package/src/core/adapters/deviceCode/index.ts +2 -0
- package/src/core/adapters/deviceCode/tempoWallet.ts +17 -0
- package/src/core/adapters/internal/fromRequest.ts +115 -88
- package/src/server/CliAuth.test.ts +29 -18
- package/src/server/CliAuth.ts +10 -2
- package/src/server/Handler.test-d.ts +19 -33
- package/src/server/Handler.ts +2 -1
- package/src/server/index.ts +1 -0
- package/src/server/internal/handlers/codeAuth.ts +1 -0
- package/src/server/internal/handlers/deviceCode.test.ts +140 -0
- package/src/server/internal/handlers/deviceCode.ts +323 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { Hex } from 'ox'
|
|
2
|
+
import { describe, expect, test } from 'vp/test'
|
|
3
|
+
import type { DeviceCode } from 'wata'
|
|
4
|
+
|
|
5
|
+
import { accounts, chain } from '../../../../test/config.js'
|
|
6
|
+
import { createDeviceCodeHost, submitVerify } from '../../../../test/deviceCode.js'
|
|
7
|
+
import { createServer } from '../../../../test/utils.js'
|
|
8
|
+
import * as Provider from '../../Provider.js'
|
|
9
|
+
import { deviceCode } from './deviceCode.js'
|
|
10
|
+
|
|
11
|
+
const root = accounts[0]!
|
|
12
|
+
const accessKey = accounts[1]!
|
|
13
|
+
const expiry = Math.floor(Date.now() / 1000) + 3_600
|
|
14
|
+
|
|
15
|
+
function connectRequest() {
|
|
16
|
+
return {
|
|
17
|
+
method: 'wallet_connect',
|
|
18
|
+
params: [
|
|
19
|
+
{
|
|
20
|
+
capabilities: {
|
|
21
|
+
authorizeAccessKey: {
|
|
22
|
+
expiry,
|
|
23
|
+
keyType: accessKey.keyType,
|
|
24
|
+
publicKey: accessKey.publicKey,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
} as const
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function createProvider(options: Partial<deviceCode.Options> & { url: string }) {
|
|
33
|
+
return Provider.create({
|
|
34
|
+
adapter: deviceCode({
|
|
35
|
+
name: 'Accounts Test CLI',
|
|
36
|
+
onPrompt: (prompt) => void submitVerify(prompt),
|
|
37
|
+
rdns: 'xyz.tempo.accounts.test',
|
|
38
|
+
...options,
|
|
39
|
+
}),
|
|
40
|
+
chains: [chain],
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('deviceCode', () => {
|
|
45
|
+
test('default: bootstraps wallet_connect through the device-code flow', async () => {
|
|
46
|
+
const host = createDeviceCodeHost()
|
|
47
|
+
const server = await createServer(host.listener)
|
|
48
|
+
const prompts: DeviceCode.Prompt[] = []
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const provider = createProvider({
|
|
52
|
+
onPrompt: (prompt) => {
|
|
53
|
+
prompts.push(prompt)
|
|
54
|
+
void submitVerify(prompt)
|
|
55
|
+
},
|
|
56
|
+
url: `${server.url}/auth/device`,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const result = await provider.request(connectRequest())
|
|
60
|
+
const account = result.accounts[0]!
|
|
61
|
+
const keyAuthorization = account.capabilities.keyAuthorization
|
|
62
|
+
? {
|
|
63
|
+
...account.capabilities.keyAuthorization,
|
|
64
|
+
signature: { type: account.capabilities.keyAuthorization.signature.type },
|
|
65
|
+
}
|
|
66
|
+
: undefined
|
|
67
|
+
|
|
68
|
+
expect(prompts).toHaveLength(1)
|
|
69
|
+
expect(prompts[0]!.userCode).toMatch(/^[BCDFGHJKLMNPQRSTVWXZ]{4}-[BCDFGHJKLMNPQRSTVWXZ]{4}$/)
|
|
70
|
+
expect(prompts[0]!.verificationUri).toBe(`${server.url}/auth/device/verify`)
|
|
71
|
+
expect(prompts[0]!.verificationUriFull).toBe(
|
|
72
|
+
`${server.url}/auth/device/verify?user_code=${prompts[0]!.userCode}`,
|
|
73
|
+
)
|
|
74
|
+
expect({
|
|
75
|
+
account: {
|
|
76
|
+
address: account.address,
|
|
77
|
+
capabilities: keyAuthorization ? { keyAuthorization } : {},
|
|
78
|
+
},
|
|
79
|
+
}).toMatchInlineSnapshot(`
|
|
80
|
+
{
|
|
81
|
+
"account": {
|
|
82
|
+
"address": "${root.address}",
|
|
83
|
+
"capabilities": {
|
|
84
|
+
"keyAuthorization": {
|
|
85
|
+
"address": "${accessKey.address.toLowerCase()}",
|
|
86
|
+
"chainId": "${Hex.fromNumber(chain.id)}",
|
|
87
|
+
"expiry": "${Hex.fromNumber(expiry)}",
|
|
88
|
+
"keyId": "${accessKey.address.toLowerCase()}",
|
|
89
|
+
"keyType": "secp256k1",
|
|
90
|
+
"signature": {
|
|
91
|
+
"type": "secp256k1",
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
`)
|
|
98
|
+
} finally {
|
|
99
|
+
await server.closeAsync()
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test('behavior: forwards request context to the host', async () => {
|
|
104
|
+
const host = createDeviceCodeHost()
|
|
105
|
+
const server = await createServer(host.listener)
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const provider = createProvider({ url: `${server.url}/auth/device` })
|
|
109
|
+
await provider.request(connectRequest())
|
|
110
|
+
|
|
111
|
+
expect(host.requests()).toHaveLength(1)
|
|
112
|
+
expect(host.requests()[0]!.method).toBe('wallet_connect')
|
|
113
|
+
} finally {
|
|
114
|
+
await server.closeAsync()
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('behavior: preserves a null username capability', async () => {
|
|
119
|
+
const host = createDeviceCodeHost({ username: null })
|
|
120
|
+
const server = await createServer(host.listener)
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const provider = createProvider({ url: `${server.url}/auth/device` })
|
|
124
|
+
const result = await provider.request(connectRequest())
|
|
125
|
+
|
|
126
|
+
expect(result.accounts[0]!.capabilities.username).toBeNull()
|
|
127
|
+
} finally {
|
|
128
|
+
await server.closeAsync()
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test('behavior: denial rejects with a user-rejected error', async () => {
|
|
133
|
+
const host = createDeviceCodeHost()
|
|
134
|
+
const server = await createServer(host.listener)
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const provider = createProvider({
|
|
138
|
+
onPrompt: (prompt) => void submitVerify(prompt, { action: 'deny' }),
|
|
139
|
+
url: `${server.url}/auth/device`,
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
await expect(
|
|
143
|
+
provider.request(connectRequest()).catch((error: { code: number; message: string }) => ({
|
|
144
|
+
code: error.code,
|
|
145
|
+
message: error.message,
|
|
146
|
+
})),
|
|
147
|
+
).resolves.toMatchInlineSnapshot(`
|
|
148
|
+
{
|
|
149
|
+
"code": 4001,
|
|
150
|
+
"message": "User denied the device-code request.",
|
|
151
|
+
}
|
|
152
|
+
`)
|
|
153
|
+
} finally {
|
|
154
|
+
await server.closeAsync()
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
test('behavior: times out while waiting for authorization', async () => {
|
|
159
|
+
const host = createDeviceCodeHost({ pollingInterval: 10 })
|
|
160
|
+
const server = await createServer(host.listener)
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const provider = createProvider({
|
|
164
|
+
onPrompt: () => {},
|
|
165
|
+
timeout: 100,
|
|
166
|
+
url: `${server.url}/auth/device`,
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const error = await provider.request(connectRequest()).then(
|
|
170
|
+
() => undefined,
|
|
171
|
+
(error: Error) => error,
|
|
172
|
+
)
|
|
173
|
+
expect(error?.message).toMatch(/^Timed out waiting for device code [A-Z-]+\. Continue at /)
|
|
174
|
+
} finally {
|
|
175
|
+
await server.closeAsync()
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('behavior: onPrompt failures surface the URL and code', async () => {
|
|
180
|
+
const host = createDeviceCodeHost()
|
|
181
|
+
const server = await createServer(host.listener)
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const provider = createProvider({
|
|
185
|
+
onPrompt: () => {
|
|
186
|
+
throw new Error('browser unavailable')
|
|
187
|
+
},
|
|
188
|
+
url: `${server.url}/auth/device`,
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
const error = await provider.request(connectRequest()).then(
|
|
192
|
+
() => undefined,
|
|
193
|
+
(error: Error) => error,
|
|
194
|
+
)
|
|
195
|
+
expect(error?.message).toMatch(
|
|
196
|
+
/^Failed to surface device code [A-Z-]+\. Open .*\/auth\/device\/verify\?user_code=[A-Z-]+ manually\.$/,
|
|
197
|
+
)
|
|
198
|
+
} finally {
|
|
199
|
+
await server.closeAsync()
|
|
200
|
+
}
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
test('behavior: rejects methods outside the allowlist without a ceremony', async () => {
|
|
204
|
+
const host = createDeviceCodeHost()
|
|
205
|
+
const server = await createServer(host.listener)
|
|
206
|
+
const prompts: DeviceCode.Prompt[] = []
|
|
207
|
+
|
|
208
|
+
try {
|
|
209
|
+
const provider = createProvider({
|
|
210
|
+
methods: ['wallet_authorizeAccessKey'],
|
|
211
|
+
onPrompt: (prompt) => {
|
|
212
|
+
prompts.push(prompt)
|
|
213
|
+
void submitVerify(prompt)
|
|
214
|
+
},
|
|
215
|
+
url: `${server.url}/auth/device`,
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
await expect(provider.request(connectRequest())).rejects.toThrowErrorMatchingInlineSnapshot(
|
|
219
|
+
`[Provider.UnsupportedMethodError: \`wallet_connect\` not supported by device-code adapter.]`,
|
|
220
|
+
)
|
|
221
|
+
expect(prompts).toHaveLength(0)
|
|
222
|
+
} finally {
|
|
223
|
+
await server.closeAsync()
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, test } from 'vp/test'
|
|
2
|
+
|
|
3
|
+
import { deviceCode } from './deviceCode.js'
|
|
4
|
+
|
|
5
|
+
describe('deviceCode', () => {
|
|
6
|
+
test('accepts explicitly undefined optional metadata', () => {
|
|
7
|
+
const description: string | undefined = undefined
|
|
8
|
+
const icon: string | undefined = undefined
|
|
9
|
+
const websiteUrl: string | undefined = undefined
|
|
10
|
+
|
|
11
|
+
deviceCode({
|
|
12
|
+
meta: { name: 'Example', description, icon, websiteUrl },
|
|
13
|
+
name: 'Example',
|
|
14
|
+
onPrompt() {},
|
|
15
|
+
rdns: 'com.example',
|
|
16
|
+
url: 'https://example.com/auth/device',
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
})
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { Provider as core_Provider } from 'ox'
|
|
2
|
+
import { DeviceCode, Transport, Wata, deviceCode as core_deviceCode } from 'wata'
|
|
3
|
+
|
|
4
|
+
import type * as Adapter from '../../Adapter.js'
|
|
5
|
+
import type * as Keystore from '../../Keystore.js'
|
|
6
|
+
import { fromRequest } from '../internal/fromRequest.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates a device-code adapter that forwards wallet RPC through Wata.
|
|
10
|
+
*
|
|
11
|
+
* Each request runs one OAuth 2.0 Device Authorization Grant exchange
|
|
12
|
+
* (RFC 8628 with PKCE): the host issues a user code surfaced via
|
|
13
|
+
* `onPrompt`, the user approves on the host's verification page, and the
|
|
14
|
+
* adapter polls until the response arrives.
|
|
15
|
+
*/
|
|
16
|
+
export function deviceCode(options: deviceCode.Options): Adapter.Adapter {
|
|
17
|
+
const {
|
|
18
|
+
actions,
|
|
19
|
+
fetch,
|
|
20
|
+
icon,
|
|
21
|
+
keystores,
|
|
22
|
+
meta,
|
|
23
|
+
methods,
|
|
24
|
+
name,
|
|
25
|
+
onPrompt,
|
|
26
|
+
pollingInterval,
|
|
27
|
+
rdns,
|
|
28
|
+
timeout = 300_000,
|
|
29
|
+
url,
|
|
30
|
+
} = options
|
|
31
|
+
|
|
32
|
+
return fromRequest({
|
|
33
|
+
...(actions ? { actions } : {}),
|
|
34
|
+
...(icon ? { icon } : {}),
|
|
35
|
+
...(keystores ? { keystores } : {}),
|
|
36
|
+
name,
|
|
37
|
+
rdns,
|
|
38
|
+
async request(request) {
|
|
39
|
+
if (methods && !methods.includes(request.method))
|
|
40
|
+
throw new core_Provider.UnsupportedMethodError({
|
|
41
|
+
message: `\`${request.method}\` not supported by device-code adapter.`,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// Device code is single-exchange: one register + poll cycle carries one
|
|
45
|
+
// RPC request envelope, so a fresh Wata instance backs every request.
|
|
46
|
+
const wata = Wata.create({
|
|
47
|
+
transports: [
|
|
48
|
+
core_deviceCode({
|
|
49
|
+
...(fetch !== undefined ? { fetch } : {}),
|
|
50
|
+
...(meta ? { meta } : {}),
|
|
51
|
+
...(pollingInterval !== undefined ? { pollingInterval } : {}),
|
|
52
|
+
url,
|
|
53
|
+
}),
|
|
54
|
+
],
|
|
55
|
+
})
|
|
56
|
+
const session = wata.start()
|
|
57
|
+
|
|
58
|
+
// A failed prompt surface (e.g. browser open) fails the exchange — the
|
|
59
|
+
// user can never learn the code, so polling would only run out the clock.
|
|
60
|
+
session.onPrompt((prompt) => {
|
|
61
|
+
void (async () => onPrompt(prompt))().catch((error) => {
|
|
62
|
+
void session.close(new PromptError(prompt, error))
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const timer = setTimeout(() => {
|
|
67
|
+
void session.close(new TimeoutError(session.prompt))
|
|
68
|
+
}, timeout)
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const { result } = await session.send({
|
|
72
|
+
method: request.method,
|
|
73
|
+
params: request.params ?? [],
|
|
74
|
+
...(request.context ? { context: request.context } : {}),
|
|
75
|
+
})
|
|
76
|
+
return result
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error instanceof DeviceCode.UserRejectedError)
|
|
79
|
+
throw new core_Provider.UserRejectedRequestError({
|
|
80
|
+
message: 'User denied the device-code request.',
|
|
81
|
+
})
|
|
82
|
+
if (error instanceof Transport.ClosedError)
|
|
83
|
+
throw new Error('Device code expired before authorization completed.', { cause: error })
|
|
84
|
+
throw error
|
|
85
|
+
} finally {
|
|
86
|
+
clearTimeout(timer)
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export declare namespace deviceCode {
|
|
93
|
+
/** Options for {@link deviceCode}. */
|
|
94
|
+
export type Options = {
|
|
95
|
+
/** Adapter-specific action overrides layered over the request-forwarding defaults. */
|
|
96
|
+
actions?: fromRequest.Options['actions'] | undefined
|
|
97
|
+
/** Override the transport `fetch` implementation. */
|
|
98
|
+
fetch?: typeof globalThis.fetch | undefined
|
|
99
|
+
/** Data URI of the provider icon, announced via EIP-6963. */
|
|
100
|
+
icon?: Adapter.Meta['icon'] | undefined
|
|
101
|
+
/** Keystores backing locally generated access keys (SDK default otherwise). */
|
|
102
|
+
keystores?: Keystore.Keystores | undefined
|
|
103
|
+
/** Consumer metadata shown on the host's approval page. */
|
|
104
|
+
meta?:
|
|
105
|
+
| {
|
|
106
|
+
name: string
|
|
107
|
+
description?: string | undefined
|
|
108
|
+
icon?: string | undefined
|
|
109
|
+
websiteUrl?: string | undefined
|
|
110
|
+
}
|
|
111
|
+
| undefined
|
|
112
|
+
/** Allowlist of RPC methods forwarded over the transport. Forwards all when omitted. */
|
|
113
|
+
methods?: readonly string[] | undefined
|
|
114
|
+
/** Provider display name. */
|
|
115
|
+
name: string
|
|
116
|
+
/** Surfaces the pairing prompt (user code + verification URL) to the user. */
|
|
117
|
+
onPrompt: (prompt: DeviceCode.Prompt) => Promise<void> | void
|
|
118
|
+
/** Poll cadence override in milliseconds. Defaults to the host-advertised interval. */
|
|
119
|
+
pollingInterval?: number | undefined
|
|
120
|
+
/** Reverse-DNS provider identifier. */
|
|
121
|
+
rdns: string
|
|
122
|
+
/** Overall wait for user approval in milliseconds. @default 300000 */
|
|
123
|
+
timeout?: number | undefined
|
|
124
|
+
/** Base URL of the host's device-code endpoints (derives `/register` and `/token`). */
|
|
125
|
+
url: string
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Thrown when the `onPrompt` callback fails (e.g. no browser could be opened). */
|
|
130
|
+
export class PromptError extends Error {
|
|
131
|
+
override cause?: unknown | undefined
|
|
132
|
+
prompt: DeviceCode.Prompt
|
|
133
|
+
|
|
134
|
+
constructor(prompt: DeviceCode.Prompt, cause?: unknown) {
|
|
135
|
+
super(
|
|
136
|
+
`Failed to surface device code ${prompt.userCode}. Open ${prompt.verificationUriFull ?? prompt.verificationUri} manually.`,
|
|
137
|
+
)
|
|
138
|
+
this.name = 'PromptError'
|
|
139
|
+
this.cause = cause
|
|
140
|
+
this.prompt = prompt
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Thrown when the user did not approve within {@link deviceCode.Options.timeout}. */
|
|
145
|
+
export class TimeoutError extends Error {
|
|
146
|
+
prompt: DeviceCode.Prompt | undefined
|
|
147
|
+
|
|
148
|
+
constructor(prompt: DeviceCode.Prompt | undefined) {
|
|
149
|
+
super(
|
|
150
|
+
prompt
|
|
151
|
+
? `Timed out waiting for device code ${prompt.userCode}. Continue at ${prompt.verificationUriFull ?? prompt.verificationUri}.`
|
|
152
|
+
: 'Timed out waiting for device code approval.',
|
|
153
|
+
)
|
|
154
|
+
this.name = 'TimeoutError'
|
|
155
|
+
this.prompt = prompt
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type * as Adapter from '../../Adapter.js'
|
|
2
|
+
import { deviceCode } from './deviceCode.js'
|
|
3
|
+
|
|
4
|
+
/** Creates the Tempo Wallet adapter using the Wata device-code transport. */
|
|
5
|
+
export function tempoWallet(options: tempoWallet.Options): Adapter.Adapter {
|
|
6
|
+
return deviceCode({
|
|
7
|
+
...options,
|
|
8
|
+
name: 'Tempo Wallet',
|
|
9
|
+
rdns: 'xyz.tempo',
|
|
10
|
+
url: 'https://wallet.tempo.xyz/auth/device',
|
|
11
|
+
})
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export declare namespace tempoWallet {
|
|
15
|
+
/** Options for {@link tempoWallet}. */
|
|
16
|
+
export type Options = Omit<deviceCode.Options, 'name' | 'rdns' | 'url'>
|
|
17
|
+
}
|
|
@@ -16,106 +16,133 @@ import * as Rpc from '../../zod/rpc.js'
|
|
|
16
16
|
* `request`.
|
|
17
17
|
*/
|
|
18
18
|
export function fromRequest(options: fromRequest.Options): Adapter.Adapter {
|
|
19
|
-
const {
|
|
19
|
+
const {
|
|
20
|
+
actions,
|
|
21
|
+
bind,
|
|
22
|
+
cleanup,
|
|
23
|
+
close,
|
|
24
|
+
icon,
|
|
25
|
+
keystores,
|
|
26
|
+
name,
|
|
27
|
+
rdns,
|
|
28
|
+
request: requestRemote,
|
|
29
|
+
} = options
|
|
20
30
|
|
|
21
|
-
return Adapter.define(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
31
|
+
return Adapter.define(
|
|
32
|
+
{ ...(icon ? { icon } : {}), name, rdns },
|
|
33
|
+
({ getAccount, getClient, store }) => {
|
|
34
|
+
// Late-bind the store so the request channel can reconcile local
|
|
35
|
+
// connection state (e.g. a wallet `accountsChanged` notification).
|
|
36
|
+
bind?.({ store })
|
|
25
37
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
const provider = core_Provider.from({
|
|
39
|
+
async request(request) {
|
|
40
|
+
const { accounts, activeAccount, chainId } = store.getState()
|
|
41
|
+
if (request.method === 'eth_chainId') return Hex.fromNumber(chainId)
|
|
42
|
+
// Forward the active account + chain as request context so the wallet
|
|
43
|
+
// acts against the session the app believes it is using rather than
|
|
44
|
+
// whatever the wallet last selected for itself.
|
|
45
|
+
const account = accounts[activeAccount]?.address
|
|
46
|
+
return await requestRemote({
|
|
47
|
+
method: request.method,
|
|
48
|
+
params: request.params as readonly unknown[] | undefined,
|
|
49
|
+
context: { chainId, ...(account ? { account } : {}) },
|
|
50
|
+
})
|
|
51
|
+
},
|
|
52
|
+
})
|
|
41
53
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
// Connect capabilities are read from the first account only: the remote
|
|
55
|
+
// wallet connects a single account per exchange.
|
|
56
|
+
function toConnectReturn(result: Rpc.wallet_connect.Encoded['returns']) {
|
|
57
|
+
const capabilities = result.accounts[0]?.capabilities
|
|
58
|
+
return {
|
|
59
|
+
accounts: result.accounts.map((account) => ({ address: account.address })),
|
|
60
|
+
...(capabilities?.auth ? { auth: capabilities.auth } : {}),
|
|
61
|
+
...(capabilities?.identity ? { identity: capabilities.identity } : {}),
|
|
62
|
+
...(capabilities?.keyAuthorization
|
|
63
|
+
? { keyAuthorization: capabilities.keyAuthorization }
|
|
64
|
+
: {}),
|
|
65
|
+
...(capabilities?.personalSign ? { personalSign: capabilities.personalSign } : {}),
|
|
66
|
+
...(capabilities?.signature ? { signature: capabilities.signature } : {}),
|
|
67
|
+
...(capabilities?.username !== undefined ? { username: capabilities.username } : {}),
|
|
68
|
+
}
|
|
55
69
|
}
|
|
56
|
-
}
|
|
57
70
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
71
|
+
return {
|
|
72
|
+
forwardsAuth: true,
|
|
73
|
+
...(keystores ? { accessKey: { keystores } } : {}),
|
|
74
|
+
actions: {
|
|
75
|
+
async createAccount(_parameters, request) {
|
|
76
|
+
return toConnectReturn(
|
|
77
|
+
(await provider.request(request)) as Rpc.wallet_connect.Encoded['returns'],
|
|
78
|
+
)
|
|
79
|
+
},
|
|
80
|
+
async loadAccounts(_parameters, request) {
|
|
81
|
+
return toConnectReturn(
|
|
82
|
+
(await provider.request(request)) as Rpc.wallet_connect.Encoded['returns'],
|
|
83
|
+
)
|
|
84
|
+
},
|
|
85
|
+
async deposit(_parameters, request) {
|
|
86
|
+
return (await provider.request(request)) as Rpc.wallet_deposit.Encoded['returns']
|
|
87
|
+
},
|
|
88
|
+
async depositZone(parameters, request) {
|
|
89
|
+
return (await provider.request({
|
|
90
|
+
...request,
|
|
91
|
+
params: [z.encode(Rpc.wallet_depositZone.parameters, parameters)] as const,
|
|
92
|
+
})) as Rpc.wallet_depositZone.Encoded['returns']
|
|
93
|
+
},
|
|
94
|
+
// `disconnect` (when `close` is set) only tears down the local
|
|
95
|
+
// wallet session — it is never forwarded. No `switchChain` action:
|
|
96
|
+
// chain selection is local state the provider handles itself, and
|
|
97
|
+
// forwarding it would open a wallet session for nothing.
|
|
98
|
+
...(close ? { disconnect: close } : {}),
|
|
99
|
+
async swap(_parameters, request) {
|
|
100
|
+
return (await provider.request(request)) as Rpc.wallet_swap.Encoded['returns']
|
|
101
|
+
},
|
|
102
|
+
async transfer(parameters, request) {
|
|
103
|
+
return (await provider.request({
|
|
104
|
+
...request,
|
|
105
|
+
params: [z.encode(Rpc.wallet_transfer.parameters, parameters)] as const,
|
|
106
|
+
})) as Rpc.wallet_transfer.Encoded['returns']
|
|
107
|
+
},
|
|
108
|
+
async withdrawZone(parameters, request) {
|
|
109
|
+
return (await provider.request({
|
|
110
|
+
...request,
|
|
111
|
+
params: [z.encode(Rpc.wallet_withdrawZone.parameters, parameters)] as const,
|
|
112
|
+
})) as Rpc.wallet_withdrawZone.Encoded['returns']
|
|
113
|
+
},
|
|
114
|
+
...actions?.({ getClient, request: requestRemote, store }),
|
|
94
115
|
},
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
116
|
+
...(cleanup ? { cleanup } : close ? { cleanup: () => void close() } : {}),
|
|
117
|
+
getAccount(parameters = {}) {
|
|
118
|
+
return {
|
|
119
|
+
account: {
|
|
120
|
+
address: parameters.address ?? getAccount({ signable: false }).address,
|
|
121
|
+
type: 'json-rpc' as const,
|
|
122
|
+
},
|
|
123
|
+
transport: custom(provider),
|
|
124
|
+
}
|
|
100
125
|
},
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return {
|
|
105
|
-
account: {
|
|
106
|
-
address: parameters.address ?? getAccount({ signable: false }).address,
|
|
107
|
-
type: 'json-rpc' as const,
|
|
108
|
-
},
|
|
109
|
-
transport: custom(provider),
|
|
110
|
-
}
|
|
111
|
-
},
|
|
112
|
-
}
|
|
113
|
-
})
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
)
|
|
114
129
|
}
|
|
115
130
|
|
|
116
131
|
export declare namespace fromRequest {
|
|
117
132
|
/** Options for {@link fromRequest}. */
|
|
118
133
|
export type Options = {
|
|
134
|
+
/**
|
|
135
|
+
* Adapter-specific action overrides layered over the request-forwarding
|
|
136
|
+
* defaults. Receives the request channel plus the adapter's client and
|
|
137
|
+
* store for actions that need local bookkeeping around an exchange.
|
|
138
|
+
*/
|
|
139
|
+
actions?:
|
|
140
|
+
| ((context: {
|
|
141
|
+
getClient: Adapter.SetupFn.Parameters['getClient']
|
|
142
|
+
request: Options['request']
|
|
143
|
+
store: Adapter.SetupFn.Parameters['store']
|
|
144
|
+
}) => Partial<Adapter.Instance['actions']>)
|
|
145
|
+
| undefined
|
|
119
146
|
/**
|
|
120
147
|
* Called once during setup with the adapter's store, for late wiring
|
|
121
148
|
* that needs to mutate local state (e.g. reconciling connection state
|