@usebutr/core 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,243 @@
1
+ # @usebutr/core
2
+
3
+ ## 3.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - [#203](https://github.com/pedroapfilho/usebutr/pull/203) [`1263f86`](https://github.com/pedroapfilho/usebutr/commit/1263f8666afd43b4f7dbf8df1763709c3edf09d9) Thanks [@pedroapfilho](https://github.com/pedroapfilho)! - Adapters now say what they can do by having the method: `capabilities` is gone, and an optional method (`sendTx`, `signMessage`, `getBalance`, `switchChain`, …) exists only when it works for that wallet. No adapter returns a placeholder balance or a receipt that stays pending anymore.
8
+
9
+ - `sendTx(tx, { account, chain })` replaces `sendTx` and `sendTxToChain`, and `signMessage`, `signTransaction` and `getBalance` take options objects. `chain` is a `ChainBase`. An adapter either routes the call to that chain, switches the wallet to it, or rejects; none ignores it.
10
+ - An `account` the wallet does not expose now rejects instead of signing with the first account. `chain.name` is the chain's name, never the wallet's.
11
+ - `getAccounts()` resolves the active account first. `getAccount` and `switchAccount` are removed.
12
+ - `getSigner()` resolves a tagged `WalletSigner` (`eip1193`, `wallet-standard`, `walletconnect`, `ledger-*`, …), so it can be narrowed with `switch (signer.kind)` instead of cast.
13
+ - `createWalletManager(config, { initialState })` owns discovery, hydration and persistence, and works without React. `config.sources` replaces the `discovery`, `connectors` and `createConnector` props. `fromAdapters` registers WalletConnect, Ledger and hand-built adapters, which now also appear in `useDiscoveredWallets()`.
14
+ - `WalletManagerProvider` takes `config` and `initialState`. `useSigner(wallet)` and `useBalance(wallet, { token, account })` take the wallet entry itself, and id-taking hooks read the active wallet only when the id is omitted (`null` reads none). The hooks are `useWalletManager`, `useWalletState`, `useConnect`, `useWallet`, `useSelectedWallet` (now typed per platform), `useAccounts`, `useConnectionStatus`, `useIsHydrated`, `useIsReconnecting`, `useDiscoveredWallets`, `useConnectedWallets`, the `…ByPlatform` groupings, `useBalance` and `useSigner`.
15
+ - `manager.connect()` and `useConnect().connectAsync()` resolve the `ConnectedWallet` and reject with a `ConnectionError`, now an `Error` subclass with a `kind` and a new `WalletNotFound` kind. `useConnect().connect()` never rejects: the failure lands in its `error`.
16
+ - `WalletPersistence` is `load()` / `save(state)`, and `createWalletStorage` replaces `new WalletStorage`. Storage keys are unchanged.
17
+ - Chain registries (`EVM_CHAINS`, `SVM_CHAINS`, …, `CHAINS_BY_PLATFORM`) move to `@usebutr/core`, and `BITCOIN_CHAINS` gains `testnet4`. `@usebutr/wallets` loads every platform's signer kinds, so an app that imports only it can narrow `signer.kind`.
18
+ - `@usebutr/react` requires React 19, which its `use()` call already needed.
19
+ - WalletConnect requests on Solana, Sui and Bitcoin now carry their chain, so they no longer fall through to the first namespace in a multi-namespace session. Solana takes `SVM_CHAINS` chains and maps them to the genesis-hash ids WalletConnect sessions use.
20
+ - Ledger's Solana `signTransaction` returns the full signed transaction, Sui signs the intent message, and Bitcoin `signMessage` returns a BIP-137 signature.
21
+ - `@usebutr/testing`'s fake adapter resolves a `signer` you pass instead of registering a test-only signer kind.
22
+ - `setAccount` selects an exposed account without changing any account's chain. Unknown accounts are ignored; chain changes come from the adapter.
23
+ - WalletConnect EVM events use the namespace in `session_event`, including empty account lists that end the connection. Local chain switches still update the manager.
24
+ - Failed persistence saves wait for every key write to finish before reporting failure, so delayed writes cannot overwrite a later disconnect.
25
+ - `@usebutr/svm/transaction` provides the shared legacy/v0 signing codec used by Ledger and WalletConnect, with consistent layout and signature validation.
26
+
27
+ See the migration guide at https://docs.usebutr.com/migration.
28
+
29
+ ### Patch Changes
30
+
31
+ - [#199](https://github.com/pedroapfilho/usebutr/pull/199) [`5d1f234`](https://github.com/pedroapfilho/usebutr/commit/5d1f23403263f7a1f8a48be061727972fe749b5f) Thanks [@pedroapfilho](https://github.com/pedroapfilho)! - Raise the zod dependency floor to 4.6.1.
32
+
33
+ ## 2.0.1
34
+
35
+ ### Patch Changes
36
+
37
+ - [#179](https://github.com/pedroapfilho/usebutr/pull/179) [`bc5b58c`](https://github.com/pedroapfilho/usebutr/commit/bc5b58cd87601be38dbb77a14e16eaaa36e8f012) Thanks [@pedroapfilho](https://github.com/pedroapfilho)! - Include package READMEs, MIT licenses, and documentation and issue links on npm.
38
+
39
+ ## 2.0.0
40
+
41
+ ### Major Changes
42
+
43
+ - 9b1caa2: Correctness pass across the storage, hydration and connector layers. Several of
44
+ these change observable behaviour; the ones worth knowing about before upgrading:
45
+
46
+ **Breaking**
47
+
48
+ - `SuiWallet.signTransaction` now returns `{ bytes, signature }` instead of
49
+ `Uint8Array`. Sui's `executeTransactionBlock` needs both, and each connector
50
+ previously returned a different half, so the old value could not be broadcast
51
+ and a consumer could not tell which half they held. SVM and Bitcoin keep their
52
+ single-`Uint8Array` shape.
53
+ - `@usebutr/svm`'s `sendTx` returns the signature base58-encoded, matching the
54
+ WalletConnect namespace, the Ledger app, Solana explorers and
55
+ `getSignatureStatuses`. It previously returned base64.
56
+ - `@usebutr/sui`'s feature input types drop the `string` arm from
57
+ `transaction`; wallets only ever accepted the `toJSON()` form. Strings and BCS
58
+ bytes are now wrapped for you, so `sendTx` accepts strictly more than before.
59
+
60
+ **Behaviour fixes**
61
+
62
+ - `WalletStorage.setPool` and `removePoolEntry` could self-deadlock on a corrupt
63
+ pool payload, leaving `connectWallet` unresolved and every later pool write
64
+ jammed for the page's life. Both are fixed, and `readWalletSnapshot` now shares
65
+ one codec with `WalletStorage` so the server and client decodes cannot drift.
66
+ - A seeded (SSR) entry whose silent reconnect failed used to stay in the pool
67
+ backed by a placeholder connector forever, so `useSigner`/`useBalance`
68
+ reported an error and `useConnectionStatus` reported `"reconnecting"`
69
+ permanently. Those entries are now dropped.
70
+ - `useSigner` and `useBalance` stay `idle` for a wallet that is still
71
+ reconnecting instead of surfacing a placeholder rejection as `status: "error"`.
72
+ - `useConnectionStatus` lets a live `"connecting"`/`"error"` take precedence over
73
+ the derived `"reconnecting"`, so an in-flight attempt and a connection error
74
+ are no longer hidden. New `useIsReconnecting(connectorId?)` answers the
75
+ per-wallet question directly.
76
+ - A background restore no longer writes the connect-attempt status or steals
77
+ `activeConnectorId`, and a superseded `CONNECT_FAILED` no longer overwrites the
78
+ current attempt.
79
+ - An externally-disconnected connector is now torn down, not just dropped from
80
+ the pool, so a cached adapter is not reused while still holding a session.
81
+ - EVM: a malformed provider response is treated as absent rather than `""`, which
82
+ previously produced chain `eip155:0`, zero-length signatures, empty transaction
83
+ hashes and a confident `0 ETH`. `switchChain` gained the same-chain
84
+ short-circuit `sendTxToChain` already had.
85
+ - WalletConnect: accounts carry their own chain instead of all being stamped with
86
+ the active one, so a multi-chain session no longer returns a wrong-chain
87
+ address. One pairing now covers every configured namespace, the pairing URI
88
+ survives a reconnect, and concurrent connects share one pairing.
89
+ - Bitcoin: the sats-connect (Xverse) adapter honours `silent`, so a reload no
90
+ longer triggers an unsolicited approval prompt, and exposes the payment address
91
+ as its single account instead of also presenting the ordinals address as
92
+ spendable.
93
+ - Polkadot: the injected adapter actually delivers events to subscribers,
94
+ `switchChain` notifies like its Wallet Standard sibling, and chain resolution
95
+ prefers the mainnets rather than whatever the wallet listed first.
96
+ - Ledger: the device session is committed atomically, so a locked device no
97
+ longer leaves an open transport that reports itself connected, and concurrent
98
+ connects open one transport.
99
+ - SVM: `sendTxToChain` submits to the chain you asked for instead of the
100
+ adapter's current one, and rejects a chain the wallet does not advertise.
101
+ - `@usebutr/svm`'s `switchChain` capability now counts only `solana:` chains, so
102
+ a multi-namespace wallet no longer advertises a method that always throws.
103
+
104
+ **Testing**
105
+
106
+ - `createFakePersistence` is now the real `WalletStorage` over memory drivers
107
+ rather than a parallel implementation, so it inherits upsert semantics,
108
+ validation and JSON round-tripping. Two divergences are fixed as a result:
109
+ `setPool` upserts rather than replaces, and `clearAll` leaves the
110
+ user-disconnected flag alone.
111
+ - `createFakeConnectedWallet` rejects being given both an `adapter` and explicit
112
+ `addresses`/`accounts`, a combination whose halves could disagree.
113
+
114
+ ## 1.1.0
115
+
116
+ ### Minor Changes
117
+
118
+ - 8ecaf89: Close five gaps that made multi-chain integration harder than it needed to be.
119
+
120
+ **Group wallets by platform without writing the loop yourself.**
121
+ `groupByPlatform(items, getPlatform)` in `@usebutr/core` buckets any list into
122
+ a `Map<ChainPlatform, T[]>` keyed in `CHAIN_PLATFORMS` order with empty
123
+ platforms omitted, and `@usebutr/react` adds
124
+ `useDiscoveredWalletsByPlatform()` / `useConnectedWalletsByPlatform()` on top.
125
+ A multi-chain wallet announces one adapter per platform, so every app hitting
126
+ more than one chain was writing this bucketing by hand.
127
+
128
+ **`autoDiscovery` takes an allowlist array, and says something when it's empty.**
129
+ `autoDiscovery(["evm", "svm"])` now works alongside the object form and reads
130
+ as the allowlist it is. An options value that enables no platforms logs a
131
+ warning instead of silently discovering nothing: a list built at runtime that
132
+ comes back empty was otherwise indistinguishable from "no wallets installed".
133
+ The bare `autoDiscovery()` everything-path is unchanged and stays silent.
134
+
135
+ **`createFakeConnectedWallet` in `@usebutr/testing`.** Builds the
136
+ `{ account, accounts, connector }` pool entry that UI tests actually render,
137
+ with accounts constructed through `buildAccount` so the `<chain>:<address>` id
138
+ format is never restated in a fixture. Defaults to the platform's mainnet chain
139
+ and a deterministic address; pass `adapter` to wrap a connector you already
140
+ built.
141
+
142
+ **The icon sanitization contract is now on the type.** `Connector.icon` is
143
+ already run through `sanitizeIcon` at discovery, so it is a trimmed non-empty
144
+ string or `undefined`, safe to hand to `next/image` with no second call and no
145
+ `icon !== ""` guard. `ConnectorMeta.icon` documents the opposite: it is
146
+ consumer-supplied and not sanitized.
147
+
148
+ **`createSignInFlow` in `@usebutr/core`.** Wraps the nonce, capability gate,
149
+ signature, base64 encoding, and verification handshake that every wallet-auth
150
+ app writes identically. Solana wallets advertising `solana:signIn` take the
151
+ SIWS path automatically. It deliberately does not define a message format: pass
152
+ `buildMessage` to match your backend. Also re-exports the SVM SIWS types
153
+ (`SolanaSignInFeature`, `SolanaSignInInput`, `SolanaSignInOutput`) from
154
+ `@usebutr/svm`, which were defined but not exported.
155
+
156
+ ## 1.0.0
157
+
158
+ ### Major Changes
159
+
160
+ - f0a5116: **Breaking:** the `platform` field is gone from the `PlatformDiscoverer` type in `@usebutr/core`, and from the `evmDiscoverer`, `svmDiscoverer`, `suiDiscoverer`, `bitcoinDiscoverer` and `polkadotDiscoverer` objects that implement it. Nothing read it: the aggregator keys discoverers by `ChainPlatform` in its own registry, so the field only restated the key.
161
+
162
+ Migration: read the platform from the `KNOWN_DISCOVERERS` key in `@usebutr/wallets` (`Object.entries(KNOWN_DISCOVERERS)`), or from `adapter.chainPlatform` on a discovered adapter. Custom `PlatformDiscoverer` implementations must drop the `platform` property; keeping it is now an excess-property error.
163
+
164
+ - f0a5116: **Breaking:** the deprecated `Wallet` type alias is removed from `@usebutr/core`.
165
+
166
+ Migration: import `WalletBase` instead (`type Wallet = WalletBase` was all the alias ever was), or the per-platform surface you actually mean: `EvmWallet`, `SvmWallet`, `SuiWallet`, `BitcoinWallet`, `PolkadotWallet`.
167
+
168
+ ### Minor Changes
169
+
170
+ - 7887cf0: Add `bytesToBase58` and `base58ToBytes` to the shared encoding module, alongside the existing hex and base64 helpers. Base58 was hand-rolled in eight places across the connector packages and the demo apps; a single tested implementation removes the drift surface on Solana addresses and signatures.
171
+
172
+ ### Patch Changes
173
+
174
+ - 7887cf0: Split the 497-line `types/wallet.ts` into focused modules (platform, account, capabilities, connector, wallet, manager) behind the same barrel. Pure file motion: every exported name and type is unchanged.
175
+
176
+ ## 0.5.0
177
+
178
+ ### Minor Changes
179
+
180
+ - 4467a5e: Compare the adapter instance in `walletEqual` instead of `connector.id`, so selectors re-render when hydration swaps a shadow adapter for the live one. Previously an SSR consumer stayed pinned to the placeholder whose methods throw `ShadowConnectorError`.
181
+
182
+ `isShadowAdapter` and `ShadowConnectorError` are now exported from the package root.
183
+
184
+ ## 0.4.2
185
+
186
+ ### Patch Changes
187
+
188
+ - c1309ee: Validate persisted wallet data and WalletConnect responses with shared Zod schemas.
189
+
190
+ ## 0.4.1
191
+
192
+ ### Patch Changes
193
+
194
+ - 937dfae: Bump runtime dependency floors (`@wallet-standard/app` 1.1.1, `@ledgerhq/*` latest minors, `@walletconnect/universal-provider` 2.23.10) and modernize public type declarations from method signatures to property function types (oxlint `method-signature-style`). Type-level only — no runtime behavior change.
195
+
196
+ ## 0.4.0
197
+
198
+ ### Minor Changes
199
+
200
+ - b5322ae: Add shared byte-encoding utilities (`bytesToHex`, `bytesToHexPrefixed`,
201
+ `hexToBytes`, `base64ToBytes`, `bytesToBase64`) and consolidate the per-connector
202
+ copies onto them. Behavior preserved: prefixed (`0x`) and bare hex are distinct
203
+ variants so each chain keeps its existing output.
204
+
205
+ ### Patch Changes
206
+
207
+ - d5f32c7: Fix Polkadot wallet connections failing to persist and reconnect on reload.
208
+ The storage validators' chain-platform allowlist was missing `polkadot`, so
209
+ Polkadot pool entries were rejected on write — and because the write rejects
210
+ the whole batch, a co-connected sibling (e.g. Solana) could be dropped too.
211
+ The allowlist is now derived from a single `CHAIN_PLATFORMS` source of truth
212
+ shared with the `ChainPlatform` type, so the runtime checks can't drift from
213
+ the type again.
214
+ - a46eecd: Ship unminified ESM so downstream bundlers (Vite/esbuild dep pre-bundling) process the package correctly; fixes a ReferenceError in consumer dev servers. The consuming app minifies once at its own build.
215
+
216
+ ## 0.3.0
217
+
218
+ ### Minor Changes
219
+
220
+ - 886ee1d: Add Polkadot/Substrate support. New `@usebutr/polkadot` package discovers wallets via injectedWeb3 (polkadot-js, Talisman, SubWallet, Nova, Enkrypt) with a Wallet Standard `polkadot:*` fallback. `ChainPlatform` widens to include `"polkadot"`; `autoDiscovery({ polkadot: true })` and `CHAINS_BY_PLATFORM` now cover it. Message signing works via the injected `signer.signRaw`; transaction signing is delegated to the consumer through `getSigner()` (e.g. polkadot-api), matching butr's no-RPC posture.
221
+
222
+ ## 0.2.2
223
+
224
+ ### Patch Changes
225
+
226
+ - db5d7e9: Fix intermittent wallet disconnect on page reload. Two storage-write bugs caused a remembered connection to be erased:
227
+
228
+ - An external `disconnected` event (EIP-1193 emits `accountsChanged: []`) fired on a simple wallet **auto-lock**, not just on permission revocation — and the store persisted the resulting empty pool, wiping the saved connection on every lock. The store now mirrors the disconnect into reducer state (so the UI hides the wallet) but leaves storage untouched; the next hydrate retries and self-heals once the wallet is unlocked. Explicit `disconnectWallet` still evicts via `removePoolEntry`.
229
+ - A transient `eth_accounts: []` during eager restore (a locked wallet) was treated as a permanent failure and the storage entry was deleted. Hydration now preserves storage on a failed restore and reports it as `dropped` for telemetry only, so a reload retries.
230
+
231
+ Additionally, `WalletStorage.setPool` is now **additive** and serialized through an internal mutation queue, so concurrent fire-and-forget writes can't interleave their read-modify-write phases and clobber each other's entries. `connectWallet` / `disconnectWallet` now await their storage writes so callers can trust persistence has landed on the next line.
232
+
233
+ ## 0.2.1
234
+
235
+ ### Patch Changes
236
+
237
+ - f846e77: Wallet-announced icons are trimmed of surrounding whitespace on ingestion. Some wallets ship data-URI icons with a leading newline, which strict consumers reject — Next.js's `<Image>` throws because `src` must not start with a control character. `@usebutr/core` exports a `sanitizeIcon` helper; the EIP-6963 and Wallet Standard adapters apply it, and an all-whitespace icon now resolves to `undefined` rather than a blank string.
238
+
239
+ ## 0.2.0
240
+
241
+ ### Minor Changes
242
+
243
+ - b77a477: Persisted pool entries now require the `accounts` field. Entries written by older versions without it are dropped on read with a warning.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pedro Filho
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,86 @@
1
+ # @usebutr/core
2
+
3
+ Core types, store, storage, and discovery seam for butr. No React, no protocols.
4
+
5
+ Part of [butr](https://www.usebutr.com), a multi-chain wallet discovery and
6
+ connection-state library. Your application owns the picker UI and chain client.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ npm install @usebutr/core zustand
12
+ ```
13
+
14
+ The wallet manager works outside React. Pair it with `@usebutr/wallets` (or one
15
+ platform's `discover*Adapters`) to find real wallets, or use `@usebutr/react`
16
+ for the provider and hooks.
17
+
18
+ ## Usage
19
+
20
+ ```ts
21
+ import { createWalletManager, fromAdapters } from "@usebutr/core";
22
+ import { autoDiscovery } from "@usebutr/wallets";
23
+
24
+ const manager = createWalletManager({
25
+ onConnectError: (error, connectorId) => console.warn(connectorId, error.kind),
26
+ sources: [autoDiscovery()],
27
+ });
28
+
29
+ // Creating a manager has no side effects; start() subscribes the sources,
30
+ // restores persisted connections once, and bridges wallet events.
31
+ const stop = manager.start();
32
+
33
+ const wallet = await manager.connect("io.metamask");
34
+
35
+ // A method exists only when it works for this wallet.
36
+ if (wallet.connector.signMessage) {
37
+ await wallet.connector.signMessage(new TextEncoder().encode("hello"), {
38
+ account: wallet.account,
39
+ });
40
+ }
41
+
42
+ manager.subscribe((state) => console.log(state.pool.size, state.activeConnectorId));
43
+ ```
44
+
45
+ - **Sources.** A `WalletSource` is `(onAdapter) => unsubscribe`, so every
46
+ `discover*Adapters` export goes into `sources` as-is. `fromAdapters(adapters)`
47
+ wraps an array or a promise of built adapters (WalletConnect, Ledger,
48
+ hand-rolled ones). The first adapter announced for an id wins.
49
+ - **State.** `getState()` holds the discovered `adapters`, the live `pool`,
50
+ the `selection` per platform, `activeConnectorId`, `reconnectingIds`, and the
51
+ latest attempt's `connectionStatus` / `connectionError`.
52
+ - **Actions.** `connect`, `disconnect`, `disconnectAll`, `requestAccounts`,
53
+ `setAccount`, `setActive`, `setSelection`, `clearConnectionError`. `connect`
54
+ resolves the `ConnectedWallet` and rejects with a `ConnectionError` whose
55
+ `kind` is `UserRejected`, `RequestPending`, `WalletLocked`, `ChainMismatch`,
56
+ `NotConnected`, `Timeout`, `WalletNotFound` or `Unknown`.
57
+ - **Signers.** `connector.getSigner()` resolves a tagged `WalletSigner`; narrow
58
+ it with `switch (signer.kind)`. Each transport package registers its kind.
59
+ - **Chains and accounts.** Every registry lives here (`EVM_CHAINS`,
60
+ `SVM_CHAINS`, `SUI_CHAINS`, `BITCOIN_CHAINS`, `POLKADOT_CHAINS`, their
61
+ `*_LIST`, `CHAINS_BY_PLATFORM`). Adapters build chains with
62
+ `resolveChain(id, list)` and accounts with `buildAccount(address, chain)`.
63
+
64
+ ## Persistence
65
+
66
+ Connections persist to localStorage and sessionStorage by default, written
67
+ after every change once start-up hydration has read what was there. Swap the
68
+ drivers with `createWalletStorage({ persistent, session })` (for example
69
+ `createCookieStorageDriver()` so a server can read them), or pass any
70
+ `WalletPersistence`: an object with `load()` and `save(state)`.
71
+
72
+ To render connections on the server, read the cookies with
73
+ `readWalletSnapshot(cookies)` and pass the snapshot as
74
+ `createWalletManager(config, { initialState })`. Seeded wallets render at
75
+ once and stay in `reconnectingIds` until their silent reconnect lands.
76
+
77
+ ## Documentation
78
+
79
+ - [Package reference](https://docs.usebutr.com/api/core)
80
+ - [Getting started](https://docs.usebutr.com/getting-started/quickstart)
81
+ - [Examples and source](https://github.com/pedroapfilho/usebutr)
82
+ - [Report an issue](https://github.com/pedroapfilho/usebutr/issues)
83
+
84
+ ## License
85
+
86
+ [MIT](./LICENSE), copyright 2026 Pedro Filho.