@buyhatke-dev/wallet-kit
v0.2.0
Published
Headless, UI-agnostic wallet kit for EVM, Solana, Tron, Bitcoin, and Stellar (Reown AppKit + ethers), shared across Buyhatke apps.
Downloads
1,018
Readme
@buyhatke-dev/wallet-kit
Headless, UI-agnostic wallet kit shared across Buyhatke apps. The package owns wallet connection, normalized connection state, message and transaction signing, balance reads, and native/token sends across EVM, Solana, Tron, Bitcoin, and Stellar — desktop (Reown AppKit, injected TronLink, Stellar's own picker) and mobile deep links (Phantom, Solflare, TronLink). Tron has all three: an injected extension flow, a WalletConnect flow through AppKit, and a deep link — separate, and never auto-selected.
The kit is deliberately narrow: no UI, no framework dependency, no env
reads, and no backend of its own. Auth handshakes, API calls,
transaction-confirmation polling, product UI, and app store shapes stay in
the consuming app. Where a backend is genuinely unavoidable — mobile
deep-link channels, Tron mobile balance reads — the kit takes an injected
function rather than an endpoint: it decides only when to call and
what the result shape means, while your app owns the URL, auth, and API
shape. The only network I/O the kit performs itself is chain RPC — Solana
JSON-RPC, Stellar Horizon, an Esplora-compatible Bitcoin API, and TronGrid
(the WalletConnect Tron flow only) — each pointed wherever you want via
rpcUrls.
Documentation
| Doc | What's in it | | --- | --- | | docs/api.md | Full API reference: config, every method, error codes, unit helpers, subpaths, per-chain packages | | docs/architecture.md | Design principles, internals, and how each flow works end to end | | docs/chains-and-networks.md | Chain model, defaults, the ~700-chain EVM DB, network switching, name/symbol resolution, per-chain support matrix | | docs/integration-guide.md | Wiring the kit into an app: event bridging, error copy, bundler setup, migration sketches | | docs/mobile-deep-links.md | Mobile flows: the injected-channel contract, backend API shapes, the pre-mint auth pattern | | docs/limitations-and-roadmap.md | Honest current limitations and what's planned | | CHANGELOG.md | Version history |
Install
npm i @buyhatke-dev/wallet-kitCore chain SDKs arrive transitively: Reown AppKit and chain adapters, ethers, @solana/web3.js, and @stellar/stellar-sdk. Install only the optional Solana or Tron wallet adapters and Stellar picker your app enables. Drop duplicate direct copies so one version tree exists. Svelte is also an optional peer for the glue subpath:
npm i svelte # ./svelte subpath onlyStellar's SDKs are imported lazily at connect('stellar') and build into
their own chunk, so apps that never connect Stellar never load them —
details in api.md §8.1.
Quickstart
import { createWalletKit, isWalletKitError } from '@buyhatke-dev/wallet-kit';
const kit = createWalletKit({
projectId: '<walletconnect-project-id>',
metadata: { name: 'My App', url: 'https://myapp.com', icons: [] },
rpcUrls: { solana: 'https://your-solana-rpc.example.com' }, // always set this
});
await kit.connect('ethereum'); // Reown modal (any numeric EVM chain id also works)
await kit.connect('solana'); // Reown modal, Solana adapters
await kit.connect('tron'); // injected TronLink, no modal
await kit.connect('bitcoin'); // Reown modal; AppKit discovers bip122 wallets itself
await kit.connect('stellar'); // Stellar's own picker, no AppKit modal
// Tron's other browser flow: Reown's Tron modal, i.e. WalletConnect pairing,
// so a desktop user can connect the wallet on their phone. Separate from the
// line above — offer whichever you want, or both as two buttons.
await kit.connect('tron', { connector: 'appkit' });
// Rendering your own wallet list? Connect one wallet directly, no picker.
// Installed → injected provider with no modal; otherwise WalletConnect.
await kit.connect('ethereum', { wallet: 'metamask' });
// THE event stream — full snapshots on every change. NOTE: subscribe() does
// not emit on registration; seed with getConnection() first (the ./svelte
// helpers do this for you). Contract: docs/api.md → subscribe.
walletStore.set(kit.getConnection());
kit.subscribe((c) => walletStore.set(c));
const sig = await kit.signMessage('hello'); // every chain but Stellar
// Sign WITHOUT broadcasting — for challenge-response auth (Stellar SEP-10),
// whose wallets have no arbitrary-message signing and whose challenge must
// never be submitted. Also works on EVM and Solana.
const signedXdr = await kit.signTransaction(challengeXdr);
const balance = await kit.getBalance({ token: 'native' });
// { raw: '1000000000000000000', decimals: 18, formatted: '1', exists: true }
const { hash } = await kit.sendToken({
to: '<recipient>',
token: '<contract-or-mint>', // or 'native'
amount: '10.5', // human-readable; BigInt math internally
memo: 'order-42', // destination tag — see below
});Errors are typed everywhere:
try {
await kit.sendToken(params);
} catch (e) {
if (isWalletKitError(e)) console.log(e.code, e.message);
// USER_REJECTED · CONNECTION_CANCELLED · TIMEOUT · NO_WALLET · NO_ADDRESS
// UNSUPPORTED_CHAIN · UNSUPPORTED_CONNECTOR · WRONG_NETWORK
// INSUFFICIENT_BALANCE · INVALID_AMOUNT · RPC_ERROR · WALLET_ERROR
}Mobile deep links are explicit opt-in (never auto-detected) and require app-injected delivery channels — see docs/mobile-deep-links.md:
await kit.connect('phantom', 'mobile');
await kit.sendToken({ to, token, amount, decimals, meta: { orderId } });Behavior highlights
- Serialized lifecycle: rapid
prepare/connectcalls are latest-request-wins; superseded requests reject withCONNECTION_CANCELLEDinstead of racing. - EVM sends absorb the ethers "network changed" rejection — re-align to the intended chain and retry (max 3), recovering any embedded broadcast hash first so a retry can never double-send.
- Any EVM chain works even if not listed in your config — resolved on demand from Reown's ~700-network DB (names and symbols too).
- Decimal-safe money math end to end (string/BigInt, never floats);
formatUnits/parseUnitsare exported for app-side use. - Same
Balanceshape on every chain, with account-existence semantics (exists) for a missing Solana ATA, an unfunded Stellar account, or a missing trustline. memois safe to pass unconditionally — applied asMEMO_TEXTon Stellar, ignored where there's no memo concept. Exchange deposit addresses are shared; a deposit sent without its memo is credited to nobody.- Bitcoin balances read the Esplora
/address/:addrsummary, not/utxo— Esplora caps that endpoint at 500 outputs, which turns any reused address into a hard read failure.
Subpaths
@buyhatke-dev/wallet-kit— the kit.@buyhatke-dev/wallet-kit/solana—buildSplSendInstructions: client-side SPL transfer instructions without@solana/spl-token.@buyhatke-dev/wallet-kit/svelte—bindWalletKit/connectionStore.
Bitcoin and Stellar each build into their own chunk, so apps that never touch them never load them.
Scope & versioning
v1 targets EVM + Solana + Tron + Bitcoin + Stellar with a single active connection. TON, Ledger flows, and concurrent/aux wallets stay app-side for now — see limitations & roadmap. Every chain works with the kit alone — Stellar's SDKs ship as kit dependencies (§8.1). Rollout order: 0fiat (done) → echo-money → onramp, per the integration guide.
Publishing
Source: GitHub Buyhatke/wallet-kit (private). Package: npmjs under the
@buyhatke-dev org (public access). npm version patch && npm publish &&
git push --follow-tags. Prereleases go out off latest:
npm version prerelease && npm publish --tag next.
Dev
npm install
npm run typecheck
npm run lint
npm run build # tsup -> dist (ESM + d.ts)
npm test # vitest (unit, no network)
npm run test:live # WK_LIVE=1 — real endpoints, cross-checked against a second source