diff --git a/.changeset/noir-wallet-zcash.md b/.changeset/noir-wallet-zcash.md new file mode 100644 index 0000000..627d785 --- /dev/null +++ b/.changeset/noir-wallet-zcash.md @@ -0,0 +1,6 @@ +--- +"@swapkit/wallet-extensions": minor +"@swapkit/wallets": minor +--- + +Add Noir Wallet connector for Zcash (`connectNoirWallet`). Noir Wallet is a shielded-first Zcash browser extension; the connector delegates balance and transaction building to the extension and supports deposit-address swap routes (e.g. NEAR Intents). OP_RETURN memo routes are rejected with `wallet_noir_wallet_memo_not_supported`. `NOIR_WALLET` and the `wallet_noir_wallet_*` error codes (80101-80103) register through the `@swapkit/helpers` 5.1.0 extensible registries. diff --git a/.changeset/swapkit-sdk-36el3mv6gxftc.md b/.changeset/swapkit-sdk-36el3mv6gxftc.md new file mode 100644 index 0000000..83ebfb3 --- /dev/null +++ b/.changeset/swapkit-sdk-36el3mv6gxftc.md @@ -0,0 +1,17 @@ +--- +"@swapkit/sdk": patch +"@swapkit/wallet-extensions": patch +"@swapkit/wallet-hardware": patch +"@swapkit/wallet-mobile": patch +"@swapkit/wallets": patch +--- + +Update SwapKit SDK dependencies: + +- @swapkit/core: 5.0.2 → 5.0.3 +- @swapkit/helpers: 5.0.2 → 5.1.0 +- @swapkit/plugins: 5.0.0 → 5.0.3 +- @swapkit/server: 5.0.0 → 5.1.1 +- @swapkit/toolboxes: 5.1.1 → 5.1.2 +- @swapkit/wallet-core: 5.0.0 → 5.0.3 +- @swapkit/wallet-keystore: 5.0.0 → 5.0.3 diff --git a/.changeset/tonconnect-registry.md b/.changeset/tonconnect-registry.md new file mode 100644 index 0000000..feb092b --- /dev/null +++ b/.changeset/tonconnect-registry.md @@ -0,0 +1,5 @@ +--- +"@swapkit/wallets": patch +--- + +`TON_CONNECT` now registers through the `@swapkit/helpers` 5.1.0 extensible `WalletOption` registry (swapkit/sdk#346) instead of the interim outside-the-enum literal shim. No behavior change — the runtime value stays `"TON_CONNECT"`. diff --git a/README.md b/README.md new file mode 100644 index 0000000..bc5c6d7 --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +# SwapKit Wallets + +Wallet connectors for the [SwapKit SDK](https://github.com/swapkit/sdk): +browser extensions, hardware devices, and mobile/protocol wallets, exposed +through a single `loadWallet` entry point and typed `SKWallets` registry. + +## Packages + +| Package | Contents | +| --- | --- | +| [`@swapkit/wallets`](packages/wallets) | aggregator: `loadWallet`, `SKWallets`/`SKWalletsSupportedChains` types, and connectors without their own package (keystore, tonconnect, xaman, radix, passkeys, …) | +| [`@swapkit/wallet-extensions`](packages/wallet-extensions) | injected browser-extension providers (MetaMask/EVM, Keplr, Vultisig, Ctrl, Phantom, Petra, Noir Wallet, …) | +| [`@swapkit/wallet-hardware`](packages/wallet-hardware) | Ledger, Trezor, KeepKey | +| [`@swapkit/wallet-mobile`](packages/wallet-mobile) | mobile wallet support | +| [`@swapkit/sdk`](packages/sdk) | batteries-included bundle re-exporting core, plugins, toolboxes, and wallets | + +Each connector is a lazy-loaded subpath export — apps pull only the wallets +they use. + +## Development + +Requires [bun](https://bun.sh) — use the version pinned in +[`.github/workflows/ci.yml`](.github/workflows/ci.yml) to keep `bun.lock` +reproducible. + +```bash +bun install +bun run build:ci # build all packages + .d.ts generation +bun run type-check # tsc across packages +bun test # bun test across packages +bun run lint # biome check --fix +bun run playground:vite-lite:start # browser playground +``` + +## Adding a wallet + +See [docs/adding-a-wallet.md](docs/adding-a-wallet.md) — covers the connector +module, registering the `WalletOption` and error codes through the +`@swapkit/helpers` extensible registries (no helpers release required), the +`loadWallet`/`SKWallets` wiring, and the gotchas. + +## Releases + +Versioning goes through [changesets](https://github.com/changesets/changesets): +every user-facing change ships a `.changeset/*.md` entry, and `@swapkit/*` +dependency bumps additionally need the generated changeset from +`bun generate:dep-changeset` (CI enforces it). Merges to `develop` roll up +into a "Version Packages" PR; merging that publishes to npm. diff --git a/bun.lock b/bun.lock index 117e7ce..9c352e3 100644 --- a/bun.lock +++ b/bun.lock @@ -15,13 +15,13 @@ "@cosmjs/proto-signing": "0.37.0", "@cosmjs/stargate": "0.37.0", "@scure/bip32": "2.2.0", - "@swapkit/core": "^5.0.2", - "@swapkit/helpers": "^5.0.2", - "@swapkit/plugins": "^5.0.0", - "@swapkit/server": "^5.0.0", - "@swapkit/toolboxes": "^5.1.1", - "@swapkit/wallet-core": "^5.0.0", - "@swapkit/wallet-keystore": "^5.0.0", + "@swapkit/core": "^5.0.3", + "@swapkit/helpers": "^5.1.0", + "@swapkit/plugins": "^5.0.3", + "@swapkit/server": "^5.1.1", + "@swapkit/toolboxes": "^5.1.2", + "@swapkit/wallet-core": "^5.0.3", + "@swapkit/wallet-keystore": "^5.0.3", "@types/bun": "1.3.13", "@types/node": "25.6.0", "ledger-bitcoin": "0.3.0", @@ -31,23 +31,23 @@ }, "packages/sdk": { "name": "@swapkit/sdk", - "version": "4.6.47", + "version": "4.6.48", "dependencies": { "@stricahq/typhonjs": "~3.0.1", - "@swapkit/core": "^5.0.2", - "@swapkit/helpers": "^5.0.2", - "@swapkit/plugins": "^5.0.0", - "@swapkit/server": "^5.0.0", - "@swapkit/toolboxes": "^5.1.1", - "@swapkit/wallet-core": "^5.0.0", - "@swapkit/wallet-keystore": "^5.0.0", + "@swapkit/core": "^5.0.3", + "@swapkit/helpers": "^5.1.0", + "@swapkit/plugins": "^5.0.3", + "@swapkit/server": "^5.1.1", + "@swapkit/toolboxes": "^5.1.2", + "@swapkit/wallet-core": "^5.0.3", + "@swapkit/wallet-keystore": "^5.0.3", "@swapkit/wallets": "workspace:*", "cosmjs-types": "0.10.1", }, }, "packages/wallet-extensions": { "name": "@swapkit/wallet-extensions", - "version": "4.5.29", + "version": "4.5.30", "dependencies": { "@aptos-labs/ts-sdk": "~1.34.0", "@cosmjs/amino": "~0.37.0", @@ -59,10 +59,10 @@ "@near-js/transactions": "~2.5.0", "@scure/base": "~2.2.0", "@solana/web3.js": "~1.98.4", - "@swapkit/helpers": "^5.0.2", - "@swapkit/toolboxes": "^5.1.1", + "@swapkit/helpers": "^5.1.0", + "@swapkit/toolboxes": "^5.1.2", "@swapkit/utxo-signer": "^3.0.0", - "@swapkit/wallet-core": "^5.0.0", + "@swapkit/wallet-core": "^5.0.3", "@wallet-standard/app": "^1.1.1", "cosmjs-types": "0.10.1", "ethers": "^6.14.0", @@ -89,7 +89,7 @@ }, "packages/wallet-hardware": { "name": "@swapkit/wallet-hardware", - "version": "4.9.31", + "version": "4.9.32", "dependencies": { "@cosmjs/amino": "~0.37.0", "@cosmjs/crypto": "0.37.0", @@ -112,10 +112,10 @@ "@near-js/transactions": "~2.5.0", "@scure/base": "2.2.0", "@scure/bip32": "2.2.0", - "@swapkit/helpers": "^5.0.2", - "@swapkit/toolboxes": "^5.1.1", + "@swapkit/helpers": "^5.1.0", + "@swapkit/toolboxes": "^5.1.2", "@swapkit/utxo-signer": "^3.0.0", - "@swapkit/wallet-core": "^5.0.0", + "@swapkit/wallet-core": "^5.0.3", "@trezor/connect-web": "~9.7.3", "cosmjs-types": "~0.10.1", "ethers": "^6.14.0", @@ -157,16 +157,16 @@ }, "packages/wallet-mobile": { "name": "@swapkit/wallet-mobile", - "version": "4.3.22", + "version": "4.3.23", "dependencies": { - "@swapkit/helpers": "^5.0.2", - "@swapkit/toolboxes": "^5.1.1", - "@swapkit/wallet-core": "^5.0.0", + "@swapkit/helpers": "^5.1.0", + "@swapkit/toolboxes": "^5.1.2", + "@swapkit/wallet-core": "^5.0.3", }, }, "packages/wallets": { "name": "@swapkit/wallets", - "version": "4.11.0", + "version": "4.11.1", "dependencies": { "@coinbase/wallet-sdk": "~4.3.7", "@cosmjs/amino": "~0.37.0", @@ -184,10 +184,10 @@ "@scure/base": "~2.2.0", "@scure/bip39": "~2.2.0", "@solana/web3.js": "~1.98.4", - "@swapkit/helpers": "^5.0.2", - "@swapkit/toolboxes": "^5.1.1", + "@swapkit/helpers": "^5.1.0", + "@swapkit/toolboxes": "^5.1.2", "@swapkit/utxo-signer": "^3.0.0", - "@swapkit/wallet-core": "^5.0.0", + "@swapkit/wallet-core": "^5.0.3", "@swapkit/wallet-extensions": "workspace:*", "@swapkit/wallet-hardware": "workspace:*", "@ton/core": "~0.63.1", @@ -1131,21 +1131,21 @@ "@swapkit/contracts": ["@swapkit/contracts@5.0.0", "", {}, "sha512-iqHjDIYr5dWiQRld7ks2sh/bxPQfkPrw96UpF135xePZJgcjufYPH4OBCoh6aY9JX0n5mr0QyIoe0PDrcBWWKw=="], - "@swapkit/core": ["@swapkit/core@5.0.2", "", { "dependencies": { "@swapkit/helpers": "5.0.2", "@swapkit/plugins": "5.0.2", "@swapkit/toolboxes": "5.1.1", "@swapkit/wallet-core": "5.0.2" }, "peerDependencies": { "@stricahq/typhonjs": "3.1.0", "@ton/core": "0.63.1", "cosmjs-types": "0.11.0", "ts-pattern": "5.9.0" } }, "sha512-J9cYBxjqOxSaILpNyH2AG4OHu8tMNvdm0NwJFP254Fizlm7Xftvuyx8OXduIZyWwneoUfTzTsmIl7/rIwuReUQ=="], + "@swapkit/core": ["@swapkit/core@5.0.3", "", { "dependencies": { "@swapkit/helpers": "5.1.0", "@swapkit/plugins": "5.0.3", "@swapkit/toolboxes": "5.1.2", "@swapkit/wallet-core": "5.0.3" }, "peerDependencies": { "@stricahq/typhonjs": "3.1.0", "@ton/core": "0.63.1", "cosmjs-types": "0.11.0", "ts-pattern": "5.9.0" } }, "sha512-9XDNdcv6FZkiAyCr5ayH/VDHLzF3Ey/RE0Kwm+uqfqBLWkCZ3wILP8Z9YdNrQCNRPCq2TFgKLeSeSCZO1YV85w=="], "@swapkit/cosmos-signer": ["@swapkit/cosmos-signer@0.2.0", "", { "peerDependencies": { "@noble/curves": "2.2.0", "@noble/hashes": "2.2.0", "@scure/base": "2.2.0", "@scure/bip32": "2.2.0", "@scure/bip39": "2.2.0", "cosmjs-types": "0.11.0" } }, "sha512-++NFJuxvIzD9zZsbr69asbtZOgTsnuXHZm5/iKKup8SGdl0WeNlXk2LMDY8YNtEyNYwzzj3jTcAbVvDZQWzLmQ=="], - "@swapkit/helpers": ["@swapkit/helpers@5.0.2", "", { "dependencies": { "@noble/hashes": "2.2.0", "@swapkit/contracts": "5.0.0", "@swapkit/tokens": "5.0.2", "@swapkit/types": "1.0.1" }, "peerDependencies": { "ts-pattern": "5.9.0", "zod": "4.4.3", "zustand": "5.0.13" } }, "sha512-o4cLjYxoXfnIcxhsO+Pbdj/MqIHRt26/tyiiklpu5VORlQcyB86TCf71NOVRhVFUg5/IFFOp4soMQFvhPu2VQw=="], + "@swapkit/helpers": ["@swapkit/helpers@5.1.0", "", { "dependencies": { "@noble/hashes": "2.2.0", "@swapkit/contracts": "5.0.0", "@swapkit/tokens": "5.0.3", "@swapkit/types": "1.0.1" }, "peerDependencies": { "ts-pattern": "5.9.0", "zod": "4.4.3", "zustand": "5.0.13" } }, "sha512-zdAmjnR58fzF61QV65tgb6xAo/prU6T772y4OSOyswTD1SxO8nYIil8xss+XIDHDj9gD4SEFhyFpgcyhA+rrgg=="], - "@swapkit/plugins": ["@swapkit/plugins@5.0.2", "", { "dependencies": { "@swapkit/helpers": "5.0.2", "@swapkit/toolboxes": "5.1.1", "@swapkit/utxo-signer": "3.0.0" }, "peerDependencies": { "@mysten/sui": "2.17.0", "@near-js/transactions": "2.5.1", "@near-js/utils": "2.5.1", "@noble/hashes": "2.2.0", "@scure/base": "2.2.0", "@solana/web3.js": "1.98.4", "@stellar/stellar-sdk": "16.1.0", "ethers": "6.16.0", "ts-pattern": "5.9.0" } }, "sha512-5Oxsy5QVt5lB/d80J7xnPxpld8DJUQ9V74HGX1GknrRfvJl8FSm2weOa4W1YlsPvJ5/xc9PXBhwC2WD3Vjzf9g=="], + "@swapkit/plugins": ["@swapkit/plugins@5.0.3", "", { "dependencies": { "@swapkit/helpers": "5.1.0", "@swapkit/toolboxes": "5.1.2", "@swapkit/utxo-signer": "3.0.0" }, "peerDependencies": { "@mysten/sui": "2.17.0", "@near-js/transactions": "2.5.1", "@near-js/utils": "2.5.1", "@noble/hashes": "2.2.0", "@scure/base": "2.2.0", "@solana/web3.js": "1.98.4", "@stellar/stellar-sdk": "16.1.0", "ethers": "6.16.0", "ts-pattern": "5.9.0" } }, "sha512-2nrj7aUDccRO2pJTreq5VWGv1cSWVg9a5s9ZDvJC66Vqj7bXMf5YzKuMmc3Ynm1XHQ39fhGbLyzsC+P3FllX5w=="], "@swapkit/sdk": ["@swapkit/sdk@workspace:packages/sdk"], - "@swapkit/server": ["@swapkit/server@5.1.0", "", { "dependencies": { "@swapkit/helpers": "5.0.2", "@swapkit/tokens": "5.0.2", "@swapkit/toolboxes": "5.1.1", "@swapkit/wallet-keystore": "5.0.2" }, "peerDependencies": { "ts-pattern": "5.9.0" } }, "sha512-7zC300r37Bs8urVDEj3aDAdQRXl46wrMDyXdr+Qr0/8TzcusMDx/f8DPDGt2BRz1TY+skjGbpcc/xQfueyT5TA=="], + "@swapkit/server": ["@swapkit/server@5.1.1", "", { "dependencies": { "@swapkit/helpers": "5.1.0", "@swapkit/tokens": "5.0.3", "@swapkit/toolboxes": "5.1.2", "@swapkit/wallet-keystore": "5.0.3" }, "peerDependencies": { "ts-pattern": "5.9.0" } }, "sha512-+maFtVW7VVHDHQe6NkRWQgKFOP9Mtv74Q5pVk9jx0R7MgiRdz6OPbAn/MzXJ3SCxOCDvfeWHlvQWfajqmbiq3A=="], - "@swapkit/tokens": ["@swapkit/tokens@5.0.2", "", { "peerDependencies": { "ts-pattern": "5.9.0" } }, "sha512-32zHRhy560xQ/JSF1KNJrJW6+nLdw6mUPZVSvV4ZcTyS4p8b+rz2bDnlf6p9sSVhuubp3KlXJOHYkTy/D9Vmig=="], + "@swapkit/tokens": ["@swapkit/tokens@5.0.3", "", { "peerDependencies": { "ts-pattern": "5.9.0" } }, "sha512-/5ii1kdxBT2+zU63RU1MsRe7juxfAGR+cNytqFd2JgYG2+BmCGdzz6aoBC22wxdwPFU6cfC9r7xe0qJdAxWpvQ=="], - "@swapkit/toolboxes": ["@swapkit/toolboxes@5.1.1", "", { "dependencies": { "@swapkit/helpers": "5.0.2", "@swapkit/utxo-signer": "3.0.0" }, "peerDependencies": { "@aptos-labs/ts-sdk": "7.0.1", "@mysten/sui": "2.17.0", "@near-js/accounts": "2.5.1", "@near-js/crypto": "2.5.1", "@near-js/providers": "2.5.1", "@near-js/signers": "2.5.1", "@near-js/transactions": "2.5.1", "@near-js/utils": "2.5.1", "@noble/curves": "2.2.0", "@noble/hashes": "2.2.0", "@orbs-network/ton-access": "2.3.3", "@polkadot/api": "16.5.6", "@polkadot/keyring": "14.0.3", "@polkadot/rpc-provider": "16.5.6", "@polkadot/util": "14.0.3", "@polkadot/util-crypto": "14.0.3", "@provablehq/sdk": "0.11.3", "@radixdlt/babylon-gateway-api-sdk": "1.10.1", "@radixdlt/radix-dapp-toolkit": "2.3.0", "@scure/base": "2.2.0", "@scure/bip32": "2.2.0", "@scure/bip39": "2.2.0", "@solana/spl-memo": "0.2.5", "@solana/spl-token": "0.4.14", "@solana/web3.js": "1.98.4", "@stellar/stellar-sdk": "16.1.0", "@stricahq/bip32ed25519": "1.1.2", "@stricahq/cbors": "1.0.4", "@stricahq/typhonjs": "3.1.0", "@swapkit/cosmos-signer": "0.2.0", "@ton/core": "0.63.1", "@ton/crypto": "3.3.0", "@ton/ton": "16.2.4", "bignumber.js": "9.3.1", "cosmjs-types": "0.11.0", "ethers": "6.16.0", "micro-key-producer": "0.8.6", "near-seed-phrase": "0.2.1", "protobufjs": "8.0.3", "ripple-address-codec": "5.0.1", "starknet": "10.0.2", "ts-pattern": "5.9.0", "xrpl": "4.6.0" } }, "sha512-ZopaAv3ZJZ4bBspk/0ElCqJPxAHzhS45gbPeipWXuuigtgGeWFMQaWI6oVcI+vakSqlWldhyqhIoZGF3FgVNOA=="], + "@swapkit/toolboxes": ["@swapkit/toolboxes@5.1.2", "", { "dependencies": { "@swapkit/helpers": "5.1.0", "@swapkit/utxo-signer": "3.0.0" }, "peerDependencies": { "@aptos-labs/ts-sdk": "7.0.1", "@mysten/sui": "2.17.0", "@near-js/accounts": "2.5.1", "@near-js/crypto": "2.5.1", "@near-js/providers": "2.5.1", "@near-js/signers": "2.5.1", "@near-js/transactions": "2.5.1", "@near-js/utils": "2.5.1", "@noble/curves": "2.2.0", "@noble/hashes": "2.2.0", "@orbs-network/ton-access": "2.3.3", "@polkadot/api": "16.5.6", "@polkadot/keyring": "14.0.3", "@polkadot/rpc-provider": "16.5.6", "@polkadot/util": "14.0.3", "@polkadot/util-crypto": "14.0.3", "@provablehq/sdk": "0.11.3", "@radixdlt/babylon-gateway-api-sdk": "1.10.1", "@radixdlt/radix-dapp-toolkit": "2.3.0", "@scure/base": "2.2.0", "@scure/bip32": "2.2.0", "@scure/bip39": "2.2.0", "@solana/spl-memo": "0.2.5", "@solana/spl-token": "0.4.14", "@solana/web3.js": "1.98.4", "@stellar/stellar-sdk": "16.1.0", "@stricahq/bip32ed25519": "1.1.2", "@stricahq/cbors": "1.0.4", "@stricahq/typhonjs": "3.1.0", "@swapkit/cosmos-signer": "0.2.0", "@ton/core": "0.63.1", "@ton/crypto": "3.3.0", "@ton/ton": "16.2.4", "bignumber.js": "9.3.1", "cosmjs-types": "0.11.0", "ethers": "6.16.0", "micro-key-producer": "0.8.6", "near-seed-phrase": "0.2.1", "protobufjs": "8.0.3", "ripple-address-codec": "5.0.1", "starknet": "10.0.2", "ts-pattern": "5.9.0", "xrpl": "4.6.0" } }, "sha512-gNj6sOjuvPMQpHS2TcAsX0UeeQfgbxhgJX1j5q5e/tN3BK+E7wt0mXnWfwhtw3WeWc9oEfBx1XEwd0DLQOFu8w=="], "@swapkit/types": ["@swapkit/types@1.0.1", "", {}, "sha512-LMBtyw20Udf913p8//tiE8gk80Zcnbg0VSkgczctZUMSg/OZ4FuBmaOlacrFJA45FqNR7npYgdusgOzIGiWYVg=="], @@ -1153,13 +1153,13 @@ "@swapkit/utxo-signer": ["@swapkit/utxo-signer@3.0.0", "", { "peerDependencies": { "@noble/curves": "2.2.0", "@noble/hashes": "2.2.0", "@scure/base": "2.2.0", "micro-packed": "0.9.0" } }, "sha512-naPyfACwNmQPtoaa4QNXuIsZQLPYNJKt0eFjWK3LatqIhw3BTAnd6Fxaha66Ec3xebn1wr0v1gbzAIkWPwYSHw=="], - "@swapkit/wallet-core": ["@swapkit/wallet-core@5.0.2", "", { "dependencies": { "@swapkit/helpers": "5.0.2" } }, "sha512-McFuhAKzrarQzA+8QD2mO4H1Ti1zoVt3hmqPohXooofZc0iSZPSGm83jSxW/QelnTe/ZXJSoRHb9ZZDRl2oGrg=="], + "@swapkit/wallet-core": ["@swapkit/wallet-core@5.0.3", "", { "dependencies": { "@swapkit/helpers": "5.1.0" } }, "sha512-VmzgXeovQDW2HFmpcsdbhOJ1P4yxzTRDQ+LxSpf1va5dUSxZZI/wPnvrrfPAOTzEBFsL3FQWRPhbMlXtKAShag=="], "@swapkit/wallet-extensions": ["@swapkit/wallet-extensions@workspace:packages/wallet-extensions"], "@swapkit/wallet-hardware": ["@swapkit/wallet-hardware@workspace:packages/wallet-hardware"], - "@swapkit/wallet-keystore": ["@swapkit/wallet-keystore@5.0.2", "", { "dependencies": { "@swapkit/helpers": "5.0.2", "@swapkit/toolboxes": "5.1.1", "@swapkit/wallet-core": "5.0.2" }, "peerDependencies": { "@noble/hashes": "2.2.0", "@scure/bip39": "2.2.0" } }, "sha512-qeVADbUWYkcEVmamLjGvBgwrDleFWGraNgwt+NLFGLuaeWUboqmrvXcghVoyqZGE1VfnpEDQG7iC93HaNtY7KA=="], + "@swapkit/wallet-keystore": ["@swapkit/wallet-keystore@5.0.3", "", { "dependencies": { "@swapkit/helpers": "5.1.0", "@swapkit/toolboxes": "5.1.2", "@swapkit/wallet-core": "5.0.3" }, "peerDependencies": { "@noble/hashes": "2.2.0", "@scure/bip39": "2.2.0" } }, "sha512-Acyn8QiU4R/btGrCRiNPeiwjgmQOjLFWw8DyJ3859cXbB2HVtV+jIqTxiQF1i1F2YmbNXP//iuAWB2cNwFJ9Cg=="], "@swapkit/wallet-mobile": ["@swapkit/wallet-mobile@workspace:packages/wallet-mobile"], diff --git a/docs/adding-a-wallet.md b/docs/adding-a-wallet.md new file mode 100644 index 0000000..68bd67b --- /dev/null +++ b/docs/adding-a-wallet.md @@ -0,0 +1,186 @@ +# Adding a wallet connector + +How to add a new wallet to this repo, including registering its `WalletOption` +and error codes through the extensible registries that shipped with +`@swapkit/helpers` 5.1.0 ([swapkit/sdk#346]). No `@swapkit/helpers` release is +needed to add a wallet. + +Worked examples in this repo: + +- `packages/wallet-extensions/src/noir-wallet/` — injected browser-extension + wallet with custom error codes (added via fork PR #111 + #144) +- `packages/wallets/src/tonconnect/` — connector living directly in + `@swapkit/wallets`, registry registration only + +## Where things live + +| Package | Contents | +| --- | --- | +| `@swapkit/wallet-extensions` | injected browser-extension providers (`window.*`) | +| `@swapkit/wallet-hardware` | Ledger, Trezor, KeepKey | +| `@swapkit/wallets` | aggregator: `loadWallet`, `SKWallets` types, plus a few connectors that need no separate package (tonconnect, xaman, radix, …) | + +The `WalletOption` union, `SwapKitError` keys, and the registries themselves +are defined upstream in `@swapkit/helpers` (SDK monorepo). + +## 1. The connector module + +Create `packages/wallet-extensions/src//index.ts` built around +`createWallet` from `@swapkit/wallet-core`: + +- `connect` receives `{ addChain, walletType }` and returns the + `connect` function. For each supported chain, spread the chain's + toolbox into `addChain({ ...toolbox, address, chain, walletType, ... })` and + override what the wallet handles itself (`transfer`, `getBalance`, + `signMessage`, `signAndBroadcastTransaction`, …). +- `directSigningSupport` maps chains where the wallet can sign an + API-prebuilt transaction (`route.tx`). With `{ [chain]: true }`, core routes + swaps to the generic SwapKit plugin, which decodes `route.tx` and calls your + `signAndBroadcastTransaction` override. With `{}`, swaps go through the + provider plugin, which calls high-level methods like `transfer`. Wallets + that only expose a "send" RPC can still support direct signing by + translating the decoded transaction back into a send — see + `extractUtxoTransferIntent` in + `packages/wallet-extensions/src/helpers/utxoTransferIntent.ts` (Vultisig, + Ctrl, KeepKey BEX). +- If the wallet cannot serve a method the toolbox spread exposes, override it + with a clear `SwapKitError` throw instead of letting the toolbox default + fail deep inside signing (see `unsupportedUtxoSignTransaction`). + +## 2. The register module + +Create `packages/wallet-extensions/src//register.ts`. The type-level +declaration and the runtime registration must sit side by side — one without +the other compiles to a trap (see [Gotchas](#gotchas)): + +```ts +import { registerErrorCodes, registerWalletOption } from "@swapkit/helpers"; + +declare module "@swapkit/helpers" { + interface WalletOptionRegistry { + MY_WALLET: "MY_WALLET"; + } + interface SwapKitErrorRegistry { + wallet_my_wallet_not_found: 80201; + } +} + +registerWalletOption("MY_WALLET", "MY_WALLET"); +registerErrorCodes({ + wallet_my_wallet_not_found: 80201, +}); +``` + +Rules: + +- The `declare module` must target `"@swapkit/helpers"` — that package + declares the registries — even when consumers import `WalletOption` from + `@swapkit/core`. +- Error codes must use the **80000–89999 extension range**. It is reserved by + convention, not enforced at runtime; first-party codes live outside it and + collisions throw only when two keys claim the same number. Grep this repo's + registers for the next free block (noir-wallet holds 80101–80103). +- Registration is idempotent for identical values and throws + `helpers_invalid_params` on conflicting re-registration, so the module may + safely load through multiple import paths. +- Registered options are appended to `SKConfig`'s default wallet list + automatically (and survive `SKConfig.reinitialize()`). + +Make the connector's `index.ts` import it first: + +```ts +// Registers WalletOption.MY_WALLET and the wallet_my_wallet_* error codes +// before anything below reads them. +import "./register"; +``` + +## 3. Package exports + +Add **two** subpath exports to `packages/wallet-extensions/package.json`, +mirroring the neighbors' shape: + +```jsonc +"./my-wallet": { "bun": "./src/my-wallet/index.ts", ... }, +"./my-wallet/register": { "bun": "./src/my-wallet/register.ts", ... }, +``` + +The separate `register` subpath exists so `loadWallet` (and any app that only +references `WalletOption.MY_WALLET` — wallet pickers, `walletType` +comparisons) can import a ~20-line side-effect module instead of statically +pulling the whole connector, which would defeat the lazy loading in the match +arms. Apps that import the connector subpath directly need nothing extra — +`index.ts` imports `./register` itself. + +Also add the new register module to `packages/wallets/src/register.ts` — the +roll-up that `@swapkit/wallets/register` exposes to apps. An app that puts a +registered option in a module-scope wallet list (as the SwapKit UI does in its +wallet dialog) evaluates `WalletOption.MY_WALLET` before any connector loads; +its entry module must `import "@swapkit/wallets/register"` first or the list +silently contains `undefined`. + +## 4. Wiring `@swapkit/wallets` + +Three touch points: + +- **`src/utils.ts`** — side-effect import at the top (ordering matters: the + match reads `WalletOption.MY_WALLET` at call time, before any connector has + loaded), plus a lazy match arm: + + ```ts + import "@swapkit/wallet-extensions/my-wallet/register"; + // ... + .with(WalletOption.MY_WALLET, async () => (await import("@swapkit/wallet-extensions/my-wallet")).myWallet) + ``` + +- **`src/types.ts`** — `SKWallets` and `SKWalletsSupportedChains` entries, + keyed `[WalletOption.MY_WALLET]:` (computed keys in type position work + because the const's properties are distinct string literals). + +## 5. Tests + +Alongside the usual connector tests, always assert the registered value: + +```ts +test("registers MY_WALLET in the extensible WalletOption registry", () => { + expect(WalletOption.MY_WALLET).toBe("MY_WALLET"); +}); +``` + +Without this, a missing registration makes `walletType` assertions pass +vacuously: `expect(wallet.walletType).toBe(WalletOption.MY_WALLET)` is +`undefined === undefined`. This exact false-green happened in the original +Noir Wallet PR. + +## 6. Changeset + +Add a `.changeset/*.md` entry (`minor` for a new connector). If the change +also bumps `@swapkit/*` dependency versions, run `bun generate:dep-changeset` +and commit the generated file — CI's `check` workflow fails without it. + +## Gotchas + +- **Type/runtime skew.** The module augmentation is visible program-wide the + moment the `.d.ts` is in the compilation; the runtime key exists only after + `register.ts` executes. Code that reads `WalletOption.MY_WALLET` before + registration gets `undefined` — and a ts-pattern `.with(undefined, …)` arm + then matches *any* unregistered option. Keep the side-effect import above + the code that reads the key. +- **Tree-shaking.** Neither package sets `"sideEffects"` in package.json, so + bundlers keep the register imports. If `"sideEffects": false` is ever + added, the `register.ts` files must be listed as exceptions or registration + silently disappears from production bundles. +- **One `@swapkit/helpers` copy, exactly.** Published SDK packages pin exact + helpers versions. Bumping helpers alone forks the lockfile into nested + copies — two helpers instances at runtime means registrations land in one + copy while `@swapkit/core` reads the other, with no error. Bump the whole + `@swapkit` release train together, reinstall clean with the CI-pinned bun + (see `.github/workflows/ci.yml`), and verify: + + ```bash + grep -o '@swapkit/helpers@[0-9][^"]*' bun.lock | sort | uniq -c + ``` + + One `5.x` entry is correct. (A legacy `4.x` copy nested under the old + `@swapkit/ui` devDep is expected and inert.) + +[swapkit/sdk#346]: https://github.com/swapkit/sdk/pull/346 diff --git a/package.json b/package.json index b5ee423..9b95380 100644 --- a/package.json +++ b/package.json @@ -8,13 +8,13 @@ "@cosmjs/proto-signing": "0.37.0", "@cosmjs/stargate": "0.37.0", "@scure/bip32": "2.2.0", - "@swapkit/core": "^5.0.2", - "@swapkit/helpers": "^5.0.2", - "@swapkit/plugins": "^5.0.0", - "@swapkit/server": "^5.0.0", - "@swapkit/toolboxes": "^5.1.1", - "@swapkit/wallet-core": "^5.0.0", - "@swapkit/wallet-keystore": "^5.0.0", + "@swapkit/core": "^5.0.3", + "@swapkit/helpers": "^5.1.0", + "@swapkit/plugins": "^5.0.3", + "@swapkit/server": "^5.1.1", + "@swapkit/toolboxes": "^5.1.2", + "@swapkit/wallet-core": "^5.0.3", + "@swapkit/wallet-keystore": "^5.0.3", "@types/bun": "1.3.13", "@types/node": "25.6.0", "ledger-bitcoin": "0.3.0", diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 47f5556..c78a2fe 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -2,13 +2,13 @@ "author": "swapkit-dev", "dependencies": { "@stricahq/typhonjs": "~3.0.1", - "@swapkit/core": "^5.0.2", - "@swapkit/helpers": "^5.0.2", - "@swapkit/plugins": "^5.0.0", - "@swapkit/server": "^5.0.0", - "@swapkit/toolboxes": "^5.1.1", - "@swapkit/wallet-core": "^5.0.0", - "@swapkit/wallet-keystore": "^5.0.0", + "@swapkit/core": "^5.0.3", + "@swapkit/helpers": "^5.1.0", + "@swapkit/plugins": "^5.0.3", + "@swapkit/server": "^5.1.1", + "@swapkit/toolboxes": "^5.1.2", + "@swapkit/wallet-core": "^5.0.3", + "@swapkit/wallet-keystore": "^5.0.3", "@swapkit/wallets": "workspace:*", "cosmjs-types": "0.10.1" }, diff --git a/packages/wallet-extensions/package.json b/packages/wallet-extensions/package.json index 5ce85db..6b9394b 100644 --- a/packages/wallet-extensions/package.json +++ b/packages/wallet-extensions/package.json @@ -11,10 +11,10 @@ "@near-js/transactions": "~2.5.0", "@scure/base": "~2.2.0", "@solana/web3.js": "~1.98.4", - "@swapkit/helpers": "^5.0.2", - "@swapkit/toolboxes": "^5.1.1", + "@swapkit/helpers": "^5.1.0", + "@swapkit/toolboxes": "^5.1.2", "@swapkit/utxo-signer": "^3.0.0", - "@swapkit/wallet-core": "^5.0.0", + "@swapkit/wallet-core": "^5.0.3", "@wallet-standard/app": "^1.1.1", "cosmjs-types": "0.10.1", "ethers": "^6.14.0", @@ -82,6 +82,18 @@ "require": "./dist/src/keplr/index.cjs", "types": "./dist/types/keplr/index.d.ts" }, + "./noir-wallet": { + "bun": "./src/noir-wallet/index.ts", + "default": "./dist/src/noir-wallet/index.js", + "require": "./dist/src/noir-wallet/index.cjs", + "types": "./dist/types/noir-wallet/index.d.ts" + }, + "./noir-wallet/register": { + "bun": "./src/noir-wallet/register.ts", + "default": "./dist/src/noir-wallet/register.js", + "require": "./dist/src/noir-wallet/register.cjs", + "types": "./dist/types/noir-wallet/register.d.ts" + }, "./okx": { "bun": "./src/okx/index.ts", "default": "./dist/src/okx/index.js", diff --git a/packages/wallet-extensions/src/noir-wallet/__tests__/noir-wallet.test.ts b/packages/wallet-extensions/src/noir-wallet/__tests__/noir-wallet.test.ts new file mode 100644 index 0000000..ec138e6 --- /dev/null +++ b/packages/wallet-extensions/src/noir-wallet/__tests__/noir-wallet.test.ts @@ -0,0 +1,132 @@ +// @ts-nocheck - Test file with intentional mocking of browser globals +import { beforeEach, describe, expect, test } from "bun:test"; +import { AssetValue, Chain, WalletOption } from "@swapkit/helpers"; + +import { noirWallet } from "../index"; + +const TRANSPARENT_ADDRESS = "t1XVXWCvpMgBvUaed4XDqWtgQgJSu1Ghz7F"; + +function mockNoirWallet({ balance = {}, requests = [] } = {}) { + globalThis.window = { + noirwallet: { + isNoirWallet: true, + version: "1.0.25", + zcash: { + disconnect: async () => undefined, + on: () => undefined, + removeListener: () => undefined, + request: ({ method, params }) => { + requests.push({ method, params }); + + switch (method) { + case "zcash_requestAccounts": + return Promise.resolve({ accounts: [], shielded: "zs1mock", transparent: TRANSPARENT_ADDRESS }); + case "zcash_getBalance": + return Promise.resolve({ + available: "1.42", + shielded: "1.5", + spendable: "1.45", + total: "1.6", + transparent: "0.1", + ...balance, + }); + case "zcash_sendTransaction": + return Promise.resolve("mocktxid123"); + case "zcash_signMessage": + return Promise.resolve({ + address: TRANSPARENT_ADDRESS, + pubkey: "02aa", + signature: "sigabc", + signingMode: "current", + }); + default: + return Promise.reject(new Error(`unexpected method ${method}`)); + } + }, + }, + }, + }; + + return requests; +} + +async function connectAndGetWallet() { + let addedWallet = null; + const connect = noirWallet.connectNoirWallet.connectWallet({ + addChain: (wallet) => { + addedWallet = wallet; + }, + }); + + await connect([Chain.Zcash]); + + return addedWallet; +} + +describe("noirWallet", () => { + let requests = []; + + beforeEach(() => { + requests = mockNoirWallet(); + }); + + test("exposes Zcash as the only supported chain", () => { + expect(noirWallet.connectNoirWallet.supportedChains).toEqual([Chain.Zcash]); + }); + + test("registers NOIR_WALLET in the extensible WalletOption registry", () => { + expect(WalletOption.NOIR_WALLET).toBe("NOIR_WALLET"); + }); + + test("connects via zcash_requestAccounts and registers the transparent address", async () => { + const wallet = await connectAndGetWallet(); + + expect(requests.some(({ method }) => method === "zcash_requestAccounts")).toBe(true); + expect(wallet.address).toBe(TRANSPARENT_ADDRESS); + expect(wallet.chain).toBe(Chain.Zcash); + expect(wallet.walletType).toBe(WalletOption.NOIR_WALLET); + expect(wallet.walletType).toBe("NOIR_WALLET"); + }); + + test("getBalance reads the wallet's spendable (shielded pool) balance", async () => { + const wallet = await connectAndGetWallet(); + const [balance] = await wallet.getBalance(); + + expect(balance.chain).toBe(Chain.Zcash); + expect(balance.getValue("string")).toBe("1.42"); + }); + + test("transfer delegates to zcash_sendTransaction without memo/type fields", async () => { + const wallet = await connectAndGetWallet(); + const assetValue = AssetValue.from({ chain: Chain.Zcash, value: "0.5" }); + + const txid = await wallet.transfer({ assetValue, recipient: "t1RecipientAddr" }); + + expect(txid).toBe("mocktxid123"); + const sendCall = requests.find(({ method }) => method === "zcash_sendTransaction"); + expect(sendCall.params).toEqual([{ amount: "0.5", to: "t1RecipientAddr" }]); + }); + + test("transfer with memo throws (OP_RETURN routes unsupported)", async () => { + const wallet = await connectAndGetWallet(); + const assetValue = AssetValue.from({ chain: Chain.Zcash, value: "0.5" }); + + expect(() => wallet.transfer({ assetValue, memo: "=:BTC.BTC:bc1q...", recipient: "t1RecipientAddr" })).toThrow( + "wallet_noir_wallet_memo_not_supported", + ); + }); + + test("signMessage returns the signature string", async () => { + const wallet = await connectAndGetWallet(); + + expect(await wallet.signMessage("hello swapkit")).toBe("sigabc"); + }); + + test("throws when the extension is not installed", () => { + globalThis.window = {}; + + const connect = noirWallet.connectNoirWallet.connectWallet({ addChain: () => undefined }); + + expect(connect([Chain.Zcash])).rejects.toThrow("wallet_noir_wallet_not_found"); + }); +}); diff --git a/packages/wallet-extensions/src/noir-wallet/index.ts b/packages/wallet-extensions/src/noir-wallet/index.ts new file mode 100644 index 0000000..f643d65 --- /dev/null +++ b/packages/wallet-extensions/src/noir-wallet/index.ts @@ -0,0 +1,90 @@ +// Registers WalletOption.NOIR_WALLET and the wallet_noir_wallet_* error codes +// before anything below reads them. +import "./register"; + +import { AssetValue, Chain, type GenericTransferParams, SwapKitError, WalletOption } from "@swapkit/helpers"; +import { createWallet, getWalletSupportedChains } from "@swapkit/wallet-core"; +import type { NoirWalletZcashProvider } from "../types"; +import type { ExtensionWallet } from "../walletTypes"; + +export function getNoirWalletZcashProvider(): NoirWalletZcashProvider { + const provider = window.noirwallet?.zcash; + + if (!provider) { + throw new SwapKitError("wallet_noir_wallet_not_found"); + } + + return provider; +} + +export const noirWallet: ExtensionWallet<"connectNoirWallet"> = createWallet({ + connect: ({ addChain, walletType }) => + async function connectNoirWallet(_chains: Chain[]) { + const provider = getNoirWalletZcashProvider(); + + // Opens the extension's connect-approval popup and returns the primary + // account's addresses. + const account = await provider.request({ method: "zcash_requestAccounts" }); + + if (!account?.transparent) { + throw new SwapKitError("wallet_noir_wallet_connection_failed"); + } + + const { getUtxoToolbox } = await import("@swapkit/toolboxes/utxo"); + const toolbox = await getUtxoToolbox(Chain.Zcash); + + addChain({ + ...toolbox, + address: account.transparent, + chain: Chain.Zcash, + /** + * Noir Wallet spends from the shielded pool, so the spendable balance + * comes from the wallet itself rather than the transparent address' + * UTXO set. + */ + getBalance: async () => { + const balance = await provider.request({ method: "zcash_getBalance" }); + const value = balance?.available ?? balance?.spendable ?? balance?.total ?? "0"; + + return [AssetValue.from({ chain: Chain.Zcash, value })]; + }, + signMessage: async (message: string) => { + const { signature } = await provider.request({ method: "zcash_signMessage", params: [message, {}] }); + + return signature; + }, + /** + * Transaction building, fee selection and signing happen inside the + * extension. Memos are only supported towards shielded recipients, + * which rules out OP_RETURN-based routes (e.g. Maya/THORChain) — + * deposit-address routes such as NEAR Intents work. + */ + transfer: ({ recipient, assetValue, memo }: GenericTransferParams) => { + if (memo) { + throw new SwapKitError("wallet_noir_wallet_memo_not_supported"); + } + + if (!(recipient && assetValue)) { + throw new SwapKitError("wallet_missing_params", { params: { assetValue, recipient } }); + } + + return provider.request({ + method: "zcash_sendTransaction", + params: [{ amount: assetValue.getValue("string"), to: recipient }], + }); + }, + walletType, + }); + + return true; + }, + // Raw-sign RPC is not exposed by the extension; transactions are built and + // signed inside the wallet. + directSigningSupport: {}, + name: "connectNoirWallet", + supportedChains: [Chain.Zcash], + walletType: WalletOption.NOIR_WALLET, +}); + +export const NOIR_WALLET_SUPPORTED_CHAINS = getWalletSupportedChains(noirWallet); +export type NoirWalletSupportedChain = (typeof NOIR_WALLET_SUPPORTED_CHAINS)[number]; diff --git a/packages/wallet-extensions/src/noir-wallet/register.ts b/packages/wallet-extensions/src/noir-wallet/register.ts new file mode 100644 index 0000000..231371b --- /dev/null +++ b/packages/wallet-extensions/src/noir-wallet/register.ts @@ -0,0 +1,19 @@ +import { registerErrorCodes, registerWalletOption } from "@swapkit/helpers"; + +declare module "@swapkit/helpers" { + interface WalletOptionRegistry { + NOIR_WALLET: "NOIR_WALLET"; + } + interface SwapKitErrorRegistry { + wallet_noir_wallet_not_found: 80101; + wallet_noir_wallet_connection_failed: 80102; + wallet_noir_wallet_memo_not_supported: 80103; + } +} + +registerWalletOption("NOIR_WALLET", "NOIR_WALLET"); +registerErrorCodes({ + wallet_noir_wallet_connection_failed: 80102, + wallet_noir_wallet_memo_not_supported: 80103, + wallet_noir_wallet_not_found: 80101, +}); diff --git a/packages/wallet-extensions/src/types.ts b/packages/wallet-extensions/src/types.ts index 9b370c7..f6adf53 100644 --- a/packages/wallet-extensions/src/types.ts +++ b/packages/wallet-extensions/src/types.ts @@ -25,6 +25,13 @@ export type VultisigCosmosProvider = { request(request: { method: string; params?: any[] | Record }, callback?: Callback): Promise; }; +export type NoirWalletZcashProvider = { + request(request: { method: string; params?: any[] }): Promise; + on(event: string, handler: (...args: any[]) => void): void; + removeListener?(event: string, handler: (...args: any[]) => void): void; + disconnect(): Promise; +}; + type CtrlInjectedProviders = { binance: Eip1193Provider; bitcoin: Eip1193Provider; @@ -55,6 +62,8 @@ declare global { ctrl?: CtrlInjectedProviders; xfi?: CtrlInjectedProviders; + noirwallet?: { isNoirWallet: boolean; version?: string; zcash: NoirWalletZcashProvider }; + vultisig?: { bitcoin: Eip1193Provider; bitcoincash: Eip1193Provider; diff --git a/packages/wallet-hardware/package.json b/packages/wallet-hardware/package.json index 5165073..dff509f 100644 --- a/packages/wallet-hardware/package.json +++ b/packages/wallet-hardware/package.json @@ -22,10 +22,10 @@ "@near-js/transactions": "~2.5.0", "@scure/base": "2.2.0", "@scure/bip32": "2.2.0", - "@swapkit/helpers": "^5.0.2", - "@swapkit/toolboxes": "^5.1.1", + "@swapkit/helpers": "^5.1.0", + "@swapkit/toolboxes": "^5.1.2", "@swapkit/utxo-signer": "^3.0.0", - "@swapkit/wallet-core": "^5.0.0", + "@swapkit/wallet-core": "^5.0.3", "@trezor/connect-web": "~9.7.3", "cosmjs-types": "~0.10.1", "ethers": "^6.14.0", diff --git a/packages/wallet-mobile/package.json b/packages/wallet-mobile/package.json index 7a69479..50b7df3 100644 --- a/packages/wallet-mobile/package.json +++ b/packages/wallet-mobile/package.json @@ -1,9 +1,9 @@ { "author": "swapkit", "dependencies": { - "@swapkit/helpers": "^5.0.2", - "@swapkit/toolboxes": "^5.1.1", - "@swapkit/wallet-core": "^5.0.0" + "@swapkit/helpers": "^5.1.0", + "@swapkit/toolboxes": "^5.1.2", + "@swapkit/wallet-core": "^5.0.3" }, "description": "SwapKit - Wallet Mobile", "exports": { diff --git a/packages/wallets/package.json b/packages/wallets/package.json index ed358e0..b13e759 100644 --- a/packages/wallets/package.json +++ b/packages/wallets/package.json @@ -16,10 +16,10 @@ "@scure/base": "~2.2.0", "@scure/bip39": "~2.2.0", "@solana/web3.js": "~1.98.4", - "@swapkit/helpers": "^5.0.2", - "@swapkit/toolboxes": "^5.1.1", + "@swapkit/helpers": "^5.1.0", + "@swapkit/toolboxes": "^5.1.2", "@swapkit/utxo-signer": "^3.0.0", - "@swapkit/wallet-core": "^5.0.0", + "@swapkit/wallet-core": "^5.0.3", "@swapkit/wallet-extensions": "workspace:*", "@swapkit/wallet-hardware": "workspace:*", "@ton/core": "~0.63.1", @@ -182,6 +182,12 @@ "require": "./dist/src/radix/index.cjs", "types": "./dist/types/radix/index.d.ts" }, + "./register": { + "bun": "./src/register.ts", + "default": "./dist/src/register.js", + "require": "./dist/src/register.cjs", + "types": "./dist/types/register.d.ts" + }, "./talisman": { "bun": "./src/talisman.ts", "default": "./dist/src/talisman.js", diff --git a/packages/wallets/src/register.ts b/packages/wallets/src/register.ts new file mode 100644 index 0000000..f6d2ea0 --- /dev/null +++ b/packages/wallets/src/register.ts @@ -0,0 +1,15 @@ +/** + * Side-effect entry that registers every out-of-enum wallet option (and its + * error codes) with the @swapkit/helpers registries. Apps that reference + * registered options at module scope (e.g. `WalletOption.NOIR_WALLET` in a + * static wallet list) must import this before those modules evaluate: + * + * ```ts + * import "@swapkit/wallets/register"; + * ``` + * + * `loadWallet` imports this itself, so apps that only connect wallets through + * it need nothing extra. + */ +import "@swapkit/wallet-extensions/noir-wallet/register"; +import "./tonconnect/register"; diff --git a/packages/wallets/src/tonconnect/index.ts b/packages/wallets/src/tonconnect/index.ts index 7de3b08..ecc446c 100644 --- a/packages/wallets/src/tonconnect/index.ts +++ b/packages/wallets/src/tonconnect/index.ts @@ -1,7 +1,9 @@ -import { Chain, filterSupportedChains, type WalletOption } from "@swapkit/helpers"; +// Registers WalletOption.TON_CONNECT before anything below reads it. +import "./register"; + +import { Chain, filterSupportedChains, WalletOption } from "@swapkit/helpers"; import { createWallet, getWalletSupportedChains } from "@swapkit/wallet-core"; import { getWalletForChain } from "./helpers"; -import { TON_CONNECT } from "./option"; import type { TonConnectConfig } from "./types"; import { connectTonConnect, getTonConnectInstance } from "./walletMethods"; @@ -22,7 +24,7 @@ export const tonconnectWallet = createWallet({ balance: [], chain, disconnect: () => tonConnectUI.disconnect(), - walletType: TON_CONNECT, + walletType: WalletOption.TON_CONNECT, }); }); @@ -32,10 +34,7 @@ export const tonconnectWallet = createWallet({ directSigningSupport: { [Chain.Ton]: true }, name: "connectTonConnect", supportedChains: [Chain.Ton], - // createWallet constrains walletType to the WalletOption enum, which has no - // TON_CONNECT member until the registry lands upstream (swapkit/sdk#346) — - // the one sanctioned widening; runtime value is the same literal either way. - walletType: TON_CONNECT as unknown as WalletOption, + walletType: WalletOption.TON_CONNECT, }); export const TON_CONNECT_SUPPORTED_CHAINS = getWalletSupportedChains(tonconnectWallet); diff --git a/packages/wallets/src/tonconnect/option.ts b/packages/wallets/src/tonconnect/option.ts deleted file mode 100644 index 7c50aee..0000000 --- a/packages/wallets/src/tonconnect/option.ts +++ /dev/null @@ -1,6 +0,0 @@ -// @swapkit/helpers 5.0.0 shipped without the extensible WalletOption registry -// (swapkit/sdk#346 is unmerged), so TON_CONNECT lives outside the enum until a -// helpers release carries the registry again. Everything except the -// createWallet call site uses this literal type directly. -export const TON_CONNECT = "TON_CONNECT" as const; -export type TonConnectOption = typeof TON_CONNECT; diff --git a/packages/wallets/src/tonconnect/register.ts b/packages/wallets/src/tonconnect/register.ts new file mode 100644 index 0000000..ae4f932 --- /dev/null +++ b/packages/wallets/src/tonconnect/register.ts @@ -0,0 +1,9 @@ +import { registerWalletOption } from "@swapkit/helpers"; + +declare module "@swapkit/helpers" { + interface WalletOptionRegistry { + TON_CONNECT: "TON_CONNECT"; + } +} + +registerWalletOption("TON_CONNECT", "TON_CONNECT"); diff --git a/packages/wallets/src/types.ts b/packages/wallets/src/types.ts index 76755d8..8e782d2 100644 --- a/packages/wallets/src/types.ts +++ b/packages/wallets/src/types.ts @@ -5,6 +5,7 @@ import type { ctrlWallet } from "@swapkit/wallet-extensions/ctrl"; import type { evmWallet } from "@swapkit/wallet-extensions/evm-extensions"; import type { keepkeyBexWallet } from "@swapkit/wallet-extensions/keepkey-bex"; import type { keplrWallet } from "@swapkit/wallet-extensions/keplr"; +import type { noirWallet } from "@swapkit/wallet-extensions/noir-wallet"; import type { okxWallet } from "@swapkit/wallet-extensions/okx"; import type { onekeyWallet } from "@swapkit/wallet-extensions/onekey"; import type { petraWallet } from "@swapkit/wallet-extensions/petra"; @@ -43,6 +44,7 @@ export type SKWallets = { [WalletOption.LEAP]: typeof keplrWallet; [WalletOption.LEDGER]: typeof ledgerWallet; [WalletOption.METAMASK]: typeof metamaskWallet; + [WalletOption.NOIR_WALLET]: typeof noirWallet; [WalletOption.OKX]: typeof okxWallet; [WalletOption.OKX_MOBILE]: typeof evmWallet; [WalletOption.ONEKEY]: typeof onekeyWallet; @@ -53,7 +55,7 @@ export type SKWallets = { [WalletOption.POLKADOT_JS]: typeof polkadotWallet; [WalletOption.RADIX_WALLET]: typeof radixWallet; [WalletOption.TALISMAN]: typeof talismanWallet; - TON_CONNECT: typeof tonconnectWallet; + [WalletOption.TON_CONNECT]: typeof tonconnectWallet; [WalletOption.TREZOR]: typeof trezorWallet; [WalletOption.TRONLINK]: typeof tronlinkWallet; [WalletOption.TRUSTWALLET_WEB]: typeof trustwalletWallet; @@ -120,6 +122,7 @@ export type SKWalletsSupportedChains = { [WalletOption.LEAP]: typeof keplrWallet.connectKeplr.supportedChains; [WalletOption.LEDGER]: typeof ledgerWallet.connectLedger.supportedChains; [WalletOption.METAMASK]: typeof metamaskWallet.connectMetamask.supportedChains; + [WalletOption.NOIR_WALLET]: typeof noirWallet.connectNoirWallet.supportedChains; [WalletOption.OKX]: typeof okxWallet.connectOkx.supportedChains; [WalletOption.OKX_MOBILE]: typeof evmWallet.connectEVMWallet.supportedChains; [WalletOption.ONEKEY]: typeof onekeyWallet.connectOnekeyWallet.supportedChains; @@ -130,7 +133,7 @@ export type SKWalletsSupportedChains = { [WalletOption.POLKADOT_JS]: typeof polkadotWallet.connectPolkadotJs.supportedChains; [WalletOption.RADIX_WALLET]: typeof radixWallet.connectRadixWallet.supportedChains; [WalletOption.TALISMAN]: typeof talismanWallet.connectTalisman.supportedChains; - TON_CONNECT: typeof tonconnectWallet.connectTonConnect.supportedChains; + [WalletOption.TON_CONNECT]: typeof tonconnectWallet.connectTonConnect.supportedChains; [WalletOption.TREZOR]: typeof trezorWallet.connectTrezor.supportedChains; [WalletOption.TRONLINK]: typeof tronlinkWallet.connectTronLink.supportedChains; [WalletOption.TRUSTWALLET_WEB]: typeof trustwalletWallet.connectTrustWallet.supportedChains; diff --git a/packages/wallets/src/utils.ts b/packages/wallets/src/utils.ts index a20019a..5289558 100644 --- a/packages/wallets/src/utils.ts +++ b/packages/wallets/src/utils.ts @@ -1,11 +1,13 @@ import { WalletOption } from "@swapkit/helpers"; -import { TON_CONNECT, type TonConnectOption } from "./tonconnect/option"; +// Registers the out-of-enum wallet options before the match below reads them; +// the connectors themselves still load lazily via their match arms. +import "./register"; import type { SKWallets } from "./types"; export async function loadWallet(walletOption: W): Promise { const { match } = await import("ts-pattern"); - const wallet = await match(walletOption as WalletOption | TonConnectOption) + const wallet = await match(walletOption as WalletOption) .with(WalletOption.COINBASE_MOBILE, async () => (await import("./coinbase")).coinbaseWallet) .with(WalletOption.BITGET, async () => (await import("@swapkit/wallet-extensions/bitget")).bitgetWallet) .with(WalletOption.CTRL, async () => (await import("@swapkit/wallet-extensions/ctrl")).ctrlWallet) @@ -46,12 +48,13 @@ export async function loadWallet(walletOption: W): Pr .with(WalletOption.LEDGER, async () => (await import("@swapkit/wallet-hardware/ledger")).ledgerWallet) .with(WalletOption.PASSKEYS, WalletOption.PASSKEY_WALLET, async () => (await import("./passkeys")).passkeysWallet) .with(WalletOption.PETRA, async () => (await import("@swapkit/wallet-extensions/petra")).petraWallet) + .with(WalletOption.NOIR_WALLET, async () => (await import("@swapkit/wallet-extensions/noir-wallet")).noirWallet) .with(WalletOption.PHANTOM, async () => (await import("@swapkit/wallet-extensions/phantom")).phantomWallet) .with(WalletOption.POLKADOT_JS, async () => (await import("@swapkit/wallet-extensions/polkadotjs")).polkadotWallet) .with(WalletOption.RADIX_WALLET, async () => (await import("./radix")).radixWallet) .with(WalletOption.TALISMAN, async () => (await import("@swapkit/wallet-extensions/talisman")).talismanWallet) .with(WalletOption.TRONLINK, async () => (await import("@swapkit/wallet-extensions/tronlink")).tronlinkWallet) - .with(TON_CONNECT, async () => (await import("./tonconnect")).tonconnectWallet) + .with(WalletOption.TON_CONNECT, async () => (await import("./tonconnect")).tonconnectWallet) .with(WalletOption.WALLET_SELECTOR, async () => (await import("./near-wallet-selector")).walletSelectorWallet) .with(WalletOption.XAMAN, async () => (await import("./xaman")).xamanWallet) .exhaustive(); diff --git a/packages/wallets/test/tonconnect.test.ts b/packages/wallets/test/tonconnect.test.ts index a62f97f..b9fe489 100644 --- a/packages/wallets/test/tonconnect.test.ts +++ b/packages/wallets/test/tonconnect.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from "bun:test"; -import { SwapKitError } from "@swapkit/helpers"; +import { SwapKitError, WalletOption } from "@swapkit/helpers"; import { Address, beginCell } from "@ton/core"; import type { TonConnectUI } from "@tonconnect/ui"; -import { TON_CONNECT } from "../src/tonconnect/option"; +import "../src/tonconnect/register"; import { sendTonConnectTransaction, toFriendlyDestination } from "../src/tonconnect/walletMethods"; const RAW_ADDRESS = "0:83dfd552e63729b472fcbcc8c45ebcc6691702558b68ec7527e1ba403a0f31a8"; @@ -22,8 +22,8 @@ function makeFakeTonConnectUI(sentRequests: { address: string }[][]) { } describe("WalletOption", () => { - it("exposes the TON_CONNECT wallet option literal", () => { - expect(TON_CONNECT).toBe("TON_CONNECT"); + it("registers TON_CONNECT in the extensible WalletOption registry", () => { + expect(WalletOption.TON_CONNECT).toBe("TON_CONNECT"); }); });