@joeywallet/gemwallet-compat 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 +146 -0
- package/dist/convert.d.ts +50 -0
- package/dist/convert.d.ts.map +1 -0
- package/dist/convert.js +143 -0
- package/dist/convert.js.map +1 -0
- package/dist/envelope.d.ts +10 -0
- package/dist/envelope.d.ts.map +1 -0
- package/dist/envelope.js +29 -0
- package/dist/envelope.js.map +1 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +279 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +182 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/unsupported.d.ts +36 -0
- package/dist/unsupported.d.ts.map +1 -0
- package/dist/unsupported.js +53 -0
- package/dist/unsupported.js.map +1 -0
- package/package.json +53 -0
- package/src/convert.ts +183 -0
- package/src/envelope.ts +31 -0
- package/src/index.ts +394 -0
- package/src/types.ts +190 -0
- package/src/unsupported.ts +62 -0
package/src/convert.ts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GemWallet request shapes to XRPL transaction JSON.
|
|
3
|
+
*
|
|
4
|
+
* GemWallet's payloads are camelCase and its memo/signer wrappers are lowercase
|
|
5
|
+
* (`{ memo: { memoData } }`), while the ledger — and therefore everything Joey
|
|
6
|
+
* signs — uses XRPL's PascalCase field names. Every mapping here is mechanical;
|
|
7
|
+
* it is separated out because a mistake in it would show up as a transaction
|
|
8
|
+
* that silently loses a destination tag or a memo.
|
|
9
|
+
*/
|
|
10
|
+
import type {
|
|
11
|
+
AnyTransaction,
|
|
12
|
+
JoeyChain,
|
|
13
|
+
JoeyNetwork,
|
|
14
|
+
Memo as XrplMemo,
|
|
15
|
+
Signer as XrplSigner,
|
|
16
|
+
} from '@joeywallet/wallet-sdk'
|
|
17
|
+
|
|
18
|
+
import type {
|
|
19
|
+
BaseTransactionRequest,
|
|
20
|
+
Memo as GemMemo,
|
|
21
|
+
Network,
|
|
22
|
+
SendPaymentRequest,
|
|
23
|
+
SetTrustlineRequest,
|
|
24
|
+
Signer as GemSigner,
|
|
25
|
+
} from './types.js'
|
|
26
|
+
|
|
27
|
+
/** Drops keys whose value is `undefined` so the wire message stays minimal. */
|
|
28
|
+
function defined<T extends Record<string, unknown>>(source: T): Record<string, unknown> {
|
|
29
|
+
const out: Record<string, unknown> = {}
|
|
30
|
+
for (const [key, value] of Object.entries(source)) {
|
|
31
|
+
if (value !== undefined) out[key] = value
|
|
32
|
+
}
|
|
33
|
+
return out
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function toXrplMemos(memos: GemMemo[] | undefined): XrplMemo[] | undefined {
|
|
37
|
+
if (memos === undefined) return undefined
|
|
38
|
+
return memos.map((entry) => ({
|
|
39
|
+
Memo: defined({
|
|
40
|
+
MemoData: entry.memo.memoData,
|
|
41
|
+
MemoType: entry.memo.memoType,
|
|
42
|
+
MemoFormat: entry.memo.memoFormat,
|
|
43
|
+
}) as XrplMemo['Memo'],
|
|
44
|
+
}))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function toXrplSigners(signers: GemSigner[] | undefined): XrplSigner[] | undefined {
|
|
48
|
+
if (signers === undefined) return undefined
|
|
49
|
+
return signers.map((entry) => ({
|
|
50
|
+
Signer: {
|
|
51
|
+
Account: entry.signer.account,
|
|
52
|
+
TxnSignature: entry.signer.txnSignature,
|
|
53
|
+
SigningPubKey: entry.signer.signingPubKey,
|
|
54
|
+
},
|
|
55
|
+
}))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The fields every GemWallet transaction request shares. */
|
|
59
|
+
export function toXrplCommonFields(request: BaseTransactionRequest): Record<string, unknown> {
|
|
60
|
+
return defined({
|
|
61
|
+
Fee: request.fee,
|
|
62
|
+
Sequence: request.sequence,
|
|
63
|
+
AccountTxnID: request.accountTxnID,
|
|
64
|
+
LastLedgerSequence: request.lastLedgerSequence,
|
|
65
|
+
Memos: toXrplMemos(request.memos),
|
|
66
|
+
NetworkID: request.networkID,
|
|
67
|
+
Signers: toXrplSigners(request.signers),
|
|
68
|
+
SourceTag: request.sourceTag,
|
|
69
|
+
SigningPubKey: request.signingPubKey,
|
|
70
|
+
TicketSequence: request.ticketSequence,
|
|
71
|
+
TxnSignature: request.txnSignature,
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* GemWallet's `sendPayment` payload as an XRPL `Payment`.
|
|
77
|
+
*
|
|
78
|
+
* **`amount` crosses untouched, and that is correct — checked, not assumed.**
|
|
79
|
+
* It is the one line in this package where being wrong costs a factor of a
|
|
80
|
+
* million, so the source is named rather than trusted to memory:
|
|
81
|
+
* `@gemwallet/api@3.8.0` types `SendPaymentRequest.amount` as xrpl.js's
|
|
82
|
+
* `Amount`, and `packages/constants/src/payload/payload.types.ts` documents it
|
|
83
|
+
* verbatim as *"A string representing the number of XRP to deliver, in drops."*
|
|
84
|
+
* Joey's `Payment.Amount` is drops. Same unit, no conversion, and a converting
|
|
85
|
+
* shim here would be the bug rather than the fix.
|
|
86
|
+
*
|
|
87
|
+
* The one place GemWallet does convert is its extension's `parseAmount`, and
|
|
88
|
+
* only for its deprecated v1 URL-parameter path, where `amount` arrives as a
|
|
89
|
+
* *number* of XRP. That path is not this API and never reaches this function:
|
|
90
|
+
* `amount` here is a `string` or an issued-currency object, both of which
|
|
91
|
+
* GemWallet forwards to the ledger exactly as this does.
|
|
92
|
+
*
|
|
93
|
+
* `convert.test.ts` pins both halves — drops for XRP, `value` untouched for an
|
|
94
|
+
* issued currency — so a future "helpful" `xrpToDrops` fails a test instead of
|
|
95
|
+
* a user's payment.
|
|
96
|
+
*/
|
|
97
|
+
export function toPaymentTransaction(
|
|
98
|
+
request: SendPaymentRequest,
|
|
99
|
+
account?: string,
|
|
100
|
+
): AnyTransaction {
|
|
101
|
+
return {
|
|
102
|
+
TransactionType: 'Payment',
|
|
103
|
+
...(account === undefined ? {} : { Account: account }),
|
|
104
|
+
...toXrplCommonFields(request),
|
|
105
|
+
...defined({
|
|
106
|
+
// Drops. See the note above before changing this.
|
|
107
|
+
Amount: request.amount,
|
|
108
|
+
Destination: request.destination,
|
|
109
|
+
DestinationTag: request.destinationTag,
|
|
110
|
+
InvoiceID: request.invoiceID,
|
|
111
|
+
Paths: request.paths,
|
|
112
|
+
SendMax: request.sendMax,
|
|
113
|
+
DeliverMin: request.deliverMin,
|
|
114
|
+
Flags: request.flags,
|
|
115
|
+
}),
|
|
116
|
+
} as AnyTransaction
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function toTrustSetTransaction(
|
|
120
|
+
request: SetTrustlineRequest,
|
|
121
|
+
account?: string,
|
|
122
|
+
): AnyTransaction {
|
|
123
|
+
return {
|
|
124
|
+
TransactionType: 'TrustSet',
|
|
125
|
+
...(account === undefined ? {} : { Account: account }),
|
|
126
|
+
...toXrplCommonFields(request),
|
|
127
|
+
...defined({
|
|
128
|
+
LimitAmount: request.limitAmount,
|
|
129
|
+
QualityIn: request.qualityIn,
|
|
130
|
+
QualityOut: request.qualityOut,
|
|
131
|
+
Flags: request.flags,
|
|
132
|
+
}),
|
|
133
|
+
} as AnyTransaction
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* ------------------------------------------------------------------ networks */
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* The public endpoints for each chain.
|
|
140
|
+
*
|
|
141
|
+
* Duplicated from the extension's `shared/types.ts` `NETWORKS` rather than
|
|
142
|
+
* imported: this package is published to npm and cannot depend on the
|
|
143
|
+
* extension's source. GemWallet's `getNetwork()` contract promises a
|
|
144
|
+
* `websocket`, and dapps feed it straight into an xrpl.js `Client`, so
|
|
145
|
+
* answering with an empty string would break every migrating caller. They must
|
|
146
|
+
* be kept in step with the extension if an endpoint ever moves.
|
|
147
|
+
*/
|
|
148
|
+
const WEBSOCKET_BY_CHAIN: Record<JoeyChain, string> = {
|
|
149
|
+
'xrpl:0': 'wss://s1.ripple.com/',
|
|
150
|
+
'xrpl:1': 'wss://testnet.xrpl-labs.com/',
|
|
151
|
+
'xrpl:2': 'wss://s.devnet.rippletest.net:51233/',
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const GEM_NAME_BY_CHAIN: Record<JoeyChain, Network> = {
|
|
155
|
+
'xrpl:0': 'Mainnet',
|
|
156
|
+
'xrpl:1': 'Testnet',
|
|
157
|
+
'xrpl:2': 'Devnet',
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const DESCRIPTION_BY_CHAIN: Record<JoeyChain, string> = {
|
|
161
|
+
'xrpl:0': 'Main XRPL network',
|
|
162
|
+
'xrpl:1': 'XRPL Testnet',
|
|
163
|
+
'xrpl:2': 'XRPL Devnet',
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Joey's network to GemWallet's `Network` string.
|
|
168
|
+
*
|
|
169
|
+
* GemWallet also has `Custom`, which Joey never returns: the extension ships a
|
|
170
|
+
* fixed set of three endpoints.
|
|
171
|
+
*/
|
|
172
|
+
export function toGemNetwork(network: JoeyNetwork): Network {
|
|
173
|
+
return GEM_NAME_BY_CHAIN[network.chain]
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function toGemWebsocket(network: JoeyNetwork): string {
|
|
177
|
+
return WEBSOCKET_BY_CHAIN[network.chain]
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** GemWallet's human-readable network description, for the event payload. */
|
|
181
|
+
export function toGemNetworkDescription(network: JoeyNetwork): string {
|
|
182
|
+
return DESCRIPTION_BY_CHAIN[network.chain]
|
|
183
|
+
}
|
package/src/envelope.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GemWallet's `{ type, result }` envelope, reconstructed on top of Joey's
|
|
3
|
+
* promise-rejection API.
|
|
4
|
+
*
|
|
5
|
+
* The two models disagree about exactly one thing, and this file is where that
|
|
6
|
+
* disagreement is resolved: GemWallet reports a user declining as a *resolved*
|
|
7
|
+
* `{ type: 'reject' }`, and reports everything else by throwing. Joey rejects
|
|
8
|
+
* with a `JoeyRpcError` in both cases. So a 4001 becomes `{ type: 'reject' }`
|
|
9
|
+
* and every other code is rethrown, which is what a GemWallet dapp's existing
|
|
10
|
+
* `if (result.type === 'reject')` branch and its `try`/`catch` already expect.
|
|
11
|
+
*/
|
|
12
|
+
import { JoeyRpcError, isUserRejection } from '@joeywallet/wallet-sdk'
|
|
13
|
+
|
|
14
|
+
import type { BaseResponse } from './types.js'
|
|
15
|
+
|
|
16
|
+
export function response<T>(result: T): BaseResponse<T> & { type: 'response'; result: T } {
|
|
17
|
+
return { type: 'response', result }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function rejected<T>(): BaseResponse<T> & { type: 'reject' } {
|
|
21
|
+
return { type: 'reject', result: undefined }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function envelope<T>(run: () => Promise<T>): Promise<BaseResponse<T>> {
|
|
25
|
+
try {
|
|
26
|
+
return response(await run())
|
|
27
|
+
} catch (cause) {
|
|
28
|
+
if (isUserRejection(cause)) return rejected<T>()
|
|
29
|
+
throw JoeyRpcError.from(cause)
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@joeywallet/gemwallet-compat` — GemWallet's `@gemwallet/api` surface over Joey.
|
|
3
|
+
*
|
|
4
|
+
* - import { isInstalled, getAddress, sendPayment } from '@gemwallet/api'
|
|
5
|
+
* + import { isInstalled, getAddress, sendPayment } from '@joeywallet/gemwallet-compat'
|
|
6
|
+
*
|
|
7
|
+
* Function names, argument shapes and the `{ type, result }` envelope match
|
|
8
|
+
* `@gemwallet/api@3.8.0`. Four functions deliberately do not exist as working
|
|
9
|
+
* calls — see `./unsupported.ts`.
|
|
10
|
+
*/
|
|
11
|
+
import {
|
|
12
|
+
JOEY_ERROR_CODES,
|
|
13
|
+
JoeyRpcError,
|
|
14
|
+
getJoey,
|
|
15
|
+
waitForJoey,
|
|
16
|
+
type Joey,
|
|
17
|
+
type JoeyAccount,
|
|
18
|
+
type JoeyNetwork,
|
|
19
|
+
} from '@joeywallet/wallet-sdk'
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
toGemNetwork,
|
|
23
|
+
toGemNetworkDescription,
|
|
24
|
+
toGemWebsocket,
|
|
25
|
+
toPaymentTransaction,
|
|
26
|
+
toTrustSetTransaction,
|
|
27
|
+
} from './convert.js'
|
|
28
|
+
import { envelope } from './envelope.js'
|
|
29
|
+
import { refuse } from './unsupported.js'
|
|
30
|
+
import type {
|
|
31
|
+
EventLoginResponse,
|
|
32
|
+
EventLogoutResponse,
|
|
33
|
+
EventNetworkChangedResponse,
|
|
34
|
+
EventWalletChangedResponse,
|
|
35
|
+
GemEventPayloadMap,
|
|
36
|
+
GemEventType,
|
|
37
|
+
GetAddressResponse,
|
|
38
|
+
GetNetworkResponse,
|
|
39
|
+
GetPublicKeyResponse,
|
|
40
|
+
IsInstalledResponse,
|
|
41
|
+
SendPaymentRequest,
|
|
42
|
+
SendPaymentResponse,
|
|
43
|
+
SetTrustlineRequest,
|
|
44
|
+
SetTrustlineResponse,
|
|
45
|
+
SignMessageResponse,
|
|
46
|
+
SignTransactionRequest,
|
|
47
|
+
SignTransactionResponse,
|
|
48
|
+
SubmitBulkTransactionsRequest,
|
|
49
|
+
SubmitBulkTransactionsResponse,
|
|
50
|
+
SubmitTransactionRequest,
|
|
51
|
+
SubmitTransactionResponse,
|
|
52
|
+
TransactionBulkResponse,
|
|
53
|
+
} from './types.js'
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* How long to wait for a provider that has not been injected yet.
|
|
57
|
+
*
|
|
58
|
+
* 1000ms is GemWallet's own number, and dapps written against it already treat
|
|
59
|
+
* a slower answer as "not installed".
|
|
60
|
+
*/
|
|
61
|
+
const DETECT_TIMEOUT_MS = 1000
|
|
62
|
+
|
|
63
|
+
async function requireProvider(): Promise<Joey> {
|
|
64
|
+
const immediate = getJoey()
|
|
65
|
+
if (immediate !== null) return immediate
|
|
66
|
+
return await waitForJoey({ timeoutMs: DETECT_TIMEOUT_MS })
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The account this origin may use, connecting first if it has to.
|
|
71
|
+
*
|
|
72
|
+
* `connect({ silent: true })` rather than `getAccounts()` because GemWallet's
|
|
73
|
+
* `getPublicKey()` needs the public key, and only the connect result carries
|
|
74
|
+
* it — `getAccounts()` answers with bare addresses. A silent connect resolves
|
|
75
|
+
* with an empty list rather than throwing when the origin has no grant, so the
|
|
76
|
+
* non-silent call below is what actually opens the approval window, matching
|
|
77
|
+
* GemWallet's behaviour of prompting from `getAddress()`.
|
|
78
|
+
*/
|
|
79
|
+
async function requireAccount(): Promise<{ joey: Joey; account: JoeyAccount }> {
|
|
80
|
+
const joey = await requireProvider()
|
|
81
|
+
|
|
82
|
+
let result = await joey.connect({ silent: true })
|
|
83
|
+
if (result.accounts.length === 0) result = await joey.connect()
|
|
84
|
+
|
|
85
|
+
const account = result.accounts[0]
|
|
86
|
+
if (account === undefined) {
|
|
87
|
+
throw new JoeyRpcError(
|
|
88
|
+
JOEY_ERROR_CODES.UNAUTHORIZED,
|
|
89
|
+
'Joey Wallet connected without sharing an account.',
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
return { joey, account }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/* ------------------------------------------------------------------ detection */
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Never rejects, and answers immediately when the provider is already present.
|
|
99
|
+
*
|
|
100
|
+
* Matches GemWallet's contract, including its 1-second budget for a provider
|
|
101
|
+
* that has not been injected yet.
|
|
102
|
+
*/
|
|
103
|
+
export async function isInstalled(): Promise<IsInstalledResponse> {
|
|
104
|
+
if (getJoey() !== null) return { result: { isInstalled: true } }
|
|
105
|
+
try {
|
|
106
|
+
await waitForJoey({ timeoutMs: DETECT_TIMEOUT_MS })
|
|
107
|
+
return { result: { isInstalled: true } }
|
|
108
|
+
} catch {
|
|
109
|
+
return { result: { isInstalled: false } }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* -------------------------------------------------------------------- account */
|
|
114
|
+
|
|
115
|
+
export async function getAddress(): Promise<GetAddressResponse> {
|
|
116
|
+
return await envelope(async () => {
|
|
117
|
+
const { account } = await requireAccount()
|
|
118
|
+
return { address: account.address }
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function getPublicKey(): Promise<GetPublicKeyResponse> {
|
|
123
|
+
return await envelope(async () => {
|
|
124
|
+
const { account } = await requireAccount()
|
|
125
|
+
if (account.publicKey === undefined) {
|
|
126
|
+
throw new JoeyRpcError(
|
|
127
|
+
JOEY_ERROR_CODES.UNAUTHORIZED,
|
|
128
|
+
'The selected Joey account is watch-only and has no public key.',
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
return { address: account.address, publicKey: account.publicKey }
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function getNetwork(): Promise<GetNetworkResponse> {
|
|
136
|
+
return await envelope(async () => {
|
|
137
|
+
const joey = await requireProvider()
|
|
138
|
+
const network = await joey.getNetwork()
|
|
139
|
+
return {
|
|
140
|
+
// Joey is XRPL-only. GemWallet's other value, XAHAU, is never returned.
|
|
141
|
+
chain: 'XRPL',
|
|
142
|
+
network: toGemNetwork(network),
|
|
143
|
+
websocket: toGemWebsocket(network),
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* -------------------------------------------------------------------- signing */
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Not implemented. Throws {@link GemWalletUnsupportedError} synchronously.
|
|
152
|
+
*
|
|
153
|
+
* Joey has no raw message-signing method: a bare signature over a string
|
|
154
|
+
* carries no domain, nonce or timestamp and is replayable against another site.
|
|
155
|
+
* Use `signIn()` from `@joeywallet/wallet-sdk`, which signs a CAIP-122 message bound
|
|
156
|
+
* to this origin.
|
|
157
|
+
*/
|
|
158
|
+
export function signMessage(_message: string, _isHex?: boolean): Promise<SignMessageResponse> {
|
|
159
|
+
return refuse('signMessage')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function sendPayment(
|
|
163
|
+
paymentPayload: SendPaymentRequest,
|
|
164
|
+
): Promise<SendPaymentResponse> {
|
|
165
|
+
return await envelope(async () => {
|
|
166
|
+
const { joey, account } = await requireAccount()
|
|
167
|
+
const result = await joey.signAndSubmitTransaction({
|
|
168
|
+
tx_json: toPaymentTransaction(paymentPayload, account.address),
|
|
169
|
+
})
|
|
170
|
+
return { hash: result.hash }
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function setTrustline(
|
|
175
|
+
payload: SetTrustlineRequest,
|
|
176
|
+
): Promise<SetTrustlineResponse> {
|
|
177
|
+
return await envelope(async () => {
|
|
178
|
+
const { joey, account } = await requireAccount()
|
|
179
|
+
const result = await joey.signAndSubmitTransaction({
|
|
180
|
+
tx_json: toTrustSetTransaction(payload, account.address),
|
|
181
|
+
})
|
|
182
|
+
return { hash: result.hash }
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function signTransaction(
|
|
187
|
+
payload: SignTransactionRequest,
|
|
188
|
+
): Promise<SignTransactionResponse> {
|
|
189
|
+
return await envelope(async () => {
|
|
190
|
+
const { joey } = await requireAccount()
|
|
191
|
+
const result = await joey.signTransaction({ tx_json: payload.transaction })
|
|
192
|
+
// GemWallet calls the signed blob `signature`. It is the full signed
|
|
193
|
+
// transaction, not the `TxnSignature` field.
|
|
194
|
+
return { signature: result.tx_blob }
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function submitTransaction(
|
|
199
|
+
payload: SubmitTransactionRequest,
|
|
200
|
+
): Promise<SubmitTransactionResponse> {
|
|
201
|
+
return await envelope(async () => {
|
|
202
|
+
const { joey } = await requireAccount()
|
|
203
|
+
const result = await joey.signAndSubmitTransaction({ tx_json: payload.transaction })
|
|
204
|
+
return { hash: result.hash }
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export async function submitBulkTransactions(
|
|
209
|
+
payload: SubmitBulkTransactionsRequest,
|
|
210
|
+
): Promise<SubmitBulkTransactionsResponse> {
|
|
211
|
+
return await envelope(async () => {
|
|
212
|
+
const { joey } = await requireAccount()
|
|
213
|
+
|
|
214
|
+
// GemWallet correlates results by an `ID` field carried inside each
|
|
215
|
+
// transaction. `ID` is not an XRPL field and would break serialisation, so
|
|
216
|
+
// it is stripped here and re-attached by position — Joey signs the batch in
|
|
217
|
+
// the order it was given.
|
|
218
|
+
const ids: Array<string | undefined> = []
|
|
219
|
+
const tx_list = payload.transactions.map((entry) => {
|
|
220
|
+
const { ID, ...tx_json } = entry
|
|
221
|
+
ids.push(ID)
|
|
222
|
+
return { tx_json }
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
const results = await joey.signTransactionBulk({ tx_list, submit: true })
|
|
226
|
+
|
|
227
|
+
const transactions: TransactionBulkResponse[] = ids.map((id, index) => {
|
|
228
|
+
const result = results[index]
|
|
229
|
+
return {
|
|
230
|
+
...(id === undefined ? {} : { id }),
|
|
231
|
+
// A resolved bulk request carries one entry per transaction, so every
|
|
232
|
+
// one of these is `true`. The guard stays because the alternative
|
|
233
|
+
// reading — an index with no entry silently becoming `accepted: true`
|
|
234
|
+
// with no hash — is the failure this shape exists to prevent.
|
|
235
|
+
//
|
|
236
|
+
// A batch that fails part way *rejects*, and `envelope` turns that into
|
|
237
|
+
// GemWallet's error response. The signed blobs and the failing index
|
|
238
|
+
// are on the error's `data` (`SignTransactionBulkFailure`); mapping
|
|
239
|
+
// them onto per-transaction `accepted` flags would be a better answer
|
|
240
|
+
// for a GemWallet dapp than an error, and is deliberately left as a
|
|
241
|
+
// change to this package's own contract rather than smuggled in with
|
|
242
|
+
// the wallet's.
|
|
243
|
+
accepted: result !== undefined,
|
|
244
|
+
...(result === undefined ? {} : { hash: result.hash }),
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
return { transactions }
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* --------------------------------------------------------------- unsupported */
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Not implemented. Throws {@link GemWalletUnsupportedError} synchronously.
|
|
256
|
+
*
|
|
257
|
+
* @see ./unsupported.ts for why.
|
|
258
|
+
*/
|
|
259
|
+
export function setRegularKey(_payload?: unknown): Promise<never> {
|
|
260
|
+
return refuse('setRegularKey')
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Not implemented. Throws {@link GemWalletUnsupportedError} synchronously. */
|
|
264
|
+
export function setHook(_payload?: unknown): Promise<never> {
|
|
265
|
+
return refuse('setHook')
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Not implemented. Throws {@link GemWalletUnsupportedError} synchronously. */
|
|
269
|
+
export function setAccount(_payload?: unknown): Promise<never> {
|
|
270
|
+
return refuse('setAccount')
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/* -------------------------------------------------------------------- events */
|
|
274
|
+
|
|
275
|
+
type NormalisedEvent<E extends GemEventType> = E extends 'login' | 'EVENT_LOGIN'
|
|
276
|
+
? 'login'
|
|
277
|
+
: E extends 'logout' | 'EVENT_LOGOUT'
|
|
278
|
+
? 'logout'
|
|
279
|
+
: E extends 'networkChanged' | 'EVENT_NETWORK_CHANGED'
|
|
280
|
+
? 'networkChanged'
|
|
281
|
+
: E extends 'walletChanged' | 'EVENT_WALLET_CHANGED'
|
|
282
|
+
? 'walletChanged'
|
|
283
|
+
: never
|
|
284
|
+
|
|
285
|
+
function normaliseEvent(eventType: GemEventType): keyof GemEventPayloadMap {
|
|
286
|
+
switch (eventType) {
|
|
287
|
+
case 'login':
|
|
288
|
+
case 'EVENT_LOGIN':
|
|
289
|
+
return 'login'
|
|
290
|
+
case 'logout':
|
|
291
|
+
case 'EVENT_LOGOUT':
|
|
292
|
+
return 'logout'
|
|
293
|
+
case 'networkChanged':
|
|
294
|
+
case 'EVENT_NETWORK_CHANGED':
|
|
295
|
+
return 'networkChanged'
|
|
296
|
+
case 'walletChanged':
|
|
297
|
+
case 'EVENT_WALLET_CHANGED':
|
|
298
|
+
return 'walletChanged'
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function toGemNetworkEvent(network: JoeyNetwork): EventNetworkChangedResponse {
|
|
303
|
+
return {
|
|
304
|
+
network: {
|
|
305
|
+
name: toGemNetwork(network),
|
|
306
|
+
server: toGemWebsocket(network),
|
|
307
|
+
description: toGemNetworkDescription(network),
|
|
308
|
+
},
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function attach(
|
|
313
|
+
joey: Joey,
|
|
314
|
+
event: keyof GemEventPayloadMap,
|
|
315
|
+
callback: (payload: never) => void,
|
|
316
|
+
): () => void {
|
|
317
|
+
const emit = (payload: unknown): void => {
|
|
318
|
+
;(callback as (value: unknown) => void)(payload)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
switch (event) {
|
|
322
|
+
case 'login':
|
|
323
|
+
return joey.on('connect', () => emit({ loggedIn: true } satisfies EventLoginResponse))
|
|
324
|
+
case 'logout':
|
|
325
|
+
return joey.on('disconnect', () => emit({ loggedIn: false } satisfies EventLogoutResponse))
|
|
326
|
+
case 'networkChanged':
|
|
327
|
+
return joey.on('networkChanged', (network) => {
|
|
328
|
+
if (network !== null) emit(toGemNetworkEvent(network))
|
|
329
|
+
})
|
|
330
|
+
case 'walletChanged':
|
|
331
|
+
return joey.on('accountsChanged', (accounts) =>
|
|
332
|
+
emit({
|
|
333
|
+
wallet: { publicAddress: accounts[0]?.address ?? '' },
|
|
334
|
+
} satisfies EventWalletChangedResponse),
|
|
335
|
+
)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Subscribe to a wallet event.
|
|
341
|
+
*
|
|
342
|
+
* `@gemwallet/api`'s `on()` returns `void`; this returns an unsubscribe
|
|
343
|
+
* function. That is a superset — existing call sites that ignore the return
|
|
344
|
+
* value are unaffected — and it is what a single-page app needs to avoid
|
|
345
|
+
* leaking a listener on every route change.
|
|
346
|
+
*/
|
|
347
|
+
export function on<E extends GemEventType>(
|
|
348
|
+
eventType: E,
|
|
349
|
+
callback: (payload: GemEventPayloadMap[NormalisedEvent<E>]) => void,
|
|
350
|
+
): () => void {
|
|
351
|
+
const event = normaliseEvent(eventType)
|
|
352
|
+
let detach: (() => void) | null = null
|
|
353
|
+
let cancelled = false
|
|
354
|
+
|
|
355
|
+
const bind = (joey: Joey): void => {
|
|
356
|
+
if (cancelled) return
|
|
357
|
+
detach = attach(joey, event, callback as (payload: never) => void)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const immediate = getJoey()
|
|
361
|
+
if (immediate !== null) bind(immediate)
|
|
362
|
+
else {
|
|
363
|
+
void waitForJoey({ timeoutMs: DETECT_TIMEOUT_MS })
|
|
364
|
+
.then(bind)
|
|
365
|
+
.catch(() => {
|
|
366
|
+
/* no wallet, nothing to listen to */
|
|
367
|
+
})
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return () => {
|
|
371
|
+
cancelled = true
|
|
372
|
+
detach?.()
|
|
373
|
+
detach = null
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/* -------------------------------------------------------------------- exports */
|
|
378
|
+
|
|
379
|
+
export {
|
|
380
|
+
GemWalletUnsupportedError,
|
|
381
|
+
UNSUPPORTED_METHODS,
|
|
382
|
+
type UnsupportedMethod,
|
|
383
|
+
} from './unsupported.js'
|
|
384
|
+
export { envelope, rejected, response } from './envelope.js'
|
|
385
|
+
export {
|
|
386
|
+
toGemNetwork,
|
|
387
|
+
toGemWebsocket,
|
|
388
|
+
toPaymentTransaction,
|
|
389
|
+
toTrustSetTransaction,
|
|
390
|
+
toXrplMemos,
|
|
391
|
+
toXrplSigners,
|
|
392
|
+
} from './convert.js'
|
|
393
|
+
export { DEFAULT_SUBMIT_TX_BULK_ON_ERROR } from './types.js'
|
|
394
|
+
export type * from './types.js'
|