@joeywallet/wallet-sdk 0.2.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/LICENSE +21 -0
- package/README.md +705 -0
- package/dist/client.d.ts +55 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +224 -0
- package/dist/client.js.map +1 -0
- package/dist/detect.d.ts +45 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/detect.js +238 -0
- package/dist/detect.js.map +1 -0
- package/dist/errors.d.ts +75 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +120 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/mutation.d.ts +46 -0
- package/dist/mutation.d.ts.map +1 -0
- package/dist/mutation.js +67 -0
- package/dist/mutation.js.map +1 -0
- package/dist/provider.d.ts +159 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +142 -0
- package/dist/provider.js.map +1 -0
- package/dist/react.d.ts +76 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +225 -0
- package/dist/react.js.map +1 -0
- package/dist/types.d.ts +391 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +38 -0
- package/dist/types.js.map +1 -0
- package/dist/vanilla.d.ts +58 -0
- package/dist/vanilla.d.ts.map +1 -0
- package/dist/vanilla.js +161 -0
- package/dist/vanilla.js.map +1 -0
- package/package.json +70 -0
- package/src/client.ts +342 -0
- package/src/detect.ts +278 -0
- package/src/errors.ts +133 -0
- package/src/index.ts +97 -0
- package/src/mutation.ts +109 -0
- package/src/provider.ts +229 -0
- package/src/react.ts +375 -0
- package/src/types.ts +440 -0
- package/src/vanilla.ts +239 -0
package/src/detect.ts
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finding the wallet.
|
|
3
|
+
*
|
|
4
|
+
* This file is load-bearing for adoption, not just for correctness. Every XRPL
|
|
5
|
+
* wallet aggregator gives a wallet about one second to prove it exists, and a
|
|
6
|
+
* detection path that costs a message round trip to a sleeping MV3 service
|
|
7
|
+
* worker will lose that race intermittently — which the aggregator reports to
|
|
8
|
+
* the user as "not installed", the worst possible failure. So detection reads
|
|
9
|
+
* the page globals and returns; it never talks to the extension.
|
|
10
|
+
*/
|
|
11
|
+
import { createJoeyClient, type Joey } from './client.js'
|
|
12
|
+
import { JOEY_ERROR_CODES, JoeyRpcError, notInstalledError } from './errors.js'
|
|
13
|
+
import {
|
|
14
|
+
CAIP294_ANNOUNCE_EVENT,
|
|
15
|
+
CAIP294_PROMPT_EVENT,
|
|
16
|
+
WALLET_STANDARD_APP_READY_EVENT,
|
|
17
|
+
WALLET_STANDARD_REGISTER_EVENT,
|
|
18
|
+
isJoeyInjectedProvider,
|
|
19
|
+
type JoeyInjectedProvider,
|
|
20
|
+
} from './provider.js'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* One client per injected object.
|
|
24
|
+
*
|
|
25
|
+
* React re-renders and aggregator retries both call `getJoey()` repeatedly; a
|
|
26
|
+
* fresh client each time would leak the event subscriptions the previous one
|
|
27
|
+
* registered and break referential equality in hook dependency arrays.
|
|
28
|
+
*/
|
|
29
|
+
let cachedProvider: JoeyInjectedProvider | null = null
|
|
30
|
+
let cachedClient: Joey | null = null
|
|
31
|
+
|
|
32
|
+
interface JoeyGlobals {
|
|
33
|
+
joey?: unknown
|
|
34
|
+
xrpl?: { joey?: unknown } | undefined
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function globals(): JoeyGlobals | null {
|
|
38
|
+
// `globalThis` rather than `window` so importing this module in Node (SSR,
|
|
39
|
+
// Next.js, a test runner) is inert instead of a ReferenceError.
|
|
40
|
+
if (typeof globalThis === 'undefined') return null
|
|
41
|
+
return globalThis as unknown as JoeyGlobals
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readProvider(): JoeyInjectedProvider | null {
|
|
45
|
+
const scope = globals()
|
|
46
|
+
if (scope === null) return null
|
|
47
|
+
|
|
48
|
+
if (isJoeyInjectedProvider(scope.joey)) return scope.joey
|
|
49
|
+
// `window.xrpl` is Crossmark's namespace; Joey merges into it without
|
|
50
|
+
// touching anything already there, so this is a legitimate second home.
|
|
51
|
+
const nested = scope.xrpl?.joey
|
|
52
|
+
if (isJoeyInjectedProvider(nested)) return nested
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The Joey client, or `null` when the provider is not on the page.
|
|
58
|
+
*
|
|
59
|
+
* Synchronous by design. It is also safe to `await` — the value is not a
|
|
60
|
+
* promise, so `await getJoey()` costs one microtask and no round trip.
|
|
61
|
+
*/
|
|
62
|
+
export function getJoey(): Joey | null {
|
|
63
|
+
const provider = readProvider()
|
|
64
|
+
if (provider === null) return null
|
|
65
|
+
if (provider !== cachedProvider || cachedClient === null) {
|
|
66
|
+
cachedProvider = provider
|
|
67
|
+
cachedClient = createJoeyClient(provider)
|
|
68
|
+
}
|
|
69
|
+
return cachedClient
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Synchronous. Never sends a message, never awaits, never throws. */
|
|
73
|
+
export function isJoeyAvailable(): boolean {
|
|
74
|
+
return readProvider() !== null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Like {@link getJoey}, but throws `JoeyRpcError` 4900 instead of returning null. */
|
|
78
|
+
export function requireJoey(): Joey {
|
|
79
|
+
const joey = getJoey()
|
|
80
|
+
if (joey === null) throw notInstalledError()
|
|
81
|
+
return joey
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface WaitForJoeyOptions {
|
|
85
|
+
/** Default 3000. */
|
|
86
|
+
timeoutMs?: number
|
|
87
|
+
signal?: AbortSignal
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Ask any wallet already on the page to announce itself again.
|
|
92
|
+
*
|
|
93
|
+
* Necessary, not merely polite. `waitForJoey` exists for the case where the
|
|
94
|
+
* dapp's bundle ran *after* the extension installed its provider — and in that
|
|
95
|
+
* case both announcements have already been dispatched into a page with no
|
|
96
|
+
* listeners attached. Listening for a second one that will never arrive is how
|
|
97
|
+
* a detection helper times out against a wallet that is sitting right there,
|
|
98
|
+
* and the poll below was quietly carrying the whole feature.
|
|
99
|
+
*
|
|
100
|
+
* Both protocols define the app's half of the handshake and Joey implements
|
|
101
|
+
* both: CAIP-294's `wallet_prompt` makes it re-dispatch `wallet_announce`, and
|
|
102
|
+
* Wallet Standard's `wallet-standard:app-ready` carries a `register` callback
|
|
103
|
+
* that every listening wallet calls synchronously. The callback's argument is
|
|
104
|
+
* discarded — the SDK wants the injected provider global, not a Wallet Standard
|
|
105
|
+
* wallet object — but a wallet that can call it has finished installing, so it
|
|
106
|
+
* is a reliable "look again now" signal.
|
|
107
|
+
*
|
|
108
|
+
* Every step is guarded, but not the way it looks: see {@link dispatchQuietly}
|
|
109
|
+
* for why a `try`/`catch` is the wrong tool for a listener that throws. Failing
|
|
110
|
+
* to prompt costs a poll interval, never the detection.
|
|
111
|
+
*/
|
|
112
|
+
function promptForWallets(target: EventTarget, recheck: () => void): void {
|
|
113
|
+
dispatchQuietly(target, () => new CustomEvent(CAIP294_PROMPT_EVENT))
|
|
114
|
+
|
|
115
|
+
dispatchQuietly(
|
|
116
|
+
target,
|
|
117
|
+
() =>
|
|
118
|
+
new CustomEvent(WALLET_STANDARD_APP_READY_EVENT, {
|
|
119
|
+
detail: {
|
|
120
|
+
register: (): (() => void) => {
|
|
121
|
+
recheck()
|
|
122
|
+
return () => undefined
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
}),
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Dispatch one prompt event without a page listener's failure landing on
|
|
131
|
+
* anybody else.
|
|
132
|
+
*
|
|
133
|
+
* **`dispatchEvent` does not rethrow what a listener threw.** DOM's "inner
|
|
134
|
+
* invoke" *reports* the exception to the global instead and hands the
|
|
135
|
+
* dispatcher a plain boolean, so the `try`/`catch` that used to sit around
|
|
136
|
+
* these two calls caught nothing it claimed to. The SDK survived — it always
|
|
137
|
+
* would have — while the exception went on to the page's `onerror`, its crash
|
|
138
|
+
* reporter, and (in this repo) the test runner's unhandled-error channel, which
|
|
139
|
+
* failed a green suite with two errors nothing had thrown at it.
|
|
140
|
+
*
|
|
141
|
+
* So what is contained here is the *report*, not a throw. An `error` listener
|
|
142
|
+
* is installed on the global for the synchronous duration of this one dispatch
|
|
143
|
+
* and removed in `finally`. Reporting runs inline inside `dispatchEvent` and
|
|
144
|
+
* there is no `await` and no timer between install and removal, so the only
|
|
145
|
+
* errors it can possibly see are the ones this prompt caused; `preventDefault()`
|
|
146
|
+
* marks them handled, which is what suppresses the console entry and the
|
|
147
|
+
* runner's failure. Everything else the page reports is untouched.
|
|
148
|
+
*
|
|
149
|
+
* Realms differ, and the fallback is the same either way. Where the global
|
|
150
|
+
* reports asynchronously rather than through a cancellable `error` event —
|
|
151
|
+
* a bare Node `EventTarget` defers to `uncaughtException` — the absorber is
|
|
152
|
+
* already gone and the exception surfaces as that realm defines. Nothing about
|
|
153
|
+
* detection changes: the promise is unaffected in every case, because
|
|
154
|
+
* `dispatchEvent` never had the exception to give us.
|
|
155
|
+
*
|
|
156
|
+
* The `try`/`catch` that remains is for what genuinely does throw here: a realm
|
|
157
|
+
* with no `CustomEvent` constructor, or a global that refuses the dispatch.
|
|
158
|
+
*/
|
|
159
|
+
function dispatchQuietly(target: EventTarget, build: () => Event): void {
|
|
160
|
+
const absorb = (event: Event): void => {
|
|
161
|
+
event.preventDefault()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let absorbing = false
|
|
165
|
+
try {
|
|
166
|
+
target.addEventListener('error', absorb, true)
|
|
167
|
+
absorbing = true
|
|
168
|
+
} catch {
|
|
169
|
+
// A global that will not take an `error` listener. Nothing to absorb.
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
target.dispatchEvent(build())
|
|
174
|
+
} catch {
|
|
175
|
+
// No `CustomEvent` in this realm, or `dispatchEvent` refused it outright.
|
|
176
|
+
// The poll still covers us.
|
|
177
|
+
} finally {
|
|
178
|
+
if (absorbing) target.removeEventListener('error', absorb, true)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Resolve once the provider exists.
|
|
184
|
+
*
|
|
185
|
+
* For the case the synchronous path cannot cover: a dapp bundle that executed
|
|
186
|
+
* before the extension's `document_start` content script finished installing
|
|
187
|
+
* the global. Settles on the provider's own announcement events — and, because
|
|
188
|
+
* those may already have fired, asks for them again — with a slow poll as a
|
|
189
|
+
* backstop for a provider that installs the global without announcing at all.
|
|
190
|
+
*/
|
|
191
|
+
export function waitForJoey(options: WaitForJoeyOptions = {}): Promise<Joey> {
|
|
192
|
+
const { timeoutMs = 3000, signal } = options
|
|
193
|
+
|
|
194
|
+
const immediate = getJoey()
|
|
195
|
+
if (immediate !== null) return Promise.resolve(immediate)
|
|
196
|
+
|
|
197
|
+
const scope = globals()
|
|
198
|
+
if (scope === null || typeof (scope as { addEventListener?: unknown }).addEventListener !== 'function') {
|
|
199
|
+
return Promise.reject(notInstalledError())
|
|
200
|
+
}
|
|
201
|
+
const target = scope as unknown as EventTarget
|
|
202
|
+
|
|
203
|
+
return new Promise<Joey>((resolve, reject) => {
|
|
204
|
+
let settled = false
|
|
205
|
+
const cleanups: Array<() => void> = []
|
|
206
|
+
|
|
207
|
+
const finish = (run: () => void): void => {
|
|
208
|
+
if (settled) return
|
|
209
|
+
settled = true
|
|
210
|
+
for (const cleanup of cleanups) cleanup()
|
|
211
|
+
run()
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const check = (): void => {
|
|
215
|
+
const joey = getJoey()
|
|
216
|
+
if (joey !== null) finish(() => resolve(joey))
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
for (const event of [
|
|
220
|
+
CAIP294_ANNOUNCE_EVENT,
|
|
221
|
+
WALLET_STANDARD_REGISTER_EVENT,
|
|
222
|
+
'DOMContentLoaded',
|
|
223
|
+
'load',
|
|
224
|
+
]) {
|
|
225
|
+
target.addEventListener(event, check)
|
|
226
|
+
cleanups.push(() => target.removeEventListener(event, check))
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const poll = setInterval(check, 50)
|
|
230
|
+
cleanups.push(() => clearInterval(poll))
|
|
231
|
+
|
|
232
|
+
const timer = setTimeout(() => {
|
|
233
|
+
finish(() =>
|
|
234
|
+
reject(
|
|
235
|
+
new JoeyRpcError(
|
|
236
|
+
JOEY_ERROR_CODES.DISCONNECTED,
|
|
237
|
+
`Joey Wallet did not announce itself within ${timeoutMs}ms. It is probably not installed.`,
|
|
238
|
+
),
|
|
239
|
+
),
|
|
240
|
+
)
|
|
241
|
+
}, timeoutMs)
|
|
242
|
+
cleanups.push(() => clearTimeout(timer))
|
|
243
|
+
|
|
244
|
+
if (signal !== undefined) {
|
|
245
|
+
const onAbort = (): void => {
|
|
246
|
+
finish(() =>
|
|
247
|
+
reject(new JoeyRpcError(JOEY_ERROR_CODES.INTERNAL, 'Wallet detection was aborted.')),
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
if (signal.aborted) {
|
|
251
|
+
onAbort()
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
signal.addEventListener('abort', onAbort, { once: true })
|
|
255
|
+
cleanups.push(() => signal.removeEventListener('abort', onAbort))
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Covers a provider installed between the synchronous check above and the
|
|
259
|
+
// listeners being attached.
|
|
260
|
+
check()
|
|
261
|
+
if (settled) return
|
|
262
|
+
|
|
263
|
+
// Now that we are listening, ask the wallets that announced before we
|
|
264
|
+
// existed to say it again.
|
|
265
|
+
promptForWallets(target, check)
|
|
266
|
+
})
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Drop the cached client.
|
|
271
|
+
*
|
|
272
|
+
* Only needed by tests and by SPAs that swap the provider at runtime; normal
|
|
273
|
+
* dapps never call it.
|
|
274
|
+
*/
|
|
275
|
+
export function resetJoeyDetection(): void {
|
|
276
|
+
cachedProvider = null
|
|
277
|
+
cachedClient = null
|
|
278
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one error type every SDK method rejects with.
|
|
3
|
+
*
|
|
4
|
+
* Codes are EIP-1193 numbers verbatim rather than XRPL-specific strings,
|
|
5
|
+
* because every wallet aggregator in this ecosystem already branches on them.
|
|
6
|
+
* Two of them are Joey extensions in the same numeric space: 4300 (locked) and
|
|
7
|
+
* 4902 (unrecognised chain).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const JOEY_ERROR_CODES = {
|
|
11
|
+
/** The user declined the request in the wallet. */
|
|
12
|
+
USER_REJECTED: 4001,
|
|
13
|
+
/** The origin has not been granted access to the requested account. */
|
|
14
|
+
UNAUTHORIZED: 4100,
|
|
15
|
+
/** The provider does not implement this method. */
|
|
16
|
+
UNSUPPORTED_METHOD: 4200,
|
|
17
|
+
/** A vault exists but is locked, and the user did not unlock it. */
|
|
18
|
+
LOCKED: 4300,
|
|
19
|
+
/** No provider, or the provider is not connected to this origin. */
|
|
20
|
+
DISCONNECTED: 4900,
|
|
21
|
+
/** The provider is connected but cannot reach the requested chain. */
|
|
22
|
+
CHAIN_DISCONNECTED: 4901,
|
|
23
|
+
/**
|
|
24
|
+
* The requested chain is not one of `xrpl:0`, `xrpl:1`, `xrpl:2` — or the
|
|
25
|
+
* wallet is on a different one and will not sign for the one you asked for.
|
|
26
|
+
*
|
|
27
|
+
* Spelled with a `z`. Both spellings existed: the wallet's
|
|
28
|
+
* `ProviderErrorCode` used `UNRECOGNIZED_CHAIN` and this table used
|
|
29
|
+
* `UNRECOGNISED_CHAIN`, which is exactly the kind of thing that survives
|
|
30
|
+
* until a dapp imports the wrong one. EIP-1193 names the code
|
|
31
|
+
* "Unrecognized chain ID", so the standard's spelling wins over the rest of
|
|
32
|
+
* this repo's British prose, and the wallet no longer declares a second copy
|
|
33
|
+
* to disagree with.
|
|
34
|
+
*/
|
|
35
|
+
UNRECOGNIZED_CHAIN: 4902,
|
|
36
|
+
/**
|
|
37
|
+
* Too many requests from this origin in too short a window.
|
|
38
|
+
*
|
|
39
|
+
* Reachable two ways, and a dapp that does not handle it will look broken in
|
|
40
|
+
* both: the content-script bridge caps concurrent in-flight requests, and the
|
|
41
|
+
* wallet blocks an origin the user has rejected three times in a row. Back
|
|
42
|
+
* off; do not retry in a loop.
|
|
43
|
+
*/
|
|
44
|
+
LIMIT_EXCEEDED: -32005,
|
|
45
|
+
/** The message was not a well-formed request — no method, or not an object. */
|
|
46
|
+
INVALID_REQUEST: -32600,
|
|
47
|
+
/** Malformed arguments; the request never reached the approval queue. */
|
|
48
|
+
INVALID_PARAMS: -32602,
|
|
49
|
+
/** Anything the SDK could not classify. */
|
|
50
|
+
INTERNAL: -32603,
|
|
51
|
+
} as const
|
|
52
|
+
|
|
53
|
+
export type JoeyErrorCode = (typeof JOEY_ERROR_CODES)[keyof typeof JOEY_ERROR_CODES]
|
|
54
|
+
|
|
55
|
+
export class JoeyRpcError extends Error {
|
|
56
|
+
readonly code: number
|
|
57
|
+
readonly data?: unknown
|
|
58
|
+
|
|
59
|
+
constructor(code: number, message: string, data?: unknown) {
|
|
60
|
+
super(message)
|
|
61
|
+
this.name = 'JoeyRpcError'
|
|
62
|
+
this.code = code
|
|
63
|
+
if (data !== undefined) this.data = data
|
|
64
|
+
// Restores the prototype chain when this file is transpiled to ES5 by a
|
|
65
|
+
// consumer's bundler, so `instanceof JoeyRpcError` keeps working.
|
|
66
|
+
Object.setPrototypeOf(this, JoeyRpcError.prototype)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Normalise anything a provider threw into a `JoeyRpcError`.
|
|
71
|
+
*
|
|
72
|
+
* Providers reject with plain objects at least as often as with Errors, and
|
|
73
|
+
* some older shims reject with a bare string.
|
|
74
|
+
*/
|
|
75
|
+
static from(value: unknown, fallbackCode: number = JOEY_ERROR_CODES.INTERNAL): JoeyRpcError {
|
|
76
|
+
if (value instanceof JoeyRpcError) return value
|
|
77
|
+
|
|
78
|
+
if (typeof value === 'string') {
|
|
79
|
+
return new JoeyRpcError(codeFromMessage(value, fallbackCode), value)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (typeof value === 'object' && value !== null) {
|
|
83
|
+
const record = value as { code?: unknown; message?: unknown; data?: unknown }
|
|
84
|
+
const message =
|
|
85
|
+
typeof record.message === 'string' && record.message.length > 0
|
|
86
|
+
? record.message
|
|
87
|
+
: 'The wallet request failed.'
|
|
88
|
+
const code =
|
|
89
|
+
typeof record.code === 'number' ? record.code : codeFromMessage(message, fallbackCode)
|
|
90
|
+
return new JoeyRpcError(code, message, record.data)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return new JoeyRpcError(fallbackCode, 'The wallet request failed.')
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* True when the user declined.
|
|
99
|
+
*
|
|
100
|
+
* Also matches on the message, because a provider that predates the numeric
|
|
101
|
+
* codes (or a shim in between) may only carry the word. This is the same
|
|
102
|
+
* string match the existing XRPL adapters do, which is why every rejection the
|
|
103
|
+
* SDK constructs itself is worded to contain "rejected".
|
|
104
|
+
*/
|
|
105
|
+
export function isUserRejection(error: unknown): boolean {
|
|
106
|
+
if (error instanceof JoeyRpcError) {
|
|
107
|
+
if (error.code === JOEY_ERROR_CODES.USER_REJECTED) return true
|
|
108
|
+
return /reject/i.test(error.message)
|
|
109
|
+
}
|
|
110
|
+
if (typeof error === 'object' && error !== null) {
|
|
111
|
+
const record = error as { code?: unknown; message?: unknown }
|
|
112
|
+
if (record.code === JOEY_ERROR_CODES.USER_REJECTED) return true
|
|
113
|
+
return typeof record.message === 'string' && /reject/i.test(record.message)
|
|
114
|
+
}
|
|
115
|
+
return typeof error === 'string' && /reject/i.test(error)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function userRejectedError(what = 'The user rejected the request.'): JoeyRpcError {
|
|
119
|
+
return new JoeyRpcError(JOEY_ERROR_CODES.USER_REJECTED, what)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function notInstalledError(): JoeyRpcError {
|
|
123
|
+
return new JoeyRpcError(
|
|
124
|
+
JOEY_ERROR_CODES.DISCONNECTED,
|
|
125
|
+
'Joey Wallet is not installed, or its provider has not been injected into this page.',
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function codeFromMessage(message: string, fallbackCode: number): number {
|
|
130
|
+
return /reject|denied|declined|cancell?ed/i.test(message)
|
|
131
|
+
? JOEY_ERROR_CODES.USER_REJECTED
|
|
132
|
+
: fallbackCode
|
|
133
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@joeywallet/wallet-sdk` — the core API.
|
|
3
|
+
*
|
|
4
|
+
* Zero runtime dependencies, ESM only, safe to import in Node (every function
|
|
5
|
+
* that needs a page checks for one first).
|
|
6
|
+
*/
|
|
7
|
+
export {
|
|
8
|
+
getJoey,
|
|
9
|
+
isJoeyAvailable,
|
|
10
|
+
requireJoey,
|
|
11
|
+
resetJoeyDetection,
|
|
12
|
+
waitForJoey,
|
|
13
|
+
type WaitForJoeyOptions,
|
|
14
|
+
} from './detect.js'
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
createJoeyClient,
|
|
18
|
+
readAccounts,
|
|
19
|
+
readChain,
|
|
20
|
+
readNetwork,
|
|
21
|
+
type Joey,
|
|
22
|
+
} from './client.js'
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
JOEY_ERROR_CODES,
|
|
26
|
+
JoeyRpcError,
|
|
27
|
+
isUserRejection,
|
|
28
|
+
notInstalledError,
|
|
29
|
+
userRejectedError,
|
|
30
|
+
type JoeyErrorCode,
|
|
31
|
+
} from './errors.js'
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
APPROVAL_TIMEOUT_MS,
|
|
35
|
+
CAIP294_ANNOUNCE_EVENT,
|
|
36
|
+
CAIP294_PROMPT_EVENT,
|
|
37
|
+
JOEY_DAPP_FORBIDDEN_TRANSACTION_TYPES,
|
|
38
|
+
JOEY_RDNS,
|
|
39
|
+
JOEY_RPC_METHODS,
|
|
40
|
+
JOEY_WALLET_NAME,
|
|
41
|
+
MAX_BULK_TRANSACTIONS,
|
|
42
|
+
REQUEST_TIMEOUT_MS,
|
|
43
|
+
WALLET_STANDARD_APP_READY_EVENT,
|
|
44
|
+
WALLET_STANDARD_REGISTER_EVENT,
|
|
45
|
+
invoke,
|
|
46
|
+
isJoeyInjectedProvider,
|
|
47
|
+
subscribe,
|
|
48
|
+
type JoeyInjectedProvider,
|
|
49
|
+
type JoeyProviderEventName,
|
|
50
|
+
type JoeyRequestArguments,
|
|
51
|
+
type JoeyRpcMethod,
|
|
52
|
+
} from './provider.js'
|
|
53
|
+
|
|
54
|
+
export {
|
|
55
|
+
JOEY_CHAINS,
|
|
56
|
+
chainForNetworkId,
|
|
57
|
+
isJoeyChain,
|
|
58
|
+
networkIdForChain,
|
|
59
|
+
type Amount,
|
|
60
|
+
type AnyTransaction,
|
|
61
|
+
type ConnectParams,
|
|
62
|
+
type ConnectResult,
|
|
63
|
+
type IssuedCurrencyAmount,
|
|
64
|
+
type JoeyAccount,
|
|
65
|
+
type JoeyChain,
|
|
66
|
+
type JoeyEventListener,
|
|
67
|
+
type JoeyEventMap,
|
|
68
|
+
type JoeyEventName,
|
|
69
|
+
type JoeyNetwork,
|
|
70
|
+
type Memo,
|
|
71
|
+
type MPTAmount,
|
|
72
|
+
type Path,
|
|
73
|
+
type PathStep,
|
|
74
|
+
type SignAndSubmitTransactionResult,
|
|
75
|
+
type SigningContextParams,
|
|
76
|
+
type SignInMode,
|
|
77
|
+
type SignInParams,
|
|
78
|
+
type SignInResult,
|
|
79
|
+
type BulkEntryResult,
|
|
80
|
+
type BulkEntryStatus,
|
|
81
|
+
type SignTransactionBulkFailure,
|
|
82
|
+
type SignTransactionBulkParams,
|
|
83
|
+
type SignTransactionForParams,
|
|
84
|
+
type SignTransactionParams,
|
|
85
|
+
type SignTransactionResult,
|
|
86
|
+
type Signer,
|
|
87
|
+
type TransactionLike,
|
|
88
|
+
} from './types.js'
|
|
89
|
+
|
|
90
|
+
export {
|
|
91
|
+
initialMutationState,
|
|
92
|
+
mutationReducer,
|
|
93
|
+
toPublicState,
|
|
94
|
+
type MutationAction,
|
|
95
|
+
type MutationState,
|
|
96
|
+
type MutationStatus,
|
|
97
|
+
} from './mutation.js'
|
package/src/mutation.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The state machine behind the react-query-shaped hooks.
|
|
3
|
+
*
|
|
4
|
+
* Kept here, free of React, for two reasons: the reducer is the part with the
|
|
5
|
+
* interesting edge cases (a stale response landing after a newer one), and it is
|
|
6
|
+
* testable without a DOM. `@joeywallet/wallet-sdk/react` is a thin binding over it, so
|
|
7
|
+
* the SDK gains react-query's ergonomics without react-query's dependency.
|
|
8
|
+
*/
|
|
9
|
+
import { JoeyRpcError } from './errors.js'
|
|
10
|
+
|
|
11
|
+
export type MutationStatus = 'idle' | 'pending' | 'success' | 'error'
|
|
12
|
+
|
|
13
|
+
export interface MutationState<TData, TVariables> {
|
|
14
|
+
status: MutationStatus
|
|
15
|
+
data: TData | undefined
|
|
16
|
+
error: JoeyRpcError | undefined
|
|
17
|
+
/** The arguments of the most recent call, kept so a retry needs no closure. */
|
|
18
|
+
variables: TVariables | undefined
|
|
19
|
+
isIdle: boolean
|
|
20
|
+
isPending: boolean
|
|
21
|
+
isSuccess: boolean
|
|
22
|
+
isError: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type MutationAction<TData, TVariables> =
|
|
26
|
+
| { type: 'reset' }
|
|
27
|
+
| { type: 'start'; variables: TVariables; runId: number }
|
|
28
|
+
| { type: 'success'; data: TData; runId: number }
|
|
29
|
+
| { type: 'error'; error: JoeyRpcError; runId: number }
|
|
30
|
+
|
|
31
|
+
interface InternalState<TData, TVariables> extends MutationState<TData, TVariables> {
|
|
32
|
+
/** Identifies the call whose result may still write to this state. */
|
|
33
|
+
runId: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function initialMutationState<TData, TVariables>(): InternalState<TData, TVariables> {
|
|
37
|
+
return {
|
|
38
|
+
status: 'idle',
|
|
39
|
+
data: undefined,
|
|
40
|
+
error: undefined,
|
|
41
|
+
variables: undefined,
|
|
42
|
+
isIdle: true,
|
|
43
|
+
isPending: false,
|
|
44
|
+
isSuccess: false,
|
|
45
|
+
isError: false,
|
|
46
|
+
runId: 0,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function mutationReducer<TData, TVariables>(
|
|
51
|
+
state: InternalState<TData, TVariables>,
|
|
52
|
+
action: MutationAction<TData, TVariables>,
|
|
53
|
+
): InternalState<TData, TVariables> {
|
|
54
|
+
switch (action.type) {
|
|
55
|
+
case 'reset':
|
|
56
|
+
return { ...initialMutationState<TData, TVariables>(), runId: state.runId }
|
|
57
|
+
|
|
58
|
+
case 'start':
|
|
59
|
+
return {
|
|
60
|
+
status: 'pending',
|
|
61
|
+
// The previous result is cleared so a stale success cannot be rendered
|
|
62
|
+
// next to a spinner for the call that superseded it.
|
|
63
|
+
data: undefined,
|
|
64
|
+
error: undefined,
|
|
65
|
+
variables: action.variables,
|
|
66
|
+
isIdle: false,
|
|
67
|
+
isPending: true,
|
|
68
|
+
isSuccess: false,
|
|
69
|
+
isError: false,
|
|
70
|
+
runId: action.runId,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
case 'success':
|
|
74
|
+
// A response from a superseded call is dropped: the user clicked twice,
|
|
75
|
+
// and the answer to the first click must not overwrite the second.
|
|
76
|
+
if (action.runId !== state.runId) return state
|
|
77
|
+
return {
|
|
78
|
+
...state,
|
|
79
|
+
status: 'success',
|
|
80
|
+
data: action.data,
|
|
81
|
+
error: undefined,
|
|
82
|
+
isIdle: false,
|
|
83
|
+
isPending: false,
|
|
84
|
+
isSuccess: true,
|
|
85
|
+
isError: false,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
case 'error':
|
|
89
|
+
if (action.runId !== state.runId) return state
|
|
90
|
+
return {
|
|
91
|
+
...state,
|
|
92
|
+
status: 'error',
|
|
93
|
+
data: undefined,
|
|
94
|
+
error: action.error,
|
|
95
|
+
isIdle: false,
|
|
96
|
+
isPending: false,
|
|
97
|
+
isSuccess: false,
|
|
98
|
+
isError: true,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The public half of {@link InternalState}, with `runId` dropped. */
|
|
104
|
+
export function toPublicState<TData, TVariables>(
|
|
105
|
+
state: InternalState<TData, TVariables>,
|
|
106
|
+
): MutationState<TData, TVariables> {
|
|
107
|
+
const { runId: _runId, ...rest } = state
|
|
108
|
+
return rest
|
|
109
|
+
}
|