npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@joeywallet/wallet-sdk

v0.4.0

Published

Dependency-free TypeScript SDK for the Joey Wallet browser extension (XLS-72d / Wallet Standard).

Downloads

554

Readme

@joeywallet/wallet-sdk

TypeScript SDK for the Joey Wallet browser extension, a self-custody XRP Ledger wallet.

  • Zero runtime dependencies. Nothing is pulled into your bundle but this package.
  • Synchronous detection. isJoeyAvailable() and getJoey() answer without a message round trip, so a cold extension worker never reads as "not installed".
  • Promises, not envelopes. Every method resolves a value or throws JoeyRpcError.
  • Standards-first. Joey registers under XLS-72d / Wallet Standard with chains xrpl:0 / xrpl:1 / xrpl:2 and uses EIP-1193 error codes verbatim. This SDK is a convenience layer over the injected provider, not a replacement for either surface.
npm install @joeywallet/wallet-sdk
# pnpm add @joeywallet/wallet-sdk
# yarn add @joeywallet/wallet-sdk

Requires an ESM bundler. react is an optional peer dependency, needed only for @joeywallet/wallet-sdk/react.


Detect

import { getJoey, isJoeyAvailable, waitForJoey } from '@joeywallet/wallet-sdk'

// Synchronous. No message is sent to the extension.
if (isJoeyAvailable()) {
  const joey = getJoey()! // never null when isJoeyAvailable() is true
}

getJoey() returns Joey | null synchronously, reading window.joey and window.xrpl.joey. It is also safe to await — awaiting a non-promise costs one microtask.

Your bundle can execute before the extension has finished injecting its provider. When that happens, wait for the announcement instead of polling:

try {
  const joey = await waitForJoey({ timeoutMs: 3000 })
} catch {
  // No Joey on this page. Show your "install" call to action.
}

waitForJoey() resolves immediately when the provider is already present, so it is safe to use as your only detection call. It settles on the provider's own CAIP-294 wallet_announce and Wallet Standard register-wallet events — and, because a wallet that installed before your bundle ran has already dispatched both, it also dispatches the app-side prompts (wallet_prompt and wallet-standard:app-ready) so that wallet announces itself again.

Why detection never awaits the wallet

Wallet aggregators give each wallet roughly one second to prove it exists. Joey runs its logic in a Manifest V3 background service worker, which Chrome terminates after 30 seconds idle; a detection scheme that asks the worker "are you there?" pays a cold start on the first call and loses that race intermittently. So detection only ever reads page globals.


Connect

import { getJoey, isUserRejection } from '@joeywallet/wallet-sdk'

const joey = getJoey()
if (joey === null) throw new Error('Joey is not installed')

const { accounts, chain } = await joey.connect({
  // Shown on the approval screen. Without them the user is asked to trust an
  // origin and nothing else. `icon` must be `https:` or `data:image/`.
  name: 'Example Exchange',
  icon: 'https://example.com/icon.png',
})
const account = accounts[0]

console.log(account?.address) // rLNaPoKeeBjZe2qs6x52yVPZpZ8td4dc6w
console.log(chain) // 'xrpl:0'

To reconnect on page load without opening an approval window, pass silent. It resolves with an empty accounts array when the origin was never authorised, rather than throwing — so it reveals nothing about whether a wallet is installed, locked, or in use:

const { accounts } = await joey.connect({ silent: true })
if (accounts.length > 0) setAccount(accounts[0])

joey.disconnect() revokes this origin's access. joey.accounts and joey.isConnected() read the current grant synchronously, with no round trip.


Send a payment

signAndSubmitTransaction signs and submits in one approval. Amounts in XRP are drops (1 XRP = 1,000,000 drops) as a string.

const result = await joey.signAndSubmitTransaction({
  tx_json: {
    TransactionType: 'Payment',
    Account: account.address,
    Destination: 'rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe',
    Amount: '1000000', // 1 XRP
    DestinationTag: 42,
  },
})

console.log(result.hash, result.engine_result) // 'A1B2…', 'tesSUCCESS'

An issued currency uses the object form:

Amount: { currency: 'USD', issuer: 'rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B', value: '25' }

Joey fills Fee, Sequence and LastLedgerSequence for you. Pass autofill: false if you have already set them.

To get the signed blob back without submitting it, use signTransaction:

const { tx_blob, hash, tx_json } = await joey.signTransaction({ tx_json: payment })

tx_json in the result is the transaction as signed — decoded back out of tx_blob, not echoed from what you sent. It carries the Fee, Sequence and LastLedgerSequence the wallet filled in, the SigningPubKey and TxnSignature it produced, and any normalisation the serialiser applied. That is what you want to log; if it does not say what you expected, the bytes are what it says.

Choosing the account, and the chain

A user may grant your origin more than one address. When they have, sign with the one your transaction names — otherwise the wallet uses the first address it granted you and the signature does not match the transaction's Account:

await joey.signTransaction({
  account: account.address,
  chain: 'xrpl:0',
  tx_json: payment,
})

chain is a guard, not a request. If the wallet is on a different network the call is refused with CHAIN_DISCONNECTED (4901) rather than signed. There is deliberately no switchNetwork: a page-driven, wallet-wide network switch is a phishing surface, so the user changes network in the wallet and your dapp is told through the networkChanged event.

How long a call can take

import { APPROVAL_TIMEOUT_MS, REQUEST_TIMEOUT_MS } from '@joeywallet/wallet-sdk'

Anything that needs an approval — every signing method, signIn, and a non-silent connect — is waiting on a person and may legitimately be pending for APPROVAL_TIMEOUT_MS (300,000 ms), plus a few seconds of the page's own backstop. Everything else answers within REQUEST_TIMEOUT_MS (30,000 ms) or the wallet is wedged. Do not put a thirty-second timeout around signTransaction: you will cancel on a user who was still reading.

Using xrpl.js types

This package does not import from xrpl — a published .d.ts that did would fail to resolve for dapps that do not depend on xrpl.js. Every signing method is generic instead, so if you do have xrpl.js, annotate your value and keep full checking:

import type { Payment } from 'xrpl'

const payment: Payment = { TransactionType: 'Payment', /* … */ }
await joey.signAndSubmitTransaction({ tx_json: payment })

Multisign and bulk signing

// One signature toward a multisigned transaction. `tx_signer` is the address
// signing; the transaction's own Account stays the multisigned account. It has
// to be an address the user granted you — the wallet refuses with 4100 rather
// than substituting one of its own.
const { tx_blob, hash } = await joey.signTransactionFor({
  tx_signer: account.address,
  tx_json: { TransactionType: 'Payment', Account: 'rMultisigAccount', /* … */ },
})

signTransactionFor does not autofill, and autofill: true does not make it. The flag exists on the type because the signing methods share a parameter shape; on this one it is ignored. Your tx_json must already carry Fee, Sequence and LastLedgerSequence, or you get a real signature over a transaction rippled will refuse — with no error until you submit it.

The reason is that a multisign signature is one of several over identical bytes, and all three fields are inside those bytes. Two signers approving a few seconds apart would read two different LastLedgerSequence values, and the assembled transaction would validate at most one of their signatures. The Fee is worse: the rule is base_fee × (1 + signatures), a wallet contributes one signature and cannot know how many others the signer list requires, and raising the Fee afterwards discards every signature already collected. The coordinator assembling the transaction is the only party that can choose these — that is you.

import { MAX_BULK_TRANSACTIONS } from '@joeywallet/wallet-sdk'

// Up to MAX_BULK_TRANSACTIONS (32), one approval, signed in order.
const results = await joey.signTransactionBulk({
  tx_list: [{ tx_json: trustSet }, { tx_json: payment }],
  submit: true,
})

for (const entry of results) console.log(entry.hash)

submit has no default and the type requires it, because signing and submitting are not interchangeable and a dapp that guesses wrong either double-spends or never spends. true signs every transaction and then broadcasts them strictly in order; false signs them and broadcasts nothing, handing the blobs back for you to submit.

One approval covers the whole batch and one password unlocks it — the user pages through every transaction and then decides once. They cannot approve some and refuse others, on this wallet or on Joey mobile: it is one queue entry, one approve, one reject. On a Ledger it is still one password, but N confirmations on the device, one per transaction, which is what a hardware wallet is for.

The wallet numbers the batch up front: one reading of the ledger, Sequence counting up from the account's next number, and 15 more ledgers of validity per position so the transaction submitted last is not the one with the least time left. A Sequence you set yourself is kept as it is and is not renumbered.

Every entry also gets 5 extra ledgers per entry beyond the first, shared across the batch. That one is not about position: with submit: true the whole batch is signed before any of it is broadcast, so the clock starts at that single reading and entry 0 is the one with the least time and the longest wait. On a Ledger that wait is one physical confirmation per transaction. So entry i of an n-entry batch is signed with

LastLedgerSequence = ledger_current + 20 + 5 * (n - 1) + 15 * i

How long the wallet watches, and what unknown means at the tail

The wallet follows each submission until its LastLedgerSequence passes — the ledger index is the only thing entitled to say a transaction failed — but it stops watching after 10 minutes on any one entry, because this runs in an MV3 service worker with an approval window open.

Those two numbers meet in long batches. Ten minutes is about 150 ledgers at a four-second close, while a full 32-entry batch gives entry 0 a window of 20 + 5·31 = 175 ledgers and entry 31 one of 640 — roughly 43 minutes. So in a batch that size, the wallet's watch is what ends first, for every entry.

When it does, that entry comes back status: 'unknown', and so does every entry behind it that was never broadcast. This is not a failure and must not be retried as one. The transaction was submitted, the wallet stopped watching, and it may still be validated — resolve it by hash and only then decide. The entries behind an unknown are unknown for the same reason: nothing about their sequence numbers can be settled until that one is.

The widths are deliberate in this direction. A shorter window would expire as tefMAX_LEDGER, which is definite and irrecoverable and would strand the whole tail with it; stopping the watch gives up information, not money.

When a batch fails part way

It rejects, and the error's data is a SignTransactionBulkFailure:

import type { JoeyRpcError, SignTransactionBulkFailure } from '@joeywallet/wallet-sdk'

try {
  await joey.signTransactionBulk({ tx_list, submit: true })
} catch (e) {
  const data = (e as JoeyRpcError).data as SignTransactionBulkFailure | undefined
  if (!data) throw e

  for (const entry of data.results) {
    switch (entry.status) {
      case 'submitted': break                 // on the ledger; entry.hash is real
      case 'failed':    break                 // definite; entry.engine_result says why
      case 'unknown':   break                 // may yet be validated — resolve by hash
      case 'signed':    break                 // never broadcast; submit it as it stands
      case 'stranded':  break                 // never broadcast and now dead — re-sign
    }
  }
}

Every blob comes back, including the ones that were never broadcast, so you resume from failedIndex instead of asking the user to approve the batch again.

The signed / stranded split is the one thing you cannot work out for yourself. The wallet reads the account's actual sequence once the batch stops and walks the entries it never broadcast in the order you would resubmit them: an entry is signed when the replay protection it holds is what the ledger is at, and stranded when it is not — either already consumed, or behind a gap this batch will never fill. In the ordinary case that follows the failing code, because a tec* reached a ledger and consumed its sequence number while a tem*/tef*/tel* consumed nothing. It does not follow the code once you set your own Sequence values or use tickets, which is why it is computed rather than inferred: a ticketed entry holds no sequence at all, so a failure ahead of it leaves it perfectly submittable and it comes back signed; and two entries you numbered identically can never both be signed. unknown settles neither question and must not be treated as failed: the transaction may still be validated, so resubmitting it is not safe.

Submit the signed entries in the order they appear. They are a chain — each one's number only becomes current once the one before it has applied.

failedIndex is zero-based and every earlier transaction succeeded, and that meaning is identical on Joey mobile over WalletConnect. The record around it is not. Mobile rejects with data as a JSON string — WalletConnect types error data as one — holding {failedIndex, signedTxs}, where signedTxs is an array of bare tx_json carrying no status, and its message is the bare engine token (tecPATH_PARTIAL) rather than a sentence. Here data is an object holding {failedIndex, results}. A dapp integrating both wallets branches on the container and on the array's name; failedIndex is what transfers unchanged.

The error's message counts the same way the field does:

transaction at index 2 of 5 did not succeed: tecUNFUNDED_PAYMENT

Index 2 is the third transaction, and the sentence says "at index" so it cannot be read as anything else. Do not parse it — failedIndex and results[i].status carry the same facts as data — but when you print it beside the field, the two numbers agree.

The wallet's own approval window says the same thing to the user counting from one ("Transaction 3 of 5"), because there is no zero-based field beside it on that screen. Both are unambiguous; only the pair "transaction 2 of 5" next to failedIndex: 2 was not.

signTransactionBulk is not XLS-56 Batch. They are different things and Joey keeps them apart deliberately. A Batch is a single transaction carrying others inside RawTransactions, committed atomically on-ledger; Joey refuses to sign one for a website, because its approval screen renders the outer transaction and a user cannot consent to inner ones they were never shown. signTransactionBulk is the opposite arrangement: ordinary, separate transactions, each rendered on its own page of one approval, each signed on its own — and with no atomicity at all. If transaction 3 fails, 1 and 2 have still happened.


Sign in

signIn authenticates a user without a transaction. For a software account it signs a CAIP-122 message under a non-transaction domain separator, so the signature is cryptographically incapable of being replayed as a transaction signature.

It has two result shapes, and you must narrow before reading either. Which one you get is the wallet’s decision, not yours: it depends on the account the user picks, and they pick it inside the wallet after your call has been made.

import { isChallengeSignIn } from '@joeywallet/wallet-sdk'

const result = await joey.signIn({
  statement: 'Sign in to Example Exchange',
  // nonce defaults to one the wallet generates; supply your own if your
  // backend issues it.
})

if (isChallengeSignIn(result)) {
  // A hardware account. Verify the signed transaction.
  await fetch('/api/session/tx', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ address: result.address, signedTx: result.signedTx }),
  })
} else {
  await fetch('/api/session', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({
      address: result.address,
      publicKey: result.publicKey,
      message: result.message,
      signature: result.signature,
    }),
  })
}

Verify the CAIP-122 signature server-side against result.message and check the nonce, the domain and the issue time inside that message before trusting it.

Hardware accounts sign a transaction instead

A Ledger cannot produce the result above. The XRP app signs transactions and has no message primitive at all, so there is no command to ask it for a CAIP-122 signature. Rather than refuse the sign-in, the wallet proves ownership by having the device sign a canonical 1-drop Payment and hands you the signed transaction as signedTx.

That transaction is built entirely by the wallet and is unsubmittable twice over: Sequence is 0, which is never valid for a funded account, and the destination is the signing account itself, which rippled refuses as temREDUNDANT. Your challenge travels in a memo as {"wallet":"joey","challenge":"<nonce>"}, hex-encoded.

signedTx is JSON.stringify of the bare decoded transaction — not wrapped in { tx_json: … } — so JSON.parse it and use the result as a transaction object. To verify one, re-encode it and check the signature, then check the signer is the address you are about to trust:

import { encode } from 'ripple-binary-codec'
import { verifySignature } from 'ripple-keypairs' // or your verifier of choice

const tx = JSON.parse(signedTx)
const ok = verifySignature(encode(tx)) // signatureValid, signedBy
// Bind the session to the address that actually signed, never to one the
// client claimed: without that check a valid signature made with any key can
// mint a session for any account.

This is byte-identical to what the Joey mobile wallet puts in a WalletConnect session's xrpl_signin_v1_signed_tx, so a backend that already verifies mobile Joey sign-ins verifies these unchanged.

Narrow with isChallengeSignIn, which tests for the signedTx field rather than for mode. Wallets older than this shape send no mode at all, so a check written the other way round misroutes every one of them.

If you skip the narrowing, a verifier written for the CAIP-122 shape reads publicKey, message and signature as undefined, posts them, and reports "signature did not verify" — for a signature the user made correctly on their device, seconds earlier. The failure is silent on the wallet side and looks like a rejected login on yours.

Pass resources to name the scope you are asking for. It is shown on the approval screen and written into the message's Resources: section, so it is covered by the signature — rebuild the message with the same list, in the same order, when you verify:

const result = await joey.signIn({
  statement: 'Sign in to Example',
  resources: ['https://example.com/terms', 'https://example.com/api'],
})

There is no Xaman-compatible mode: 'xaman'. It signed the {TransactionType:'SignIn'} pseudo-transaction, and the property that made that safe — rippled has no such transaction type, so the blob is unsubmittable — is also why it could never be produced: ripple-binary-codec has no SignIn either, so serialising one throws. The wallet answers -32602 naming the mode rather than quietly signing a CAIP-122 message in its place, so an existing Xaman integration gets one clear error instead of a result with no tx_blob in it.

There is no signMessage. A bare signature over an arbitrary string carries no domain, nonce or timestamp, which makes it replayable against another site; signIn is the primitive to use instead.


Events

const off = joey.on('accountsChanged', (accounts) => {
  // An empty array means the user revoked this origin.
  setAccount(accounts[0] ?? null)
})

joey.on('networkChanged', (network) => setNetwork(network))
joey.on('disconnect', () => setAccount(null))

off() // or joey.off('accountsChanged', listener)

| Event | Payload | | ----------------- | ---------------------------------------- | | connect | { accounts: JoeyAccount[], chain } — on a new grant only; re-connecting an already-authorised origin emits nothing | | disconnect | { reason?: string } | | accountsChanged | JoeyAccount[] | | networkChanged | JoeyNetwork \| null |

Payloads are normalised by this SDK: the raw provider forwards whatever the wallet sent, which may be bare address strings or { accounts: [...] }.

Registering the same function twice gives you two subscriptions, and each needs its own off() — the same rule addEventListener follows.


Errors

Every method rejects with JoeyRpcError { code, message, data? }. Codes are EIP-1193 numbers.

| Code | Meaning | | ------- | --------------------------------------------------------- | | 4001 | The user rejected the request | | 4100 | This origin is not authorised for that method | | 4200 | The wallet does not support that method | | 4300 | The wallet is locked | | 4900 | Not installed, or the provider is not connected | | 4901 | The wallet is on a different chain than the one you asked for | | 4902 | Not an XRPL chain id at all | | -32005| Too many requests. Back off; do not retry in a loop | | -32600| The request was not well formed | | -32602| Malformed arguments | | -32603| Anything the SDK could not classify |

-32005 is reachable two ways and a dapp that ignores it looks broken in both: the content script caps concurrent in-flight requests, and the wallet blocks an origin whose user has rejected three requests in a row.

import { JOEY_ERROR_CODES, JoeyRpcError, isUserRejection } from '@joeywallet/wallet-sdk'

try {
  await joey.signAndSubmitTransaction({ tx_json })
} catch (error) {
  if (isUserRejection(error)) {
    return // the user said no; not an error worth reporting
  }
  if (error instanceof JoeyRpcError && error.code === JOEY_ERROR_CODES.LOCKED) {
    return showBanner('Unlock Joey and try again.')
  }
  reportToSentry(error)
}

Prefer isUserRejection(error) over error.code === 4001: it also matches providers and adapter shims that only carry the word in the message. Note that an unconnected origin is told 4001 rather than 4300 even when the wallet is in fact locked — the lock state is deliberately not readable by a site the user has not connected.


React

npm install @joeywallet/wallet-sdk react
import { JoeyProvider } from '@joeywallet/wallet-sdk/react'

export function App() {
  return (
    <JoeyProvider autoConnect>
      <Wallet />
    </JoeyProvider>
  )
}

autoConnect reconnects silently: it returns only accounts the user has already granted this origin and never opens an approval window on page load.

useJoey() gives you the connection state:

import { useJoey } from '@joeywallet/wallet-sdk/react'

function Wallet() {
  const { isReady, isAvailable, account, network, connect, disconnect } = useJoey()

  if (!isReady) return null
  if (!isAvailable) return <a href="https://joeywallet.xyz">Install Joey</a>
  if (account === null) return <button onClick={() => void connect()}>Connect</button>

  return (
    <div>
      {/* network.name is 'Mainnet', 'Testnet' or 'Devnet'. */}
      {account} on {network?.name}
      <button onClick={() => void disconnect()}>Disconnect</button>
    </div>
  )
}

The mutation hooks are shaped like react-query mutations, without depending on react-query:

import { useSignAndSubmit } from '@joeywallet/wallet-sdk/react'

function Pay({ from }: { from: string }) {
  const { mutate, isPending, error, data, reset } = useSignAndSubmit({
    onSuccess: (result) => console.log(result.hash),
  })

  return (
    <>
      <button
        disabled={isPending}
        onClick={() =>
          mutate({
            tx_json: {
              TransactionType: 'Payment',
              Account: from,
              Destination: 'rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe',
              Amount: '1000000',
            },
          })
        }
      >
        {isPending ? 'Approve in Joey…' : 'Send 1 XRP'}
      </button>
      {error && <p onClick={reset}>{error.message}</p>}
      {data && <p>{data.hash}</p>}
    </>
  )
}

mutate never rejects — read error. mutateAsync returns the promise if you want to await it. A response from a superseded call is dropped, so a double-click cannot render the first answer next to the second spinner.

Available hooks: useConnect, useDisconnect, useSignTransaction, useSignAndSubmit, useSignTransactionFor, useSignTransactionBulk, useSignIn, and useJoeyMutation for anything else.


Without a framework

<button id="connect"></button>
<script type="module">
  import { bindConnectButton, createJoeySession } from '@joeywallet/wallet-sdk/vanilla'

  const session = createJoeySession() // detects, and silently reconnects
  bindConnectButton(document.getElementById('connect'), session, {
    installUrl: 'https://joeywallet.xyz',
    onError: (error) => console.warn(error.message),
  })

  session.subscribe((state) => {
    console.log(state.account, state.network?.name)
  })
</script>

session.getState() is synchronous; session.subscribe(fn) calls fn immediately with the current state and again on every change, and returns an unsubscribe function. Call session.destroy() when you tear the page down.


Typing window.joey

This package does not declare window.joey globally, because doing so collides with other XRPL wallet SDKs that declare window.xrpl. Declare it yourself if you want it:

import type { JoeyInjectedProvider } from '@joeywallet/wallet-sdk'

declare global {
  interface Window {
    joey?: JoeyInjectedProvider
  }
}

Migrating from GemWallet

Use @joeywallet/gemwallet-compat, which exports GemWallet's exact function names and { type, result } envelope over this SDK. Migration is a one-line import change.


Reference

Joey

| Member | Returns | | ------ | ------- | | accounts | granted addresses, synchronously | | chain | JoeyChain \| null, synchronously | | isConnected() | boolean, synchronously | | connect(params?) | { accounts, chain, networkId } | | disconnect() | void | | getAccounts() | string[] — [] for an unconnected origin, never an error | | getNetwork() | { chain, networkId, name } — name is 'Mainnet' / 'Testnet' / 'Devnet' | | signTransaction({ tx_json, account?, chain?, autofill? }) | { tx_json, tx_blob, hash } | | signAndSubmitTransaction(…same…) | the above plus engine_result, engine_result_message | | signTransactionFor({ tx_signer, tx_json, account?, chain?, autofill? }) | { tx_json, tx_blob, hash } | | signTransactionBulk({ tx_list, submit, account?, chain?, autofill? }) | SignAndSubmitTransactionResult[] — engine_result only when submit: true | | signIn(params?) | { address, publicKey, signature, message?, tx_blob? } | | request({ method, params }) | escape hatch for newer wallet methods | | on(event, listener) / off(event, listener) | subscription |

Chains

| Chain | Network | NetworkID | | -------- | ------- | ----------- | | xrpl:0 | Mainnet | 0 | | xrpl:1 | Testnet | 1 | | xrpl:2 | Devnet | 2 |

Transactions Joey will not sign for a dapp

JOEY_DAPP_FORBIDDEN_TRANSACTION_TYPES is now empty, and no longer tells you what will be rejected:

import { JOEY_DAPP_FORBIDDEN_TRANSACTION_TYPES } from '@joeywallet/wallet-sdk'
// readonly string[] — []

It was always a published mirror rather than a gate: no SDK method reads it, and signTransaction, signTransactionFor and signTransactionBulk hand your tx_json to the provider unread. Emptying it removed the advance notice, not an enforcement point. A passing check against this constant is not permission to sign.

The wallet keeps its own rules and applies them at approval time, at every nesting level. At the time of writing it still refuses:

| Type | Why | | ---- | --- | | SetRegularKey | Grants permanent signing authority over the account. | | SignerListSet | The same, by multisign. | | DelegateSet | The same again (XLS-75). With Payment in its Permissions, a standing licence to drain every balance. | | AccountDelete | Irreversible. | | SetHook | Installs code that runs on every future transaction. | | Batch | Carries other transactions inside RawTransactions, which the approval screen cannot render — see the note under bulk signing. |

Plus two rules that were never expressible as a type name:

  • AccountSet is conditionally refused. It is permitted for routine flags (asfDefaultRipple and the rest) and refused when it sets or clears one that changes who controls the account — asfDisableMaster, asfRequireAuth, asfNoFreeze, asfDisallowXRP and that family. A SetRegularKey plus a disable-master AccountSet is permanent, unrevokable takeover.
  • Pseudo-transactions are refused. EnableAmendment, SetFee and UNLModify are written into a ledger by consensus; no account signs one, and rippled rejects one submitted over the network.

None of these is distinguishable from an ordinary transaction in a confirmation dialog someone is skimming, which is why the wallet refuses them rather than surfacing them for approval. Users perform them from the Joey UI, where the wording can be as blunt as it needs to be.

Because the constant no longer lists them, expect these to arrive as a 4100 from the approval queue and handle them there.