{"_id":"@butternetwork/wdk-protocol-swidge-butter","_rev":"2-6de206e336c12b7aae462a77745ade50","name":"@butternetwork/wdk-protocol-swidge-butter","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@butternetwork/wdk-protocol-swidge-butter","version":"0.1.0","keywords":["wdk","swidge","butter","bridge","swap"],"license":"Apache-2.0","_id":"@butternetwork/wdk-protocol-swidge-butter@0.1.0","maintainers":[{"name":"philllau","email":"philllllau@gmail.com"},{"name":"evan.ad","email":"evan.anderson.sun@gmail.com"},{"name":"pandarr","email":"panda007.2021@gmail.com"}],"homepage":"https://github.com/butternetwork/wdk-protocol-swidge-butter#readme","bugs":{"url":"https://github.com/butternetwork/wdk-protocol-swidge-butter/issues"},"dist":{"shasum":"98c8aadc87f432c4635d8da90894c4d3b7cf93d3","tarball":"https://registry.npmjs.org/@butternetwork/wdk-protocol-swidge-butter/-/wdk-protocol-swidge-butter-0.1.0.tgz","fileCount":84,"integrity":"sha512-hprMOgTH1madEy2Ip0nyVKp9G5HREmJXQ80QGo50iH0Se2YJy37FS4Xap9MYVaQG7cNEwRAHsl1/IQXOtQljew==","signatures":[{"sig":"MEYCIQC+Hf/2j3sHWO6JB+sVen/2UokJX/FfDJgA5RROQzRwqgIhAM9zYWhaffjso4qJPG4J7LQeFN7IWmezuA6ReBEt2CX2","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":550690},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"}},"gitHead":"788fbde2f6788677bf5c4f6b9393aaebbb13ea0d","scripts":{"lint":"npm run typecheck","test":"node --import tsx --test test/*.test.ts","build":"tsc -p tsconfig.json","prepack":"npm run build","typecheck":"tsc -p tsconfig.json --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json","example:swap":"npm run build --silent && node --env-file-if-exists=examples/.env --import tsx examples/swap.ts","example:quote":"npm run build --silent && node --env-file-if-exists=examples/.env --import tsx examples/quote.ts","example:status":"npm run build --silent && node --env-file-if-exists=examples/.env --import tsx examples/status.ts","test:e2e:status":"npm run build --silent && node --env-file-if-exists=.env.e2e --import tsx --test --test-concurrency=1 test/e2e/status.test.ts","example:discover":"npm run build --silent && node --env-file-if-exists=examples/.env --import tsx examples/discover.ts","test:e2e:read-only":"npm run build --silent && node --env-file-if-exists=.env.e2e --import tsx --test --test-concurrency=1 test/e2e/read-only.test.ts","test:e2e:same-erc20":"npm run build --silent && node --env-file-if-exists=.env.e2e --import tsx --test --test-concurrency=1 test/e2e/same-erc20.test.ts","test:e2e:same-native":"npm run build --silent && node --env-file-if-exists=.env.e2e --import tsx --test --test-concurrency=1 test/e2e/same-native.test.ts","test:e2e:cross-native":"npm run build --silent && node --env-file-if-exists=.env.e2e --import tsx --test --test-concurrency=1 test/e2e/cross-native.test.ts","example:probe-exact-out":"npm run build --silent && node --env-file-if-exists=examples/.env --import tsx examples/probe-exact-out.ts","example:probe-fee-model":"npm run build --silent && node --env-file-if-exists=examples/.env --import tsx examples/probe-fee-model.ts","example:decode-swap-data":"npm run build --silent && node --env-file-if-exists=examples/.env --import tsx examples/decode-swap-data.ts"},"_npmUser":{"name":"pandarr","email":"panda007.2021@gmail.com"},"repository":{"url":"git+https://github.com/butternetwork/wdk-protocol-swidge-butter.git","type":"git"},"_npmVersion":"11.6.4","description":"Butter Network Swidge provider for WDK.","directories":{},"_nodeVersion":"22.14.0","dependencies":{"viem":"^2.43.1","@scure/base":"^1.2.6","@noble/hashes":"^1.8.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.20.6","typescript":"^5.9.3","@types/node":"^22.14.0","@tetherto/wdk-wallet":">=1.0.0-beta.14 <2.0.0"},"peerDependencies":{"@tetherto/wdk-wallet":">=1.0.0-beta.14 <2.0.0"},"_npmOperationalInternal":{"tmp":"tmp/wdk-protocol-swidge-butter_0.1.0_1785838728969_0.5207918566789778","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"_id":"@butternetwork/wdk-protocol-swidge-butter@0.2.0","bugs":{"url":"https://github.com/butternetwork/wdk-protocol-swidge-butter/issues"},"dist":{"shasum":"055fb59e36f9150d4f085b37889211258af6706c","tarball":"https://registry.npmjs.org/@butternetwork/wdk-protocol-swidge-butter/-/wdk-protocol-swidge-butter-0.2.0.tgz","fileCount":87,"integrity":"sha512-CDQfYc1soUmVU0ZjAlyvagMKRscyOOQHYQr+4SilvpNuN49E5bngXj5LQX8ux6iP7hxsjKDMMqEMGu92zr1Bvw==","signatures":[{"sig":"MEUCIDV46OKV1qQUWPFMwYayqF2mASrq3CYNefsUmB2AgVOvAiEAoGo8fkSicqh+CrGMurljt7+faKGnBfRVFT6cb+PzPew=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIDXANqP7PBeyt/rW+OCiAoUO9g+mcBb1OMQ2k1NudLaoAiEA4JhUZpciiRmIns8sj4RqVVgQHzndJodRbRYP58vdvFg="}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@butternetwork%2fwdk-protocol-swidge-butter@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":716584},"main":"./dist/index.js","name":"@butternetwork/wdk-protocol-swidge-butter","type":"module","types":"./dist/index.d.ts","exports":{".":{"bare":"./bare.js","types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"}},"gitHead":"7030e26420ebd4948a7e6551b097479b27ed51ba","license":"Apache-2.0","scripts":{"lint":"npm run typecheck","test":"node --import tsx --test tests/*.test.ts tests/integration/*.test.ts","build":"tsc -p tsconfig.json","prepack":"npm run build","test:bare":"npm run build --silent && node scripts/check-package-exports.mjs && bare scripts/check-package-exports.mjs && node scripts/check-packed-package.mjs","typecheck":"tsc -p tsconfig.json --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json","check:repo":"node --import tsx --test checks/*.test.ts","example:swap":"npm run build --silent && node --env-file-if-exists=examples/.env --import tsx examples/swap.ts","example:quote":"npm run build --silent && node --env-file-if-exists=examples/.env --import tsx examples/quote.ts","example:status":"npm run build --silent && node --env-file-if-exists=examples/.env --import tsx examples/status.ts","test:e2e:status":"npm run build --silent && node --env-file-if-exists=.env.e2e --import tsx --test --test-concurrency=1 scripts/e2e/status.test.ts","example:discover":"npm run build --silent && node --env-file-if-exists=examples/.env --import tsx examples/discover.ts","test:e2e:read-only":"npm run build --silent && node --env-file-if-exists=.env.e2e --import tsx --test --test-concurrency=1 scripts/e2e/read-only.test.ts","test:e2e:same-erc20":"npm run build --silent && node --env-file-if-exists=.env.e2e --import tsx --test --test-concurrency=1 scripts/e2e/same-erc20.test.ts","test:e2e:same-native":"npm run build --silent && node --env-file-if-exists=.env.e2e --import tsx --test --test-concurrency=1 scripts/e2e/same-native.test.ts","test:e2e:cross-native":"npm run build --silent && node --env-file-if-exists=.env.e2e --import tsx --test --test-concurrency=1 scripts/e2e/cross-native.test.ts","example:probe-exact-out":"npm run build --silent && node --env-file-if-exists=examples/.env --import tsx examples/probe-exact-out.ts","example:probe-fee-model":"npm run build --silent && node --env-file-if-exists=examples/.env --import tsx examples/probe-fee-model.ts","example:decode-swap-data":"npm run build --silent && node --env-file-if-exists=examples/.env --import tsx examples/decode-swap-data.ts"},"version":"0.2.0","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:55d665f3-6365-4629-bb73-4f2c5d6ce5af"}},"homepage":"https://github.com/butternetwork/wdk-protocol-swidge-butter#readme","keywords":["wdk","swidge","butter","bridge","swap"],"repository":{"url":"git+https://github.com/butternetwork/wdk-protocol-swidge-butter.git","type":"git"},"_npmVersion":"11.19.1","description":"Butter Network Swidge provider for WDK.","directories":{},"maintainers":[{"name":"philllau","email":"philllllau@gmail.com"},{"name":"evan.ad","email":"evan.anderson.sun@gmail.com"},{"name":"pandarr","email":"panda007.2021@gmail.com"}],"_nodeVersion":"24.20.0","dependencies":{"viem":"^2.43.1","@scure/base":"^1.2.6","@noble/hashes":"^1.8.0","bare-node-runtime":"^1.4.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.20.6","bare":"1.31.2","typescript":"^5.9.3","@types/node":"^22.14.0","@tetherto/wdk-wallet":"1.0.0-beta.17","@tetherto/wdk-wallet-evm":"1.0.0-beta.17"},"peerDependencies":{"@tetherto/wdk-wallet":">=1.0.0-beta.17 <2.0.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/wdk-protocol-swidge-butter_0.2.0_1789632364976_0.5610676919322855"}}},"time":{"created":"2026-08-04T10:18:48.804Z","modified":"2026-09-17T08:06:05.476Z","0.1.0":"2026-08-04T10:18:49.134Z","0.2.0":"2026-09-17T08:06:05.087Z"},"bugs":{"url":"https://github.com/butternetwork/wdk-protocol-swidge-butter/issues"},"license":"Apache-2.0","homepage":"https://github.com/butternetwork/wdk-protocol-swidge-butter#readme","keywords":["wdk","swidge","butter","bridge","swap"],"repository":{"url":"git+https://github.com/butternetwork/wdk-protocol-swidge-butter.git","type":"git"},"description":"Butter Network Swidge provider for WDK.","maintainers":[{"name":"philllau","email":"philllllau@gmail.com"},{"name":"evan.ad","email":"evan.anderson.sun@gmail.com"},{"name":"pandarr","email":"panda007.2021@gmail.com"}],"readme":"# @butternetwork/wdk-protocol-swidge-butter\n\n<a href=\"https://docs.wdk.tether.io/\">\n  <img src=\"./docs/assets/built-with-wdk.png\" alt=\"Built with WDK\" width=\"240\" height=\"60\">\n</a>\n\nButter Network Swidge provider for WDK.\n\nThis package adapts WDK's Swidge interface to Butter Smart Router's `/route`,\n`/swap`, `/supportedChainInfo`, token-discovery, and Butter swap-data APIs.\nIt implements `ISwidgeProtocol` from `@tetherto/wdk-wallet`. Declared peer\ncompatibility is `>=1.0.0-beta.17 <2.0.0`; CI and development test against the\nminimum supported version, `1.0.0-beta.17`.\n\nThe package exposes its standard ESM entry in Node.js and a `bare` conditional\nentry that loads the Node compatibility runtime when imported from Bare.\n\nSee [CHANGELOG.md](./CHANGELOG.md) for release notes, breaking changes, and known\nupstream issues, and [Known limitations](#known-limitations) for what this provider\ndoes not do.\n\n## Install\n\n```sh\nnpm install @butternetwork/wdk-protocol-swidge-butter @tetherto/wdk-wallet\n```\n\nFor built-in EVM execution and the complete swap example, also install the WDK\nEVM account implementation:\n\n```sh\nnpm install @tetherto/wdk-wallet-evm@1.0.0-beta.17\n```\n\n## Complete example\n\nThis read-only example discovers Butter's current chains and advertised tokens,\nthen requests an exact-in quote. It does not send a transaction or require funds.\n\n```js\nimport ButterSwidgeProtocol from '@butternetwork/wdk-protocol-swidge-butter'\n\nconst sourceChainId = '56'\nconst destinationChainId = '137'\nconst nativeToken = '0x0000000000000000000000000000000000000000'\n\nconst protocol = new ButterSwidgeProtocol(undefined, {\n  sourceChainId,\n  entrance: 'wdk',\n  authMode: 'optional'\n})\n\nconst options = {\n  fromToken: nativeToken,\n  toToken: nativeToken,\n  toChain: destinationChainId,\n  fromTokenAmount: 1_000_000_000_000_000n,\n  slippage: 0.02\n}\n\nconst chains = await protocol.getSupportedChains()\nconst tokens = await protocol.getSupportedTokens({ fromChain: sourceChainId })\nconst quote = await protocol.quoteSwidge(options)\n\nconsole.log({ chains, tokens, quote })\n```\n\nSave the snippet as `quote.mjs` and run it with `node quote.mjs`.\n\nAnonymous requests are subject to Butter's rate limits. For authenticated quotes,\nconfigure the server-side credentials below. See\n[`examples/swap.ts`](./examples/swap.ts) for a confirmation-gated, complete EVM\ntransaction example.\n\n## Configuration\n\nEVM execution needs a full, send-capable WDK EVM account supplied by the host\napplication. A typical execution configuration is:\n\n```ts\nimport ButterSwidgeProtocol, {\n  toEvmPublicClient\n} from '@butternetwork/wdk-protocol-swidge-butter'\n\nconst protocol = new ButterSwidgeProtocol(account, {\n  sourceChainId: 56,\n  entrance: 'wdk',\n  requestTimeoutMs: 10_000, // complete Butter HTTP request, including body parsing\n  apiKeyId: process.env.BUTTER_API_KEY_ID,\n  apiSecret: process.env.BUTTER_API_SECRET,\n  maxNativeFee: 20000000000000000n, // required for cross-chain (see Safety Defaults)\n  evm: {\n    publicClient: toEvmPublicClient(viemPublicClient), // enables allowance checks + status\n    approvalTimeoutMs: 10_000\n  }\n})\n```\n\nThe account resolves the sender address, submits swap and approval calldata via\nits EVM `sendTransaction` implementation, and may confirm approval receipts via\n`getTransactionReceipt`. ERC20 execution reads allowance through the optional\n`evm.publicClient`, or through the account's `getAllowance(token, spender)` when\nno public client is configured. Standard WDK EVM accounts already provide this\nmethod. Custom account adapters must expose it or configure `evm.publicClient`;\nwithout either reader, execution throws `ButterConfigurationError` before any\ntransaction is broadcast. A read failure propagates without trying another\nreader or sending approvals. Quoting and native-token execution do not require\nan allowance reader.\n\nAn allowance equal to the input needs no approval. A zero allowance needs one\nexact approval; any other non-zero allowance is first reset with `approve(0)`\nand confirmed, then set to the exact input and confirmed before the swap. After\neach successful receipt, the same allowance reader must report exactly the\napproved value: zero before the amount approval, and the exact input before the\nswap. A successful receipt alone does not prove that `approve()` took effect.\nMismatches are polled every 2 seconds (shortened to the remaining deadline).\nEach approval shares one `evm.approvalTimeoutMs` budget between receipt and\nallowance confirmation, starting when submission returns its hash. RPC errors\nstop execution immediately; the provider never switches readers or rebroadcasts\nan approval to resolve a mismatch. This supports USDT-like tokens that reject\nchanging a non-zero allowance directly.\nWhen **every** send reports a gas fee,\nthe executed `SwidgeResult` reports the sender-reported total; otherwise it\nkeeps the route estimate. A sender may return an estimate: WDK EVM currently\nquotes gas before broadcasting, so this value is not a receipt-based actual cost.\nWhen a quote omits gas metadata, the reported fee is still included using the\nsource chain and its canonical native-token identifier, as described below.\n\nVersion 0.2 removes the former `evm.walletClient` and `toEvmWalletClient` APIs.\nPass a full WDK EVM account as the protocol's first constructor argument; there\nis no secondary sender or fallback submission path.\n\nOnly exact-in quotes are supported. Pass `fromTokenAmount` as a positive\n`bigint` in base units. This module deliberately rejects exact-out\n(`toTokenAmount`) before any network request: Butter's exact-out routing is not\nuniformly available across chains, so the option fails fast with\n`ButterExactOutUnsupportedError`. This also applies to the WDK base-class\n`swap()` delegation when it forwards a `tokenOutAmount`.\n\n`apiSecret` must not be bundled into browser or mobile clients. For public\nclients, use a backend proxy. Authentication defaults to `optional`; anonymous\nrequests are subject to Butter's unauthenticated rate limits. Set\n`authMode: 'required'` for production integrations that must never fall back to\nanonymous requests.\n\n### Affiliate and referrer\n\nTwo optional construction-time settings are forwarded to Butter `/route`:\n\n| Config | Format | Notes |\n| --- | --- | --- |\n| `affiliate` | `<nickname>` or `<nickname>:<rate>` | The affiliate collecting the integrator's share. Validated at construction. |\n| `referrer` | free-form string | **Mandatory for Solana same-chain routes**; optional on EVM. |\n\n**Leaving `affiliate` unset does not make the swap cheaper.** Butter substitutes\nits own default affiliate wallet whenever the parameter is absent, so the share\nis charged to the user either way — omitting it only forgoes *your* cut of a fee\nthe user already pays. Set it to collect that share, and know that it is being\ncollected regardless.\n\nIt is validated at construction rather than on the first request for the same\nreason: because Butter silently falls back to its own wallet, a malformed value\nwould otherwise produce a perfectly successful swap with the share quietly going\nelsewhere, and nothing to notice.\n\nA Solana **same-chain** route without `referrer` throws `ButterConfigurationError`\nbefore any request is sent — Butter documents the parameter as mandatory there, so\nthe request could never be valid.\n\nBoth participate in the route cache key, so a route quoted under one affiliate is\nnever reused after it changes. When unset, neither appears in the outgoing query\nnor in the cache key.\n\n### Exact-in only\n\nPass `fromTokenAmount`. Exact-out (`toTokenAmount`) is rejected before any network\nrequest with `ButterExactOutUnsupportedError`, on both `quoteSwidge`/`swidge` and the\nlegacy `quoteSwap`/`swap` delegation path.\n\nButter's `/route` documents `type: exactOut` as a valid value, but two things stop\nthis package from offering it:\n\n- The default production endpoint has been observed rejecting `type=exactOut` with\n  `errno 2000` (\"Parameter error\") while the identical `exactIn` request succeeds.\n- The `/route` documentation describes `amount` only as *\"amount of source token\"*,\n  with no variant for exactOut — so even against a working endpoint, which side the\n  amount denominates is unspecified, and guessing would misprice the trade.\n\n`npm run example:probe-exact-out` re-checks both against the live API (read-only, no\nfunded account, `exactIn` used as a control). If Butter later documents and supports\nthe denomination, exact-out needs a fresh input-bound design and security review;\nthe exact-in validator intentionally contains no dormant exact-out branch.\n\n## Behavior\n\n- `quoteSwidge(options)` calls Butter `/route`, stores a non-binding quote as an\n  optional execution cache, and returns it with a `routeHash` you can pin.\n- `swidge(options, config?)` can be called directly. By default it reuses a\n  matching fresh cached route or obtains a new one, enforces fee limits, calls\n  `/swap`, validates the returned transaction intent, performs EVM approval when\n  required, then sends the source transaction.\n- Route freshness is stricter on execution than on quoting. Both new and cached\n  routes need more than **15s** of their 5-minute lifetime for a **quote**, while\n  **execution** requires more than `routeExecutionMarginSeconds` (default **45s**): it\n  still has to complete the `/swap` round-trip, an optional ERC20 approval, and\n  the swap send before the quoted price has to hold on-chain. Inside the margin,\n  unpinned execution transparently re-quotes a cached route. A newly fetched route\n  inside its required window throws `ButterActionRequiredError` before `/swap`\n  or broadcasting; it is not cached or automatically retried. The default\n  deliberately does not assume an approval — when approvals are expected, raise\n  `routeExecutionMarginSeconds` above `evm.approvalTimeoutMs / 1000` (which\n  defaults to 10s). These two values are coupled; the\n  margin is configurable rather than hardcoded so the coupling stays explicit.\n- Execution rechecks that same margin after `/swap` and immediately before each\n  account `sendTransaction` call, including both ERC20 approvals and every adapter\n  transaction. Remaining lifetime must be **strictly greater** than the margin;\n  setting the margin to `0` still rejects an expired route. These checks use the\n  selected quote's original expiry, including when Butter omitted its timestamp.\n  If a wait consumes the margin, execution stops with `ButterActionRequiredError`;\n  after any broadcast it throws `ButterPartialExecutionError` instead, preserving\n  all submitted hashes, the freshness error as `cause`, and the blocked transaction's\n  role as `failedType`. Freshness diagnostics contain `hash`, `expiresAt`, `now`,\n  and `margin` (timestamps and margin are in seconds). Execution never automatically\n  re-quotes, resends, or revokes an allowance after this failure. Inspect submitted\n  transactions before trying again. The check precedes the account call: it cannot\n  constrain wallet-internal confirmation delays or chain inclusion time, and a\n  final send returning after expiry is still recorded normally.\n- Butter HTTP calls have a complete-request deadline: `requestTimeoutMs`\n  defaults to **10,000ms** and covers the fetch plus error-body or JSON-body\n  parsing. Timed-out requests abort and throw `ButterApiError`; they are not\n  retried automatically. ERC20 approval confirmation independently defaults to\n  **10,000ms** via `evm.approvalTimeoutMs` (`0` means immediate timeout).\n- Pinning a quote: pass `options.routeHash` (from a prior `quoteSwidge` result)\n  to `swidge` to execute that exact quoted route. `swidge` accepts\n  `ButterSwidgeOptions` (`SwidgeOptions & { routeHash? }`), so the field is part\n  of the public typed API. If the route has expired, expires within the execution\n  margin, or no longer matches the options, `swidge` throws\n  `ButterActionRequiredError` instead of silently re-quoting at a different\n  price — a pin is the price you approved, so it is never re-fetched the way an\n  unpinned execution is. Pins are held in the instance's in-memory\n  route cache, so quote and execution must use the same protocol instance.\n  Without `routeHash`, execution auto-re-quotes as before.\n  Hashes are opaque, non-empty strings without surrounding whitespace. Invalid\n  remote hashes throw `ButterApiError` before caching; invalid explicit pins\n  throw `ButterUnsupportedError` instead of enabling automatic re-quoting.\n- Exact-in only; see [Exact-in only](#exact-in-only) for why exact-out is rejected.\n- `getSwidgeStatus(id)` calls\n  `/api/queryBridgeInfoBySourceHash`; `{ byOrderId: true }` calls\n  `/api/queryCrossInfoByOrderId`. The options type is exported as\n  `ButterSwidgeStatusOptions`. Note this package never produces an order ID —\n  `SwidgeResult.id` is always the source-chain hash — so `byOrderId` is for callers\n  who obtained one from Butter separately. Same-chain swaps produce no cross-chain record,\n  so their status is derived from the transaction receipt — but only after the\n  source transaction is **attributed to a Butter Router**. If this instance\n  executed the id (recorded at `swidge` time) it is trusted; otherwise the source\n  tx is fetched via `evm.publicClient.getTransaction` and must target an\n  allowlisted Router **and** be `swapAndCall` (`swapAndBridge` ⇒ cross-chain).\n  This attribution holds even with explicit `{ fromChain, toChain }` hints — hints\n  never bypass it — so an unrelated transaction is never reported as a completed\n  swidge (an unverifiable same-chain id throws). It also works across process\n  restarts / new instances. Without a resolvable attribution it defaults to the\n  cross-chain API. For source hashes still recorded by this instance, cross-chain\n  status queries reject hints or reported chain IDs that conflict with the recorded\n  source and destination chains, including when no hints are supplied. Order-ID\n  queries and instances without that record retain their existing lookup behavior.\n  Omitted response chain fields remain optional; this consistency check does not\n  independently prove the API-reported settlement status. Transaction and\n  receipt lookups treat only viem's `TransactionNotFoundError` /\n  `TransactionReceiptNotFoundError` as absence; infrastructure faults (RPC\n  timeout, auth, rate-limit) propagate to the caller rather than being masked as\n  \"not found\" (which would force a false `pending` or a silent cross-API\n  fallback). Receipt-derived status\n  requires an `evm.publicClient` with `getTransactionReceipt` or an account that\n  exposes `getTransactionReceipt`, and is fail-closed (only an explicit success is\n  `completed`; an unknown receipt status stays `pending`).\n- Solana same-chain operations recorded by this instance use the account's\n  `getTransactionReceipt`: native `meta.err: null` means `completed`, a reported\n  error means `failed`, and missing or malformed metadata stays `pending`.\n  Normalized `status` receipts remain supported when `meta` is absent. Solana\n  same-chain attribution across new instances is not supported.\n- Tron same-chain operations recorded by this instance (including a source\n  broadcast before partial execution failed) use the account's\n  `getTransactionReceipt`, never `evm.publicClient`. Native `receipt.result:\n  'SUCCESS'` means `completed` when top-level `result` is absent or `'SUCESS'`\n  (the official Tron spelling). Top-level `'FAILED'` or a known contract failure\n  means `failed`, taking precedence over success. Missing, malformed, or\n  unrecognized native outcomes stay `pending`. Normalized `status` receipts\n  remain supported only when both native `result` and `receipt` fields are\n  absent. Tron same-chain attribution across new instances is not supported;\n  cross-chain status continues to use Butter's API.\n- `getSwidgeStatus` maps Butter cross states `0 → pending` (crossing),\n  `1 → completed`, and `6 → refunded`. There is no numeric `failed` state.\n  Any undocumented or intermediate code (e.g. a relaying state) maps\n  conservatively to `pending` rather than a terminal status, so an in-flight\n  transfer is never misreported as failed. Structured and boolean state values\n  also map to `pending`; only strings and finite numbers are mapped. A response\n  with no swidge info or no state still throws (the id is invalid/unknown).\n- `getSupportedChains()` merges Router-supported chains with token API\n  metadata. Each entry carries an extra `execution` field describing how this\n  instance would execute on that chain: `native` (built-in EVM), `adapter`\n  (configured `transactionAdapters`), or `quote-only`. A chain whose merged\n  metadata is missing an `id`, `type`, or `nativeToken` symbol is **dropped**\n  rather than listed with a placeholder — the same fail-closed rule\n  `getSupportedTokens` applies to a token with unusable decimals. Quoting and\n  execution are unaffected: they take chain ids from the caller, not from this\n  listing, and a dropped chain keeps any strict slippage floor it qualifies\n  for. Run `npm run example:discover` to see which chains this costs you on\n  live data (the output reports the dropped ids and their missing fields).\n- `getSupportedTokens(options)` calls Router\n  `/supportedTokenList?chainId=<id>`. Chain selection uses `fromChain`, then\n  `toChain`, then the instance's source chain; route-scoped `fromToken`\n  filtering is not available from Butter Router's per-chain listing. The result\n  is Butter's **advertised, non-exhaustive catalog**, not a route allowlist:\n  source- and destination-chain swaps can make additional tokens routeable.\n  `quoteSwidge` and `swidge` therefore never require catalog membership; `/route`\n  determines whether the requested token pair is currently routeable.\n- Token decimals resolve from `tokenDecimals` config first, then automatically\n  through Butter's `/findToken` API (cached per chain and canonical token). An\n  explicit `getSupportedTokens()` call also seeds this cache from its validated\n  catalog. Configure `tokenDecimals` only for tokens Butter cannot resolve. Tron\n  Base58Check and Butter hex forms share one cache key; Solana Base58 mints remain\n  case-sensitive.\n- Native aliases are resolved per chain before calling Butter: `sol` maps to\n  Solana's `So11111111111111111111111111111111111111112`, `trx` maps to Tron's\n  `T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb`, and `btc` maps to the zero-address token\n  identifier. The canonical addresses and the generic `native` sentinel are also\n  accepted.\n  On recognized EVM chains, `native`, `0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee`,\n  and the zero address are all sent to Butter as\n  `0x0000000000000000000000000000000000000000`, for both input and output tokens.\n  Equivalent aliases share the route cache and can be exchanged when executing\n  a pinned `routeHash`. Additional EVM chains use this encoding when listed in\n  `config.evmChainIds`; unknown chains retain their existing representation.\n  For symbol-only bridge fee components paid on the source chain, `native` and\n  that chain's `btc`/`trx`/`sol` alias share trusted source precision and use the\n  caller's input amount for fee caps. This symbol fallback applies only when the\n  caller supplied a symbolic native alias; address-form inputs still require a\n  component address, and a declared address always takes precedence over a symbol.\n\n## Status & fee mapping\n\n`getSwidgeStatus` maps Butter's state to WDK's `SwidgeStatus`:\n\n| Source | Value | `SwidgeStatus` |\n| --- | --- | --- |\n| Cross-chain `state` / `status` | `0`, `crossing`, `pending` | `pending` |\n| Cross-chain `state` / `status` | `1`, `success`, `completed` | `completed` |\n| Cross-chain `state` / `status` | `6`, `refund`, `refunded` | `refunded` |\n| Cross-chain `state` / `status` | `action-required` | `action-required` |\n| Cross-chain `state` / `status` | `refund-pending` | `refund-pending` |\n| Cross-chain `state` / `status` | `failed` | `failed` |\n| Cross-chain `state` / `status` | `cancelled` | `cancelled` |\n| Cross-chain `state` / `status` | `expired` | `expired` |\n| Cross-chain `state` / `status` | `partial` | `partial` |\n| Cross-chain `state` / `status` | any other / intermediate | `pending` (never a false terminal) |\n| Same-chain receipt | explicit success | `completed` |\n| Same-chain receipt | explicit revert | `failed` |\n| Same-chain receipt | missing / unknown | `pending` |\n| Recorded Solana same-chain receipt | `meta.err: null` | `completed` |\n| Recorded Solana same-chain receipt | non-empty error string / object | `failed` |\n| Recorded Solana same-chain receipt | missing / malformed metadata | `pending` |\n\n`quoteSwidge`/`swidge` map Butter route fees into WDK `SwidgeFee[]`. The last column\nis where each entry lands in the legacy `swap()`/`bridge()` scalars:\n\n| Butter field | `SwidgeFee.type` | Legacy field | Notes |\n| --- | --- | --- | --- |\n| `bridgeFee.in` | `protocol` | `bridgeFee` | inbound leg of the bridge fee, in **its own** token |\n| `bridgeFee.out` | `protocol` | `bridgeFee` | outbound leg of the bridge fee, in **its own** token |\n| `bridgeFee.affiliate` | `affiliate` | *(not visible)* | integrator/affiliate share — **counted against `maxProtocolFeeBps`** |\n| `bridgeFee.amount` | — | — | never priced; used only to detect that a fee exists which no component describes |\n| `gasFee` | `network` | `fee` | source-chain gas; route estimate, replaced by sender-reported gas when every send reports a fee (which may itself be estimated) |\n| `swapFee.nativeFee` | `protocol` | `bridgeFee` | native-denominated actual swap fee, including any charge configured by `feeConfig` |\n| `swapFee.tokenFee` | `protocol` | `bridgeFee` | input-token-denominated actual swap fee, including any charge configured by `feeConfig` |\n| `feeConfig` | — | — | referrer fee configuration used to validate `/swap` calldata; never added as a separate fee |\n| No reported fees | `network` | `fee` | zero-amount native-token placeholder, so `fees[]` is never empty |\n\nNetwork fees, `swapFee.nativeFee`, and zero-fee placeholders always use the\nconfigured source chain and its canonical native-token identifier in `fees[]`:\nthe zero address on recognized EVM chains (including `evmChainIds`),\n`So11111111111111111111111111111111111111112` on Solana,\n`T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb` on Tron, and `native` on Bitcoin or\nunrecognized chains. These identifiers replace response-provided symbols such\nas BNB or TRX. Consumers should identify fee currencies by `chain` and `token`\ntogether. Native precision still uses `nativeTokenDecimals` or the chain default.\n\nAn explicit `gasFee.chainId` must match the source chain, and an explicit\n`gasFee.address` must identify its native asset using a supported address or\nalias. Conflicts and invalid field types throw `ButterApiError` during quoting\nor before execution requests `/swap`, even for zero or absent gas amounts.\nMissing, null, or blank identity fields are allowed; symbols do not determine\nfee identity. Omitting a fee amount still does not count as reporting zero for\nfee-cap enforcement. Bridge and source-token fee identities are unchanged.\n\n`bridgeFee` is reported per component, and the top-level `bridgeFee.amount` summary is\n**never priced**. It is a single figure in a single token describing a fee that can\nspan three tokens, so it is not attributable — and amounts in different tokens cannot\nbe added, which rules out reconstructing a component from it or even checking it\nagainst the components' sum. When a route reports a summary but no `in`, `out` or\n`affiliate`, the fee is omitted from `fees[]` with a `bridge-fee-components-missing`\nwarning and a configured protocol cap refuses outright, rather than measuring a number\nit cannot attribute. `npm run example:probe-fee-model` shows how a live route\ndecomposes.\n\n**The affiliate share counts against `maxProtocolFeeBps`**, even though `fees[]`\nkeeps WDK's `affiliate` type for it. This is a deliberate deviation: WDK has no\naffiliate cap, and leaving the share unbounded bites hardest when you do *not* set\n`affiliate` — Butter then substitutes its own wallet, so your users pay a cut you\nnever chose. `maxProtocolFeeBps` is the only knob available to bound it.\n\nThe cap is enforced before `/swap` during `swidge`; quoting reports the same\ncomponents but does not reject a route over the cap. Enforcement converts each\ncomponent into a dimensionless fee-to-value ratio, sums the ratios, and compares\nthat exact sum with `maxProtocolFeeBps / 10_000`:\n\n- `swapFee.tokenFee / requestedAmountIn`;\n- for a native source, `swapFee.nativeFee / requestedAmountIn`;\n- for a non-native source,\n  `(swapFee.nativeFee / gasFee.amount) × (gasFee.inUSD / totalAmountInUSD)`;\n- each `bridgeFee.in`, `bridgeFee.out`, and `bridgeFee.affiliate` component\n  divided by `requestedAmountIn` when its payment chain and token both match the source;\n- otherwise, each bridge component divided by a route-leg amount carrying the\n  same payment chain and token address, with inbound components matched inbound-first and outbound\n  and affiliate components matched outbound-first.\n\nDifferent token amounts are never added directly. Missing source decimals,\nsame-token route amounts, required USD metadata, or bridge-component metadata\nneeded for valuation makes a configured cap fail closed with\n`ButterFeeValuationError`; the package does not silently treat an unvalued fee as\nzero.\n\nWhen a non-zero fee needs USD valuation, both `gasFee.inUSD` and\n`totalAmountInUSD` must be strictly positive. A zero USD estimate cannot value\na non-zero fee and throws `ButterFeeValuationError`, including with a zero bps\ncap. Explicitly zero fees do not require unused USD metadata. Quoting remains\navailable, and native-source fee ratios use the caller's input directly.\nPositive USD estimates still come from Butter; this check is not an independent\nprice oracle.\n\nEvery non-zero bridge component requires a valid `bridgeFee.chainId`, the payment\nchain documented by Butter. A missing or invalid chain rejects both quoting and\nexecution with `ButterFeeValuationError`, even without a fee cap. Identical addresses\non different chains are different currencies. Explicit zero components do not\nrequire unused chain metadata. The component token supplies its denomination;\nthe top-level bridge fee amount and token summary are not used for valuation.\n\n`swapFee` is Butter's authoritative actual fee result and already includes the fee\nconfigured by `feeConfig`. Fee mapping and `maxProtocolFeeBps` therefore read only\n`swapFee`; `feeConfig` is never added, used as a fallback, or compared with it. The\nconfiguration remains security-sensitive during execution: `/swap` calldata must\nencode the same `(feeType, referrer, rateOrNativeFee)` tuple returned by `/route`.\nWhen a protocol cap is configured, missing `swapFee` amounts fail closed while\nexplicit zero amounts are accepted.\n\n`fees[]` is always populated: if Butter reports no fees at all, it carries a single\nzero-amount `network` entry rather than being empty (an empty array reads as \"free\").\n\n**Read `fees[]`, not the legacy scalars.** The `protocol` group can hold three\ndifferent denominations at once — bridge token, native, and input token — so the\nlegacy `bridgeFee` total adds unlike currencies together. `bridge()`/`quoteBridge()`\nat least group by type (`fee` ← `network`, `bridgeFee` ← `protocol`);\n`swap()`/`quoteSwap()` do **not** group at all and sum every entry regardless of\ntype or currency. Both behaviours live in the WDK base class, which providers must\nnot override, so this needs a WDK-side fix. Set `onWarning` to be told when it\napplies to a given route:\n\n```js\nconst protocol = new ButterSwidgeProtocol(account, {\n  sourceChainId: 56,\n  entrance: 'wdk',\n  onWarning: ({ code, message, details }) => console.warn(code, message, details)\n})\n// -> 'mixed-currency-protocol-fees' when the protocol group spans several tokens\n// -> 'no-fees-reported'              when Butter reported none and fees[] is a placeholder\n// -> 'bridge-fee-components-missing' when a bridge fee summary cannot be split\n//                                    from its affiliate share\n```\n\n## Errors\n\nAll typed errors below are exported from the package root. `details` contains\nstructured context where the error class provides it.\n\n| Error | When thrown | User-actionable? | Recommended handling |\n| --- | --- | --- | --- |\n| `WdkError` | Base class for errors exposed by WDK modules. | Depends | Use as the common catch boundary when no narrower handling is needed. |\n| `ValueError` | A provider option or integration value is invalid. | Yes | Correct the supplied value before retrying. |\n| `UnsupportedOperationError` | The requested operation is unsupported. | Yes | Select a supported operation or execution adapter. |\n| `MaximumFeeExceededError` | A configured fee threshold is exceeded. | Yes | Review the route or configured cap; never loosen it automatically. |\n| `ProviderRequiredError` | The WDK account needs a provider to send the first transaction. | Yes | Configure the account provider before retrying. |\n| `ProviderError` | The WDK account provider rejects the first transaction. | Depends | Inspect the provider failure before deciding whether to retry. |\n| `TransactionError` | The WDK account cannot submit the first transaction. | Depends | Inspect the transaction failure and do not assume it was broadcast. |\n| `AccountRequiredError` | Execution has no full, send-capable account. | Yes | Supply a WDK account capable of sending the source-chain transaction. |\n| `ButterApiError` | Butter, a sender, or a response mapper returns malformed, inconsistent, timed-out, or unsuccessful data. | Sometimes | Preserve `details`; retry transient transport failures, otherwise report the response or integration fault. |\n| `ButterUnsupportedError` | The requested operation, option, adapter result, or transaction shape is unsupported. | Yes | Change the request or configure a supported adapter; do not force execution. |\n| `ButterConfigurationError` | Required provider, signer, router, timeout, credential, or execution configuration is missing or invalid. | Yes | Correct the integration configuration before retrying. |\n| `ButterActionRequiredError` | Execution needs a fresh quote, explicit recipient, valid slippage, or another caller decision. | Yes | Surface the requested action and retry only after the caller resolves it. |\n| `ButterFeeLimitExceededError` | A route's aggregate network or protocol fee ratio exceeds the configured cap. | Yes | Choose another route or explicitly review the cap; never loosen it automatically. |\n| `ButterFeeValuationError` | Quote mapping cannot safely parse source-denominated fee metadata, or a configured cap cannot value a fee against a trustworthy denominator. | Usually no | Requote or report the route; do not bypass fail-closed parsing or valuation. |\n| `ButterNoRouteError` | Butter explicitly reports no route or every candidate lacks liquidity. | Yes | Change the pair or amount, or retry later. |\n| `ButterPartialExecutionError` | Execution fails after at least one transaction was already broadcast. | Yes, with care | Inspect every entry in `transactions` and the preserved `cause`; never blindly retry. |\n| `ButterReadOnlyAccountError` | Execution has no send-capable WDK account. | Yes | Configure a full WDK account for the source chain. |\n| `ButterExactOutUnsupportedError` | The caller supplies an exact-out request. | Yes | Use an exact-in amount; exact-out is rejected before any network request. |\n| `ButterTransactionValidationError` | `/swap` calldata or transaction metadata does not match the quoted intent or configured limits. | Usually no | Reject the transaction, requote once if appropriate, and report repeated mismatches. |\n\n## Safety Defaults\n\n- `sourceChainId` and `entrance` are required.\n- Same-chain `/route` responses must omit `dstChain` and `bridgeChain`;\n  cross-chain responses must include a matching destination segment. Output token,\n  decimals, estimated amount, and minimum all use the validated output segment.\n- Exact-out, zero inputs, unsafe JavaScript numbers, and amount conversions that\n  would discard decimal precision are rejected.\n- Explicit cross-chain slippage below Butter's documented floor is rejected.\n  Defaults use the applicable minimum. BTC routes use the stricter 300\n  bps floor; additional IDs can be configured with `strictSlippageChainIds`.\n- `minAmountOut` is compared locally with the minimum returned by `/route`\n  because Butter's documented API does not expose a separate request parameter.\n  Same-chain quotes also require at least\n  `ceil(toTokenAmount * (10000 - slippageBps) / 10000)` in output base units,\n  calculated with integer arithmetic. Input-token fees are already reflected in\n  the quoted output and are not deducted again. Both constraints apply to fresh,\n  cached, and pinned routes; `minAmountOut: 0` does not disable slippage protection.\n  For cross-chain execution this remains a quote check, not calldata enforcement:\n  the destination minimum is inside the nested bridge payload trusted to Butter.\n- `refundAddress` is optional, and when you name one it is **verified rather\n  than assumed**. Omit it to accept Butter's own default refund destination,\n  trusted like the rest of the destination routing. Naming one asks for a\n  guarantee, so it is checked against the address the calldata actually encodes:\n  the nested bridge payload's `refundAddress` cross-chain, or `swapAndCall`'s\n  leftover receiver same-chain. If that payload cannot be decoded, the guarantee\n  cannot be checked, so execution is rejected instead of proceeding as if it\n  held — drop `refundAddress` to continue with Butter's default. It no longer has\n  to equal the source sender: on a cross-VM route the source address is not even\n  spendable on the destination chain.\n- The built-in EVM path executes **exactly one** Router transaction. A `/swap`\n  response with more than one transaction is rejected, so repeated\n  individually-valid Router calls cannot multiply native/ERC20 spend.\n- EVM Router V3 calldata is validated at a deliberate middle tier. Always\n  enforced: the target must be an allowlisted router (and match the route's\n  `contract`); the top-level intent — initiator, source token, source amount,\n  and empty permit data — must match the request; the referrer `feeData` must\n  match the route's quoted `feeConfig` as a full `(feeType, referrer,\n  rateOrNativeFee)` tuple — empty `feeData` is rejected when the route quoted a\n  non-zero fee, and a non-empty `feeData` requires the quoted tuple to be\n  complete (fail closed on any missing field) and to match exactly, so `/swap`\n  cannot inject an unchecked `feeType`/`referrer` by under-specifying the quote;\n  and the transaction value must satisfy the native-spend bounds below.\n  Same-chain `swapAndCall` additionally verifies the destination token,\n  recipient, leftover receiver, and minimum output.\n- Native-spend bounds: `tx.value` is checked as two *one-sided* bounds rather\n  than an exact `input + routerFee + bridgeFee` equality. `/route` formats the\n  router fee as a decimal string while `/swap` returns `tx.value` as a hex\n  integer, so exact equality would reject a perfectly good transaction over a\n  sub-wei artifact in that round-trip.\n  - The native **input** half is a hard **lower** bound: a value below the\n    quoted native input is rejected as under-funded.\n  - The remaining **fee** half is bounded only from **above**. Paying less than\n    quoted cannot harm you — the router reverts if the fee is genuinely\n    insufficient — so there is no lower bound and no two-sided tolerance.\n  - The fee half's upper bounds are `maxNativeFee` (the security boundary) and\n    the quoted `routerFee + bridgeFee` plus a 0.5 % formatting-drift tolerance\n    (a consistency sanity check that catches a `/swap` charging materially more\n    native than `/route` advertised).\n  The bridge messaging fee inside `tx.value` comes from the `/swap` calldata and\n  is **trusted** — it is not bounded by the quote — so `maxNativeFee` (an\n  absolute cap on the whole fee half, in native base units) is the actual\n  native-drain guard. When set, it is enforced on any chain (same-chain carries\n  only the router fee). **Cross-chain execution fails closed without it**\n  whenever the destination chain differs from the source chain — including when\n  the calldata reports a zero bridge fee, so a route cannot opt out of the cap\n  by under-reporting what `tx.value` spends. Same-chain swaps do not require it.\n\n  `maxNativeFee` can also be passed **per call** on `swidge(options)`, where it\n  takes precedence over the configured value (in both directions — a per-call cap\n  may loosen or tighten it, and `0n` means \"no native fee at all\"). Prefer the\n  per-call form when one long-lived instance serves a wide range of trade sizes:\n  a single absolute cap is either too tight for small routes or nominal for large\n  ones, and the caller knows the size at call time. Setting it per call also\n  satisfies the cross-chain fail-closed requirement.\n- Cross-VM destinations require an **explicit `recipient`** on `swidge`. WDK\n  defaults the recipient to the account address, which is only meaningful while\n  the destination chain uses the same address format; bridging EVM→Solana/BTC\n  without one would otherwise forward a `0x` address as the destination receiver.\n  Address families are resolved from a best-effort table of Butter's non-EVM chain\n  ids (`constants.ts: NON_EVM_CHAIN_FAMILIES`) — unlisted chains remain\n  `unknown`, so execution requires an explicit recipient unless the integrator\n  declares a new EVM chain through `evmChainIds`. The\n  requirement applies only to `swidge`: `quoteSwidge` still prices a cross-VM route\n  without a recipient, since asking a price before choosing a destination address\n  is the normal flow.\n- Cross-chain destination routing — the destination **recipient, output token,\n  and minimum output** encoded in the nested bridge payload — is **trusted to\n  Butter's `/swap` response and is NOT verified**. This is an accepted\n  middle-tier trust boundary, not full calldata intent validation: a compromised\n  or buggy `/swap` could route the destination output elsewhere. Only the bridge\n  target (destination chain) is checked. Source-token exposure remains bounded\n  because the module approves only the exact input amount to the router. Setting\n  `minAmountOut` rejects an inadequate route, but does not upgrade this\n  cross-chain destination guarantee beyond `quoted-only`.\n- ERC20 approval only occurs after calldata validation and only targets a\n  configured Butter router for the source chain. The approval is **always for the\n  exact input amount** — there is no unbounded/`max` approval option — so a\n  compromised router can never move more than this swap's input.\n- `maxNetworkFeeBps` and `maxProtocolFeeBps` are enforced only in `swidge`,\n  before `/swap`, approvals, or transaction submission. `quoteSwidge` never\n  throws on a cap — a quote is a non-binding estimate and always returns the\n  full fee breakdown for inspection. Per-call values override constructor\n  defaults. Cross-token fees require route-provided USD or same-stage valuation\n  metadata when a cap is enabled; unvaluable fees fail closed with\n  `ButterFeeValuationError`.\n- Quotes and discovery do not require a WDK account or local transaction adapter.\n  Execution without a send-capable WDK account fails before a\n  route request.\n- Tron, Solana, and BTC require explicit `transactionAdapters`; Tron is not\n  treated as viem-compatible EVM execution. Adapter execution bypasses the\n  Router V3 calldata validation performed on the built-in EVM path — only chain\n  ID and required transaction fields are checked, so adapters carry their own\n  trust responsibility for the provider-supplied transaction data. Adapter\n  output is still fully classified **before anything is broadcast**: each\n  declared `type` must be a legal `SwidgeTransaction` role, a multi-transaction\n  result must classify every entry (`{ transaction, type }`), and the set must\n  resolve to exactly one `source` — any violation throws with nothing sent, so a\n  failed classification cannot leave a partially-broadcast operation a retry\n  could double-execute.\n- EVM transaction submission requires a full, send-capable WDK EVM account. The\n  same account supplies the sender address and submits every approval and Router\n  transaction, preventing signer/initiator divergence. ERC20 approval is always the exact input amount — an oversized existing\n  allowance is reduced (`approve(0)` then `approve(amount)`), and an approval that\n  cannot be confirmed (no receipt source) is refused rather than sent\n  fire-and-forget. `evm.approvalConfirmations` must be a positive safe integer\n  (default **1**). Account receipt confirmation supports one confirmation only;\n  a higher count requires `evm.publicClient.waitForTransactionReceipt` whenever\n  an approval is needed, otherwise execution fails before the first approval.\n  An already exact allowance needs no approval and is unaffected by that requirement.\n  Approval receipt hashes, when supplied, must match the submitted approval.\n  Cancellation, replacement, and repricing receipts with a different hash stop\n  execution with `ButterPartialExecutionError`; inspect the replacement before\n  retrying. Hash-less custom receipts remain supported, but their provider must\n  guarantee that the receipt belongs to the requested transaction.\n  `SwidgeResult.fees` reports sender-reported source gas only when\n  **every** send returns a fee, otherwise the route estimate; bridge/protocol fees\n  remain route-derived estimates. A reported gas fee may be a broadcast-time\n  estimate, not the final cost of a mined transaction.\n- **Partial execution is reported, never silently discarded.** Execution can\n  broadcast more than one transaction (`approve(0)`, `approve(amount)`, the swap;\n  or several adapter legs). If execution fails *after* at least one transaction has\n  already gone out, `swidge()` throws a `ButterPartialExecutionError` whose\n  `transactions` lists every broadcast hash in submission order and whose `cause`\n  is the original failure. **Do not blindly retry** — those transactions are\n  already on-chain and re-sending would double-execute them; inspect them first.\n  This includes an approval that cannot be confirmed (reverted, unknown receipt\n  status, a confirmation timeout, or an allowance that cannot be verified): the approval is already on the wire, so you\n  get its hash and the underlying error as `cause` — the swap itself is still never\n  sent against an unconfirmed approval. It also includes a send that succeeded but\n  reported an unusable gas fee: a transaction is recorded the moment its send\n  returns, *before* the fee is validated, so a malformed fee never erases the hash.\n  An allowance timeout has a `ButterConfigurationError` cause with the approval\n  hash, token, spender, expected allowance, last observed allowance (when available),\n  and timeout in `details`; `failedType` remains `approval`.\n  Fees are checked at runtime as non-negative bigints and hashes as non-empty\n  strings on both the built-in EVM path and the adapter path, because a\n  host-supplied sender makes the declared types hints rather than guarantees. The\n  hash is the one value checked *before* recording — it *is* the record, so a send\n  that returns no usable hash is unidentifiable and throws unwrapped rather than\n  being reported. A failure before anything is broadcast propagates\n  unwrapped. When the broadcast set includes the `source` transaction, it is\n  registered before the throw, so `getSwidgeStatus(hash)` still resolves the\n  in-flight swidge.\n- Transaction/receipt lookups through `toEvmPublicClient` treat only a genuine\n  viem not-found as absent; every other fault (RPC timeout, auth, rate-limit)\n  propagates. The check is **copy-independent** — it matches viem's error `name`\n  plus its `BaseError` shape rather than relying on `instanceof`, which fails when\n  the host application resolves a different copy of viem than this package.\n- Legacy `swap()`/`quoteSwap()`/`bridge()`/`quoteBridge()` from the WDK base\n  class sum `fees[].amount` **across denominations** (ignoring `fee.token`), so\n  their scalar `fee`/`bridgeFee` are only meaningful when every fee shares one\n  currency. Butter fees can span native, input, and bridge tokens — read the\n  itemised `fees[]` on the `SwidgeQuote`/`SwidgeResult` for correct per-currency\n  costs. This is a WDK base-class contract issue a provider cannot fix without\n  overriding legacy methods (which is disallowed); a WDK-side change is needed.\n\nExample fee policy:\n\n```ts\nconst protocol = new ButterSwidgeProtocol(account, {\n  sourceChainId: 56,\n  entrance: 'wdk',\n  maxNetworkFeeBps: 100,\n  maxProtocolFeeBps: 200\n})\n\nawait protocol.swidge(options, { maxNetworkFeeBps: 50 })\n```\n\n## Supported chains and tokens\n\nExecution capability comes in three tiers (`discovery.ts: executionFor`), reported\nper chain as `execution` by `getSupportedChains()`:\n\n| Tier | Meaning |\n| --- | --- |\n| `native` | built-in EVM Router execution: this package validates the `/swap` calldata itself and submits it through the WDK EVM account |\n| `adapter` | execution goes through a `transactionAdapters` entry you supply. Router calldata validation does **not** apply — only chain ID and required fields are checked |\n| `quote-only` | quoting, discovery, and status work; execution is unavailable until you pin a Router via `routerContracts` or supply an adapter |\n\nButter chain support is discovered at runtime and can change independently of a\npackage release. Call `getSupportedChains()` for the current list; it starts with\nButter's `/supportedChainInfo` response, enriches the entries with token API\nmetadata, and reports this package's current execution tier on each returned\nchain. Do not infer current Butter support from `DEFAULT_ROUTER_CONTRACTS`: that\nregistry is a transaction-target allowlist, not a support catalog. A chain that\nButter no longer advertises is not returned even if a historical Router entry is\nstill pinned locally.\n\nFor a chain Butter currently advertises, the tier is selected in this order:\nTron remains `adapter`-or-`quote-only`; another chain with a verified EVM Router\nis `native`; a remaining chain with a configured `transactionAdapters` entry is\n`adapter`; otherwise it is `quote-only`. See\n[Router Registry](#router-registry) for supplying a verified Router deployment.\n\n**Butter's advertised token catalog is discovered at runtime**, not listed here —\ncall `getSupportedTokens({ fromChain })`. It is useful for recommended-token UIs\nand cache prewarming, but it is intentionally non-exhaustive and must not be used\nto reject a quote. Both discovery listings are fail-closed on missing required\nmetadata: a catalog token without usable decimals, and a chain without an `id`,\n`type`, or `nativeToken` symbol, are **dropped** rather than returned with a\nplaceholder. A chain you expect to see but don't is usually this, not an outage.\nToken precision accepts only integers from 0 through 255 or digit strings\n(surrounding whitespace is ignored). Empty strings and booleans are invalid.\nValid but conflicting `decimals` / `decimal` aliases reject the entire catalog\nresponse without seeding its entries into the precision cache.\n\nCatalog token identifiers must be non-blank strings; surrounding whitespace is\ntrimmed while identifier case is preserved. Malformed identifiers, chain IDs,\nor supplied non-string symbols/names cause that entry to be dropped. Missing\nsymbols retain the empty-string default, and missing names are omitted. Valid\nsiblings remain available, and only validated entries seed the decimals cache.\n\n## Known limitations\n\n- **No automated testnet integration tests.** The WDK integration guide asks for\n  them; this package does not have them yet. The env-gated flows in\n  [`examples/`](./examples/README.md) are the live-check mechanism in the meantime,\n  including a read-only `example:decode-swap-data` for inspecting real Router\n  calldata.\n- **Adapter and cross-chain minimum outputs are quote-only.** Check\n  `quote.destinationGuarantees`: `'enforced'` (built-in EVM same-chain execution,\n  the minimum is verified against the Router calldata) or `'quoted-only'`\n  (adapter execution delegates deeper validation to the host; cross-chain destination\n  minimum sits in the nested bridge payload that this package trusts to Butter by\n  design). WDK's field description calls it a guaranteed minimum, so the difference\n  is worth knowing.\n- **Exact-out is not supported** — see [Exact-in only](#exact-in-only). Butter\n  documents the mode, but the default production endpoint rejects it and the\n  denomination of `amount` for it is unspecified.\n- **A destination chain this package does not recognize requires an explicit\n  `recipient`.** The address-family table is best-effort and Butter adds chains\n  between releases, so an unrecognized chain is treated as \"cannot default the\n  recipient\" rather than assumed EVM. Add such a chain to `evmChainIds` once you\n  have confirmed it is EVM.\n- **`priceImpact` is not reported.** Butter only exposes it per route leg, with no\n  documented unit or whole-operation aggregation, so picking one leg would\n  misrepresent a multi-leg operation. The field is left `undefined` rather than\n  guessed.\n- **Legacy fee scalars can be meaningless.** See the fee mapping table above.\n\n## Router Registry\n\nThe package includes a versioned registry of known Router V3 deployments.\nAddresses are pinned because `/route` and `/swap` are remote, untrusted inputs;\nan API response cannot authorize a new transaction target by itself. The\nbuilt-in set is a curated transaction-target allowlist, not a statement of\ncurrent Butter chain support. `getSupportedChains()` only surfaces chains in\nButter's live response; among those, a chain without a pinned entry is quote-only\nfor built-in EVM execution until its Router is supplied via `routerContracts`\n(verify the address independently first).\n\nPer-chain configuration replaces the built-in entries for that chain:\n\n```ts\nconst protocol = new ButterSwidgeProtocol(account, {\n  sourceChainId: 56,\n  entrance: 'wdk',\n  apiKeyId,\n  apiSecret,\n  routerContracts: {\n    56: [{ address: '0x1111111111111111111111111111111111111111', version: 'v3' }],\n    137: [{ address: '0x2222222222222222222222222222222222222222', version: 'v3' }]\n  }\n})\n```\n\nUse an empty array to disable built-in EVM execution for a chain. A configured\naddress must use a validator version supported by this package; an address with\na new ABI version requires a package update.\n\nWhen Butter changes a Router address, existing installations reject calldata to\nthe new address before approval or transaction submission. This is a deliberate\nfail-closed outage, not an automatic migration. Integrators can restore service\nwithout waiting for a package release by verifying the deployment independently\nand replacing that chain's `routerContracts` entry. In an emergency involving a\nvulnerable old Router, operators must remove it (or temporarily configure `[]`)\nand notify integrators; the static defaults cannot dynamically revoke a formerly\ntrusted deployment.\n\n## Support\n\nFor non-security integration and usage questions, email\n[business@butternetwork.io](mailto:business@butternetwork.io). A `[SECURITY]`\nsubject prefix is not needed for ordinary support requests.\n\n## Security\n\nReport suspected vulnerabilities privately. Do not include vulnerability details\nin a public GitHub issue or discussion. See the [security policy](./SECURITY.md)\nfor supported versions, reporting channels, and the coordinated disclosure\nprocess.\n\n## Development\n\n```sh\nnpm test\nnpm run test:bare\nnpm run check:repo\nnpm run typecheck\nnpm run build\nnpm pack --dry-run\n```\n\n## Examples\n\nRunnable Node.js examples for discovery, exact-in quotes, read-only Router\ncalldata inspection, status lookup, and a confirmation-gated same-chain EVM swap\nare available in [`examples/`](./examples/README.md).\n\n```sh\nnpm run example:discover\nnpm run example:quote\nnpm run example:decode-swap-data\nnpm run example:probe-exact-out\nnpm run example:status\nnpm run example:swap\n```\n\nOnly `example:swap` sends a transaction, and it refuses to run without an\nexplicit confirmation value.\n","readmeFilename":"README.md"}