From 6faf09499d652082db41aa52492f19dc8ba7d129 Mon Sep 17 00:00:00 2001 From: steveluscher Date: Wed, 15 May 2024 19:01:02 +0000 Subject: [PATCH] =?UTF-8?q?A=20wallet=20registry=20that=20vends=20?= =?UTF-8?q?=E2=80=98handles=E2=80=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/sharp-lions-cover.md | 7 + .../src/__typetests__/error-typetest.ts | 42 +-- packages/core/errors/src/codes.ts | 9 +- packages/core/errors/src/context.ts | 14 +- packages/core/errors/src/messages.ts | 11 +- packages/ui/_/.gitignore | 1 + packages/ui/_/CHANGELOG.md | 1 + packages/ui/_/LICENSE | 202 +++++++++++++ packages/ui/_/README.md | 3 + packages/ui/_/package.json | 39 +++ packages/ui/_/src/index.ts | 2 + packages/ui/_/tsconfig.all.json | 17 ++ packages/ui/_/tsconfig.cjs.json | 7 + packages/ui/_/tsconfig.esm.json | 8 + packages/ui/compare/.gitignore | 1 + packages/ui/compare/CHANGELOG.md | 1 + packages/ui/compare/LICENSE | 202 +++++++++++++ packages/ui/compare/README.md | 49 ++++ packages/ui/compare/package.json | 41 +++ .../ui/compare/src/__tests__/compare-test.ts | 101 +++++++ .../compare/src/__tests__/storage-key-test.ts | 32 +++ .../ui/compare/src/__tests__/tsconfig.json | 6 + packages/ui/compare/src/compare.ts | 40 +++ packages/ui/compare/src/index.ts | 2 + packages/ui/compare/src/storage-key.ts | 13 + packages/ui/compare/tsconfig.all.json | 17 ++ packages/ui/compare/tsconfig.cjs.json | 8 + packages/ui/compare/tsconfig.esm.json | 9 + packages/ui/core/.gitignore | 1 + packages/ui/core/CHANGELOG.md | 1 + packages/ui/core/LICENSE | 202 +++++++++++++ packages/ui/core/README.md | 23 ++ packages/ui/core/package.json | 38 +++ packages/ui/core/src/UiWallet.ts | 19 ++ packages/ui/core/src/UiWalletAccount.ts | 14 + packages/ui/core/src/UiWalletHandle.ts | 6 + packages/ui/core/src/index.ts | 3 + packages/ui/core/tsconfig.all.json | 11 + packages/ui/core/tsconfig.cjs.json | 8 + packages/ui/core/tsconfig.esm.json | 9 + packages/ui/registry/.gitignore | 1 + packages/ui/registry/CHANGELOG.md | 1 + packages/ui/registry/LICENSE | 202 +++++++++++++ packages/ui/registry/README.md | 1 + packages/ui/registry/package.json | 42 +++ ...egistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ts | 71 +++++ ...egistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ts | 69 +++++ ...egistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ts | 105 +++++++ .../ui/registry/src/__tests__/tsconfig.json | 6 + ...ry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED-test.ts | 272 ++++++++++++++++++ ...ry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED-test.ts | 29 ++ ...ry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED-test.ts | 190 ++++++++++++ packages/ui/registry/src/compare.ts | 17 ++ packages/ui/registry/src/index.ts | 3 + packages/ui/registry/tsconfig.all.json | 14 + packages/ui/registry/tsconfig.cjs.json | 8 + packages/ui/registry/tsconfig.esm.json | 9 + pnpm-lock.yaml | 67 ++++- tsconfig.all.json | 12 + 59 files changed, 2297 insertions(+), 42 deletions(-) create mode 100644 .changeset/sharp-lions-cover.md create mode 100644 packages/ui/_/.gitignore create mode 100644 packages/ui/_/CHANGELOG.md create mode 100644 packages/ui/_/LICENSE create mode 100644 packages/ui/_/README.md create mode 100644 packages/ui/_/package.json create mode 100644 packages/ui/_/src/index.ts create mode 100644 packages/ui/_/tsconfig.all.json create mode 100644 packages/ui/_/tsconfig.cjs.json create mode 100644 packages/ui/_/tsconfig.esm.json create mode 100644 packages/ui/compare/.gitignore create mode 100644 packages/ui/compare/CHANGELOG.md create mode 100644 packages/ui/compare/LICENSE create mode 100644 packages/ui/compare/README.md create mode 100644 packages/ui/compare/package.json create mode 100644 packages/ui/compare/src/__tests__/compare-test.ts create mode 100644 packages/ui/compare/src/__tests__/storage-key-test.ts create mode 100644 packages/ui/compare/src/__tests__/tsconfig.json create mode 100644 packages/ui/compare/src/compare.ts create mode 100644 packages/ui/compare/src/index.ts create mode 100644 packages/ui/compare/src/storage-key.ts create mode 100644 packages/ui/compare/tsconfig.all.json create mode 100644 packages/ui/compare/tsconfig.cjs.json create mode 100644 packages/ui/compare/tsconfig.esm.json create mode 100644 packages/ui/core/.gitignore create mode 100644 packages/ui/core/CHANGELOG.md create mode 100644 packages/ui/core/LICENSE create mode 100644 packages/ui/core/README.md create mode 100644 packages/ui/core/package.json create mode 100644 packages/ui/core/src/UiWallet.ts create mode 100644 packages/ui/core/src/UiWalletAccount.ts create mode 100644 packages/ui/core/src/UiWalletHandle.ts create mode 100644 packages/ui/core/src/index.ts create mode 100644 packages/ui/core/tsconfig.all.json create mode 100644 packages/ui/core/tsconfig.cjs.json create mode 100644 packages/ui/core/tsconfig.esm.json create mode 100644 packages/ui/registry/.gitignore create mode 100644 packages/ui/registry/CHANGELOG.md create mode 100644 packages/ui/registry/LICENSE create mode 100644 packages/ui/registry/README.md create mode 100644 packages/ui/registry/package.json create mode 100644 packages/ui/registry/src/UiWalletAccountRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ts create mode 100644 packages/ui/registry/src/UiWalletHandleRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ts create mode 100644 packages/ui/registry/src/UiWalletRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ts create mode 100644 packages/ui/registry/src/__tests__/tsconfig.json create mode 100644 packages/ui/registry/src/__tests__/uiWalletAccountRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED-test.ts create mode 100644 packages/ui/registry/src/__tests__/uiWalletHandleRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED-test.ts create mode 100644 packages/ui/registry/src/__tests__/uiWalletRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED-test.ts create mode 100644 packages/ui/registry/src/compare.ts create mode 100644 packages/ui/registry/src/index.ts create mode 100644 packages/ui/registry/tsconfig.all.json create mode 100644 packages/ui/registry/tsconfig.cjs.json create mode 100644 packages/ui/registry/tsconfig.esm.json diff --git a/.changeset/sharp-lions-cover.md b/.changeset/sharp-lions-cover.md new file mode 100644 index 00000000..c0c4cac8 --- /dev/null +++ b/.changeset/sharp-lions-cover.md @@ -0,0 +1,7 @@ +--- +'@wallet-standard/errors': patch +'@wallet-standard/ui-core': patch +'@wallet-standard/core': major +--- + +Introduced the `UiWallet` and `UiWalletAccount` data structures. These act both as descriptions of Wallet Standard wallets and accounts, as well as ‘handles’ to the underlying `Wallet` and `WalletAccount` objects in the registry. diff --git a/packages/core/errors/src/__typetests__/error-typetest.ts b/packages/core/errors/src/__typetests__/error-typetest.ts index c48710cc..d3ae846f 100644 --- a/packages/core/errors/src/__typetests__/error-typetest.ts +++ b/packages/core/errors/src/__typetests__/error-typetest.ts @@ -3,46 +3,48 @@ import type { WalletStandardErrorCode } from '../codes.js'; import type { WalletStandardErrorContext } from '../context.js'; import { isWalletStandardError, WalletStandardError } from '../error.js'; -const { WALLET_STANDARD_ERROR__PLACEHOLDER } = WalletStandardErrorCodeModule; +const { WALLET_STANDARD_ERROR__REGISTRY__WALLET_ACCOUNT_NOT_FOUND, WALLET_STANDARD_ERROR__REGISTRY__WALLET_NOT_FOUND } = + WalletStandardErrorCodeModule; // If this line raises a type error, you might have forgotten to add a new error to the // `WalletStandardErrorCode` union in `src/codes.ts`. Object.values(WalletStandardErrorCodeModule) satisfies WalletStandardErrorCode[]; -const walletStandardPlaceholderError = new WalletStandardError(WALLET_STANDARD_ERROR__PLACEHOLDER, { - just: '!', - here: '!', - until: '!', - the: '!', - first: '!', - error: '!', - gets: '!', - created: '!', +const walletAccountNotFoundError = new WalletStandardError(WALLET_STANDARD_ERROR__REGISTRY__WALLET_ACCOUNT_NOT_FOUND, { + address: 'abc', + walletName: 'Mock Wallet', }); { - const code = walletStandardPlaceholderError.context.__code; - code satisfies typeof WALLET_STANDARD_ERROR__PLACEHOLDER; + const code = walletAccountNotFoundError.context.__code; + code satisfies typeof WALLET_STANDARD_ERROR__REGISTRY__WALLET_ACCOUNT_NOT_FOUND; + // @ts-expect-error Wrong error code. + code satisfies typeof WALLET_STANDARD_ERROR__REGISTRY__WALLET_NOT_FOUND; } -walletStandardPlaceholderError.context satisfies WalletStandardErrorContext[typeof WALLET_STANDARD_ERROR__PLACEHOLDER]; +walletAccountNotFoundError.context satisfies WalletStandardErrorContext[typeof WALLET_STANDARD_ERROR__REGISTRY__WALLET_ACCOUNT_NOT_FOUND]; // @ts-expect-error Non existent context property. -walletStandardPlaceholderError.context.chains; +walletAccountNotFoundError.context.chains; -// @ts-expect-error Missing context -new WalletStandardError(WALLET_STANDARD_ERROR__PLACEHOLDER); +new WalletStandardError(WALLET_STANDARD_ERROR__REGISTRY__WALLET_NOT_FOUND); +// @ts-expect-error Missing context property (`address` and `walletName`) +new WalletStandardError(WALLET_STANDARD_ERROR__REGISTRY__WALLET_ACCOUNT_NOT_FOUND); const unknownError = null as unknown as WalletStandardError; -if (unknownError.context.__code === WALLET_STANDARD_ERROR__PLACEHOLDER) { - unknownError.context satisfies WalletStandardErrorContext[typeof WALLET_STANDARD_ERROR__PLACEHOLDER]; +if (unknownError.context.__code === WALLET_STANDARD_ERROR__REGISTRY__WALLET_ACCOUNT_NOT_FOUND) { + unknownError.context satisfies WalletStandardErrorContext[typeof WALLET_STANDARD_ERROR__REGISTRY__WALLET_ACCOUNT_NOT_FOUND]; + // @ts-expect-error Context belongs to another error code + unknownError.context satisfies WalletStandardErrorContext[typeof WALLET_STANDARD_ERROR__REGISTRY__WALLET_NOT_FOUND]; } const e = null as unknown; if (isWalletStandardError(e)) { e.context satisfies Readonly<{ __code: WalletStandardErrorCode }>; } -if (isWalletStandardError(e, WALLET_STANDARD_ERROR__PLACEHOLDER)) { - e.context satisfies WalletStandardErrorContext[typeof WALLET_STANDARD_ERROR__PLACEHOLDER]; +if (isWalletStandardError(e, WALLET_STANDARD_ERROR__REGISTRY__WALLET_ACCOUNT_NOT_FOUND)) { + e.context satisfies WalletStandardErrorContext[typeof WALLET_STANDARD_ERROR__REGISTRY__WALLET_ACCOUNT_NOT_FOUND]; + // @ts-expect-error Context belongs to another error code + e.context satisfies WalletStandardErrorContext[typeof WALLET_STANDARD_ERROR__REGISTRY__WALLET_NOT_FOUND]; } // `WalletStandardErrorContext` must not contain any keys reserved by `ErrorOptions` (eg. `cause`) diff --git a/packages/core/errors/src/codes.ts b/packages/core/errors/src/codes.ts index 535a006c..ed9b83c1 100644 --- a/packages/core/errors/src/codes.ts +++ b/packages/core/errors/src/codes.ts @@ -24,7 +24,10 @@ * - `_UNIMPLEMENTED`: Some required component is not available in the environment. E.g. `SUBTLE_CRYPTO_VERIFY_FUNCTION_UNIMPLEMENTED`. */ -export const WALLET_STANDARD_ERROR__PLACEHOLDER = 0 as const; +// Registry-related errors. +// Reserve error codes in the range [3834000-3834999]. +export const WALLET_STANDARD_ERROR__REGISTRY__WALLET_NOT_FOUND = 3834000 as const; +export const WALLET_STANDARD_ERROR__REGISTRY__WALLET_ACCOUNT_NOT_FOUND = 3834001 as const; /** * A union of every Wallet Standard error code @@ -42,4 +45,6 @@ export const WALLET_STANDARD_ERROR__PLACEHOLDER = 0 as const; * `@wallet-standard/errors` is not safe, for a variety of reasons covered here: * https://stackoverflow.com/a/28818850 */ -export type WalletStandardErrorCode = typeof WALLET_STANDARD_ERROR__PLACEHOLDER; +export type WalletStandardErrorCode = + | typeof WALLET_STANDARD_ERROR__REGISTRY__WALLET_ACCOUNT_NOT_FOUND + | typeof WALLET_STANDARD_ERROR__REGISTRY__WALLET_NOT_FOUND; diff --git a/packages/core/errors/src/context.ts b/packages/core/errors/src/context.ts index 5bdd63b5..bc0300cc 100644 --- a/packages/core/errors/src/context.ts +++ b/packages/core/errors/src/context.ts @@ -1,4 +1,4 @@ -import type { WALLET_STANDARD_ERROR__PLACEHOLDER, WalletStandardErrorCode } from './codes.js'; +import type { WALLET_STANDARD_ERROR__REGISTRY__WALLET_ACCOUNT_NOT_FOUND, WalletStandardErrorCode } from './codes.js'; type DefaultUnspecifiedErrorContextToUndefined = { [P in WalletStandardErrorCode]: P extends keyof T ? T[P] : undefined; @@ -12,15 +12,9 @@ type DefaultUnspecifiedErrorContextToUndefined = { * - Don't change or remove members of an error's context. */ export type WalletStandardErrorContext = DefaultUnspecifiedErrorContextToUndefined<{ - [WALLET_STANDARD_ERROR__PLACEHOLDER]: { - just: unknown; - here: unknown; - until: unknown; - the: unknown; - first: unknown; - error: unknown; - gets: unknown; - created: unknown; + [WALLET_STANDARD_ERROR__REGISTRY__WALLET_ACCOUNT_NOT_FOUND]: { + address: string; + walletName: string; }; }>; diff --git a/packages/core/errors/src/messages.ts b/packages/core/errors/src/messages.ts index f115c0cd..90dd4073 100644 --- a/packages/core/errors/src/messages.ts +++ b/packages/core/errors/src/messages.ts @@ -1,5 +1,8 @@ import type { WalletStandardErrorCode } from './codes.js'; -import { WALLET_STANDARD_ERROR__PLACEHOLDER } from './codes.js'; +import { + WALLET_STANDARD_ERROR__REGISTRY__WALLET_ACCOUNT_NOT_FOUND, + WALLET_STANDARD_ERROR__REGISTRY__WALLET_NOT_FOUND, +} from './codes.js'; /** * To add a new error, follow the instructions at @@ -13,5 +16,9 @@ export const WalletStandardErrorMessages: Readonly<{ // TypeScript will fail to build this project if add an error code without a message. [P in WalletStandardErrorCode]: string; }> = { - [WALLET_STANDARD_ERROR__PLACEHOLDER]: 'Just here until the first error gets created', + [WALLET_STANDARD_ERROR__REGISTRY__WALLET_ACCOUNT_NOT_FOUND]: + "No account with address $address could be found in the '$walletName' wallet", + [WALLET_STANDARD_ERROR__REGISTRY__WALLET_NOT_FOUND]: + 'No underlying Wallet Standard wallet could be found for this handle. This can happen if ' + + 'the wallet associated with the handle has been unregistered.', }; diff --git a/packages/ui/_/.gitignore b/packages/ui/_/.gitignore new file mode 100644 index 00000000..a65b4177 --- /dev/null +++ b/packages/ui/_/.gitignore @@ -0,0 +1 @@ +lib diff --git a/packages/ui/_/CHANGELOG.md b/packages/ui/_/CHANGELOG.md new file mode 100644 index 00000000..26455953 --- /dev/null +++ b/packages/ui/_/CHANGELOG.md @@ -0,0 +1 @@ +# @wallet-standard/ui diff --git a/packages/ui/_/LICENSE b/packages/ui/_/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/packages/ui/_/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/ui/_/README.md b/packages/ui/_/README.md new file mode 100644 index 00000000..16a3dd66 --- /dev/null +++ b/packages/ui/_/README.md @@ -0,0 +1,3 @@ +# `@wallet-standard/ui` + +This package includes a set of primitives for building UI libraries that use Wallet Standard wallets and accounts. UI library authors are expected to use the registry to create `UiWallet` and `UiWalletAccount` objects for use in client applications. diff --git a/packages/ui/_/package.json b/packages/ui/_/package.json new file mode 100644 index 00000000..8bb98de1 --- /dev/null +++ b/packages/ui/_/package.json @@ -0,0 +1,39 @@ +{ + "name": "@wallet-standard/ui", + "version": "0.0.0", + "author": "Solana Maintainers ", + "repository": "https://github.com/wallet-standard/wallet-standard", + "license": "Apache-2.0", + "publishConfig": { + "access": "public" + }, + "files": [ + "lib", + "src", + "LICENSE" + ], + "engines": { + "node": ">=16" + }, + "type": "module", + "sideEffects": false, + "main": "./lib/cjs/index.js", + "module": "./lib/esm/index.js", + "types": "./lib/types/index.d.ts", + "exports": { + "require": "./lib/cjs/index.js", + "import": "./lib/esm/index.js", + "types": "./lib/types/index.d.ts" + }, + "scripts": { + "clean": "shx mkdir -p lib && shx rm -rf lib", + "package": "shx mkdir -p lib/cjs && shx echo '{ \"type\": \"commonjs\" }' > lib/cjs/package.json" + }, + "dependencies": { + "@wallet-standard/ui-compare": "workspace:^", + "@wallet-standard/ui-core": "workspace:^" + }, + "devDependencies": { + "shx": "^0.3.4" + } +} diff --git a/packages/ui/_/src/index.ts b/packages/ui/_/src/index.ts new file mode 100644 index 00000000..8df2db0c --- /dev/null +++ b/packages/ui/_/src/index.ts @@ -0,0 +1,2 @@ +export * from '@wallet-standard/ui-compare'; +export * from '@wallet-standard/ui-core'; diff --git a/packages/ui/_/tsconfig.all.json b/packages/ui/_/tsconfig.all.json new file mode 100644 index 00000000..2749d7fe --- /dev/null +++ b/packages/ui/_/tsconfig.all.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../tsconfig.root.json", + "references": [ + { + "path": "../core/tsconfig.all.json" + }, + { + "path": "../compare/tsconfig.all.json" + }, + { + "path": "./tsconfig.cjs.json" + }, + { + "path": "./tsconfig.esm.json" + } + ] +} diff --git a/packages/ui/_/tsconfig.cjs.json b/packages/ui/_/tsconfig.cjs.json new file mode 100644 index 00000000..099b9aa7 --- /dev/null +++ b/packages/ui/_/tsconfig.cjs.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../tsconfig.cjs.json", + "include": ["src"], + "compilerOptions": { + "outDir": "lib/cjs" + } +} diff --git a/packages/ui/_/tsconfig.esm.json b/packages/ui/_/tsconfig.esm.json new file mode 100644 index 00000000..4900d2fa --- /dev/null +++ b/packages/ui/_/tsconfig.esm.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.esm.json", + "include": ["src"], + "compilerOptions": { + "outDir": "lib/esm", + "declarationDir": "lib/types" + } +} diff --git a/packages/ui/compare/.gitignore b/packages/ui/compare/.gitignore new file mode 100644 index 00000000..a65b4177 --- /dev/null +++ b/packages/ui/compare/.gitignore @@ -0,0 +1 @@ +lib diff --git a/packages/ui/compare/CHANGELOG.md b/packages/ui/compare/CHANGELOG.md new file mode 100644 index 00000000..cd511f5e --- /dev/null +++ b/packages/ui/compare/CHANGELOG.md @@ -0,0 +1 @@ +# @wallet-standard/ui-compare diff --git a/packages/ui/compare/LICENSE b/packages/ui/compare/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/packages/ui/compare/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/ui/compare/README.md b/packages/ui/compare/README.md new file mode 100644 index 00000000..a6c8781a --- /dev/null +++ b/packages/ui/compare/README.md @@ -0,0 +1,49 @@ +# `@wallet-standard/ui-compare` + +This package includes types and functions useful for comparing wallet accounts to determine if they represent the same account in the same wallet. + +## Functions + +### `getUiWalletAccountStorageKey()` + +Produces a stable string that can be used to uniquely identify a `UiWalletAccount`. + +You can use this to identify a list item when rendering a list of accounts, or as a way to store your app's last selected account as a string in browser storage. + +```ts +// When selecting an account in the UI +function onSelectAccount(account: UiWalletAccount) { + window.localStorage.set('lastSelectedAccount', getUiWalletAccountStorageKey(account)); +} + +// Later, when reloading the app +const savedAccountKey = window.localStorage.get('lastSelectedAccount'); +const selectedAccount = wallets + .flatMap(({ accounts }) => accounts) + .find((account) => savedAccountKey === getUiWalletAccountStorageKey(account)); +``` + +### `uiWalletAccountsAreSame()` + +Given two `UiWalletAccount` objects, this method will tell you if they represent the same underlying `WalletAccount`. + +```ts +const previousSelectedWalletAccount = usePrev(selectedWalletAccount); +useEffect(() => { + if (!uiWalletsAccountsAreSame(selectedWalletAccount, previousSelectedWalletAccount)) { + console.log('A new account was selected!'); + } +}, [previousSelectedWalletAccount, selectedWalletAccount]); +``` + +`UiWalletAccount` objects are meant to be used in client apps to render UI; they are not the _actual_ underlying `WalletAccount` objects. In particular, they can change over time and you can not presume that two `UiWalletAccount` objects will be referentially equal – even though they represent the ‘same’ account. + +> [!WARNING] +> It is insufficient to compare two accounts on the basis of their addresses; it's possible for two different wallets to be configured with the same account. Use this method whenever you need to know for sure that two `UiWalletAccount` objects represent the same address _and_ belong to the same underlying `Wallet`. + +### `uiWalletAccountBelongsToUiWallet()` + +Given a `UiWalletAccount`, this method will tell you if the account belongs to a specific `UiWallet`. + +> [!WARNING] +> It's possible for two different wallets to be configured with the same account. Use this method whenever you need to know for sure that a `UiWalletAccount` belongs to a particular `UiWallet`. diff --git a/packages/ui/compare/package.json b/packages/ui/compare/package.json new file mode 100644 index 00000000..8637e4ea --- /dev/null +++ b/packages/ui/compare/package.json @@ -0,0 +1,41 @@ +{ + "name": "@wallet-standard/ui-compare", + "version": "0.0.0", + "author": "Solana Maintainers ", + "repository": "https://github.com/wallet-standard/wallet-standard", + "license": "Apache-2.0", + "publishConfig": { + "access": "public" + }, + "files": [ + "lib", + "src", + "LICENSE" + ], + "engines": { + "node": ">=16" + }, + "type": "module", + "sideEffects": false, + "main": "./lib/cjs/index.js", + "module": "./lib/esm/index.js", + "types": "./lib/types/index.d.ts", + "exports": { + "require": "./lib/cjs/index.js", + "import": "./lib/esm/index.js", + "types": "./lib/types/index.d.ts" + }, + "scripts": { + "clean": "shx mkdir -p lib && shx rm -rf lib", + "package": "shx mkdir -p lib/cjs && shx echo '{ \"type\": \"commonjs\" }' > lib/cjs/package.json", + "test": "jest -c ../../../node_modules/@wallet-standard/test-config/jest.config.ts --rootDir ." + }, + "dependencies": { + "@wallet-standard/base": "workspace:*", + "@wallet-standard/ui-core": "workspace:*", + "@wallet-standard/ui-registry": "workspace:*" + }, + "devDependencies": { + "shx": "^0.3.4" + } +} diff --git a/packages/ui/compare/src/__tests__/compare-test.ts b/packages/ui/compare/src/__tests__/compare-test.ts new file mode 100644 index 00000000..65372020 --- /dev/null +++ b/packages/ui/compare/src/__tests__/compare-test.ts @@ -0,0 +1,101 @@ +import type { Wallet, WalletAccount, WalletVersion } from '@wallet-standard/base'; +import type { UiWalletAccount, UiWallet } from '@wallet-standard/ui-core'; +import { + getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, + getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, +} from '@wallet-standard/ui-registry'; + +import { uiWalletAccountBelongsToUiWallet, uiWalletAccountsAreSame } from '../compare.js'; + +describe('uiWalletAccountsAreSame()', () => { + let mockUiWalletAccount: UiWalletAccount; + let mockWallet: Wallet; + let mockWalletAccount: WalletAccount; + beforeEach(() => { + mockWalletAccount = { + address: 'abc', + chains: [], + features: [], + publicKey: new Uint8Array([1, 2, 3]), + } as WalletAccount; + mockWallet = { + accounts: [], + chains: [], + features: {}, + icon: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEAAAAALAAAAAABAAEAAAIBAAA=', + name: 'Mock:Wallet', + version: '1.0.0' as WalletVersion, + }; + mockUiWalletAccount = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + }); + it('returns true for the same account object', () => { + expect(uiWalletAccountsAreSame(mockUiWalletAccount, mockUiWalletAccount)).toBe(true); + }); + it('returns true if the addresses and underlying `Wallet` match, despite the objects being different', () => { + const clonedMockUiWalletAccount = + getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet, { + ...mockWalletAccount, + chains: ['solana:danknet'], + } as WalletAccount); + expect(uiWalletAccountsAreSame(mockUiWalletAccount, clonedMockUiWalletAccount)).toBe(true); + }); + it('returns false if the addresses match but the underlying `Wallet` does not', () => { + const clonedMockUiWalletAccount = + getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + { ...mockWallet }, + mockWalletAccount + ); + expect(uiWalletAccountsAreSame(mockUiWalletAccount, clonedMockUiWalletAccount)).toBe(false); + }); + it('returns false if the addresses do not match even if the underlying `Wallet` does', () => { + const clonedMockUiWalletAccount = + getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet, { + ...mockWalletAccount, + address: 'xyz', + }); + expect(uiWalletAccountsAreSame(mockUiWalletAccount, clonedMockUiWalletAccount)).toBe(false); + }); +}); + +describe('uiWalletAccountBelongsToUiWallet()', () => { + let mockWalletAccount: WalletAccount; + let mockWallet: Wallet; + let mockUiWalletAccount: UiWalletAccount; + let mockUiWallet: UiWallet; + beforeEach(() => { + mockWalletAccount = { + address: 'abc', + chains: [], + features: [], + publicKey: new Uint8Array([1, 2, 3]), + }; + mockWallet = { + accounts: [], + chains: [], + features: {}, + icon: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEAAAAALAAAAAABAAEAAAIBAAA=', + name: 'Mock:Wallet', + version: '1.0.0' as WalletVersion, + }; + mockUiWalletAccount = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + mockUiWallet = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + }); + it('returns true if the UI wallet account belongs to the UI wallet', () => { + expect(uiWalletAccountBelongsToUiWallet(mockUiWalletAccount, mockUiWallet)).toBe(true); + }); + it('returns false if the UI wallet account does not belong to the UI wallet', () => { + const differentWallet = { ...mockWallet } as Wallet; /* different object */ + const identicalAccountInDifferentWallet = + getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + differentWallet, + mockWalletAccount + ); + expect(uiWalletAccountBelongsToUiWallet(identicalAccountInDifferentWallet, mockUiWallet)).toBe(false); + }); +}); diff --git a/packages/ui/compare/src/__tests__/storage-key-test.ts b/packages/ui/compare/src/__tests__/storage-key-test.ts new file mode 100644 index 00000000..4cade6e8 --- /dev/null +++ b/packages/ui/compare/src/__tests__/storage-key-test.ts @@ -0,0 +1,32 @@ +import type { Wallet, WalletVersion } from '@wallet-standard/base'; +import type { UiWalletAccount } from '@wallet-standard/ui-core'; +import { getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED } from '@wallet-standard/ui-registry'; + +import { getUiWalletAccountStorageKey } from '../storage-key.js'; + +describe('getUiWalletAccountStorageKey()', () => { + let mockUiWalletAccount: UiWalletAccount; + beforeEach(() => { + const mockWalletAccount = { + address: 'abc', + chains: [], + features: [], + publicKey: new Uint8Array([1, 2, 3]), + }; + const mockWallet: Wallet = { + accounts: [], + chains: [], + features: {}, + icon: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEAAAAALAAAAAABAAEAAAIBAAA=', + name: 'Mock:Wallet', + version: '1.0.0' as WalletVersion, + }; + mockUiWalletAccount = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + }); + it('vends a colon separated key for a wallet account', () => { + expect(getUiWalletAccountStorageKey(mockUiWalletAccount)).toBe('Mock_Wallet:abc'); + }); +}); diff --git a/packages/ui/compare/src/__tests__/tsconfig.json b/packages/ui/compare/src/__tests__/tsconfig.json new file mode 100644 index 00000000..5fb5cded --- /dev/null +++ b/packages/ui/compare/src/__tests__/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.all.json", + "compilerOptions": { + "isolatedModules": false + } +} diff --git a/packages/ui/compare/src/compare.ts b/packages/ui/compare/src/compare.ts new file mode 100644 index 00000000..ecf474d2 --- /dev/null +++ b/packages/ui/compare/src/compare.ts @@ -0,0 +1,40 @@ +import type { UiWallet, UiWalletAccount } from '@wallet-standard/ui-core'; +import { getWalletForHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED } from '@wallet-standard/ui-registry'; + +/** + * Given two `UiWalletAccount` objects, this method will tell you if they represent the same + * underlying `WalletAccount`. + * + * `UiWalletAccount` objects are meant to be used in client apps to render UI; they are not the + * _actual_ underlying `WalletAccount` objects. In particular, they can change over time and you can + * not presume that two `UiWalletAccount` objects will be referentially equal - even though they + * represent the 'same' account. + * + * WARNING: It is insufficient to compare two accounts on the basis of their addresses; it's + * possible for two different wallets to be configured with the same account. Use this method + * whenever you need to know for sure that two `UiWalletAccount` objects represent the same + * address _and_ belong to the same underlying `Wallet`. + */ +export function uiWalletAccountsAreSame(a: UiWalletAccount, b: UiWalletAccount): boolean { + if (a.address !== b.address) { + return false; + } + const underlyingWalletA = getWalletForHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(a); + const underlyingWalletB = getWalletForHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(b); + return underlyingWalletA === underlyingWalletB; +} + +/** + * + * Given a `UiWalletAccount`, this method will tell you if the account belongs to a specific + * `UiWallet`. + * + * WARNING: It's possible for two different wallets to be configured with the same account. Use this + * method whenever you need to know for sure that a `UiWalletAccount` belongs to a particular + * `UiWallet`. + */ +export function uiWalletAccountBelongsToUiWallet(account: UiWalletAccount, wallet: UiWallet): boolean { + const underlyingWalletForUiWallet = getWalletForHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(wallet); + const underlyingWalletForUiWalletAccount = getWalletForHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(account); + return underlyingWalletForUiWallet === underlyingWalletForUiWalletAccount; +} diff --git a/packages/ui/compare/src/index.ts b/packages/ui/compare/src/index.ts new file mode 100644 index 00000000..047d873f --- /dev/null +++ b/packages/ui/compare/src/index.ts @@ -0,0 +1,2 @@ +export * from './compare.js'; +export * from './storage-key.js'; diff --git a/packages/ui/compare/src/storage-key.ts b/packages/ui/compare/src/storage-key.ts new file mode 100644 index 00000000..887ff8c0 --- /dev/null +++ b/packages/ui/compare/src/storage-key.ts @@ -0,0 +1,13 @@ +import type { UiWalletAccount } from '@wallet-standard/ui-core'; +import { getWalletForHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED } from '@wallet-standard/ui-registry'; + +/** + * Produces a stable string that can be used to uniquely identify a `UiWalletAccount`. + * + * You can use this to identify a list item when rendering a list of accounts, or as a way to store + * your app's last selected account as a string in browser storage. + */ +export function getUiWalletAccountStorageKey(uiWalletAccount: UiWalletAccount): string { + const underlyingWallet = getWalletForHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(uiWalletAccount); + return `${underlyingWallet.name.replace(':', '_')}:${uiWalletAccount.address}`; +} diff --git a/packages/ui/compare/tsconfig.all.json b/packages/ui/compare/tsconfig.all.json new file mode 100644 index 00000000..d4ca4cbf --- /dev/null +++ b/packages/ui/compare/tsconfig.all.json @@ -0,0 +1,17 @@ +{ + "extends": "../../../tsconfig.root.json", + "references": [ + { + "path": "../core/tsconfig.all.json" + }, + { + "path": "../registry/tsconfig.all.json" + }, + { + "path": "./tsconfig.cjs.json" + }, + { + "path": "./tsconfig.esm.json" + } + ] +} diff --git a/packages/ui/compare/tsconfig.cjs.json b/packages/ui/compare/tsconfig.cjs.json new file mode 100644 index 00000000..ce91f9bd --- /dev/null +++ b/packages/ui/compare/tsconfig.cjs.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.cjs.json", + "include": ["src"], + "compilerOptions": { + "outDir": "lib/cjs", + "jsx": "react" + } +} diff --git a/packages/ui/compare/tsconfig.esm.json b/packages/ui/compare/tsconfig.esm.json new file mode 100644 index 00000000..3d87d964 --- /dev/null +++ b/packages/ui/compare/tsconfig.esm.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.esm.json", + "include": ["src"], + "compilerOptions": { + "outDir": "lib/esm", + "declarationDir": "lib/types", + "jsx": "react" + } +} diff --git a/packages/ui/core/.gitignore b/packages/ui/core/.gitignore new file mode 100644 index 00000000..a65b4177 --- /dev/null +++ b/packages/ui/core/.gitignore @@ -0,0 +1 @@ +lib diff --git a/packages/ui/core/CHANGELOG.md b/packages/ui/core/CHANGELOG.md new file mode 100644 index 00000000..d7c2b76a --- /dev/null +++ b/packages/ui/core/CHANGELOG.md @@ -0,0 +1 @@ +# @wallet-standard/ui-core diff --git a/packages/ui/core/LICENSE b/packages/ui/core/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/packages/ui/core/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/ui/core/README.md b/packages/ui/core/README.md new file mode 100644 index 00000000..12e95277 --- /dev/null +++ b/packages/ui/core/README.md @@ -0,0 +1,23 @@ +# `@wallet-standard/ui-core` + +This package includes the type definitions of wallets and accounts in client applications. + +Client application developers are not expected to construct these objects themselves, but rather to obtain them by using a third-party Wallet Standard UI library appropriate for their UI framework. + +## Types + +### `UiWalletHandle` + +This type represents a `Wallet` or a `WalletAccount` in a client application that has some feature set. It acts as a ‘handle’ to the underlying `Wallet` instance in the Wallet Standard registry. + +### `UiWallet` + +This type represents a `Wallet` in a client application. It acts as a ‘handle’ to the underlying `Wallet` instance in the Wallet Standard registry, and contains a subset of its properties. + +You can pass objects of this type around your application, use its `icon` and `name` for display, inspect its `chains` and `features`, and enumerate the `accounts` for which your application has been granted authorization to use. + +### `UiWalletAccount` + +This type represents a `WalletAccount` in a client application. It acts as a ‘handle’ to the underlying `WalletAccount` instance in the Wallet Standard registry, and contains a subset of its properties. + +You can pass objects of this type around your application, use its `icon` and `name` for display, and inspect its `chains` and `features`. diff --git a/packages/ui/core/package.json b/packages/ui/core/package.json new file mode 100644 index 00000000..52a0d99a --- /dev/null +++ b/packages/ui/core/package.json @@ -0,0 +1,38 @@ +{ + "name": "@wallet-standard/ui-core", + "version": "0.0.0", + "author": "Solana Maintainers ", + "repository": "https://github.com/wallet-standard/wallet-standard", + "license": "Apache-2.0", + "publishConfig": { + "access": "public" + }, + "files": [ + "lib", + "src", + "LICENSE" + ], + "engines": { + "node": ">=16" + }, + "type": "module", + "sideEffects": false, + "main": "./lib/cjs/index.js", + "module": "./lib/esm/index.js", + "types": "./lib/types/index.d.ts", + "exports": { + "require": "./lib/cjs/index.js", + "import": "./lib/esm/index.js", + "types": "./lib/types/index.d.ts" + }, + "scripts": { + "clean": "shx mkdir -p lib && shx rm -rf lib", + "package": "shx mkdir -p lib/cjs && shx echo '{ \"type\": \"commonjs\" }' > lib/cjs/package.json" + }, + "dependencies": { + "@wallet-standard/base": "workspace:*" + }, + "devDependencies": { + "shx": "^0.3.4" + } +} diff --git a/packages/ui/core/src/UiWallet.ts b/packages/ui/core/src/UiWallet.ts new file mode 100644 index 00000000..536f257a --- /dev/null +++ b/packages/ui/core/src/UiWallet.ts @@ -0,0 +1,19 @@ +import type { Wallet } from '@wallet-standard/base'; + +import type { UiWalletAccount } from './UiWalletAccount.js'; +import type { UiWalletHandle } from './UiWalletHandle.js'; + +/** + * Represents a `Wallet` in a client application. It acts as a 'handle' to the underlying `Wallet` + * instance in the Wallet Standard registry, and contains a subset of its properties. + * + * You can pass objects of this type around your application, use its `icon` and `name` for display, + * inspect its `chains` and `features`, and enumerate the `accounts` for which your application has + * been granted authorization to use. + */ +export type UiWallet = UiWalletHandle & + Readonly< + Pick & { + accounts: readonly UiWalletAccount[]; + } + >; diff --git a/packages/ui/core/src/UiWalletAccount.ts b/packages/ui/core/src/UiWalletAccount.ts new file mode 100644 index 00000000..b70f8be8 --- /dev/null +++ b/packages/ui/core/src/UiWalletAccount.ts @@ -0,0 +1,14 @@ +import type { ReadonlyUint8Array, WalletAccount } from '@wallet-standard/base'; + +import type { UiWalletHandle } from './UiWalletHandle.js'; + +/** + * Represents a `WalletAccount` in a client application. It acts as a 'handle' to the underlying + * `WalletAccount` instance in the Wallet Standard registry, and contains a subset of its + * properties. + */ +export type UiWalletAccount = UiWalletHandle & + Pick & + Readonly<{ + publicKey: ReadonlyUint8Array; + }>; diff --git a/packages/ui/core/src/UiWalletHandle.ts b/packages/ui/core/src/UiWalletHandle.ts new file mode 100644 index 00000000..be79733e --- /dev/null +++ b/packages/ui/core/src/UiWalletHandle.ts @@ -0,0 +1,6 @@ +import type { IdentifierArray } from '@wallet-standard/base'; + +export type UiWalletHandle = { + readonly '~uiWalletHandle': unique symbol; + readonly features: IdentifierArray; +}; diff --git a/packages/ui/core/src/index.ts b/packages/ui/core/src/index.ts new file mode 100644 index 00000000..d919295a --- /dev/null +++ b/packages/ui/core/src/index.ts @@ -0,0 +1,3 @@ +export * from './UiWallet.js'; +export * from './UiWalletAccount.js'; +export * from './UiWalletHandle.js'; diff --git a/packages/ui/core/tsconfig.all.json b/packages/ui/core/tsconfig.all.json new file mode 100644 index 00000000..c2bd3818 --- /dev/null +++ b/packages/ui/core/tsconfig.all.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.root.json", + "references": [ + { + "path": "./tsconfig.cjs.json" + }, + { + "path": "./tsconfig.esm.json" + } + ] +} diff --git a/packages/ui/core/tsconfig.cjs.json b/packages/ui/core/tsconfig.cjs.json new file mode 100644 index 00000000..ce91f9bd --- /dev/null +++ b/packages/ui/core/tsconfig.cjs.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.cjs.json", + "include": ["src"], + "compilerOptions": { + "outDir": "lib/cjs", + "jsx": "react" + } +} diff --git a/packages/ui/core/tsconfig.esm.json b/packages/ui/core/tsconfig.esm.json new file mode 100644 index 00000000..3d87d964 --- /dev/null +++ b/packages/ui/core/tsconfig.esm.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.esm.json", + "include": ["src"], + "compilerOptions": { + "outDir": "lib/esm", + "declarationDir": "lib/types", + "jsx": "react" + } +} diff --git a/packages/ui/registry/.gitignore b/packages/ui/registry/.gitignore new file mode 100644 index 00000000..a65b4177 --- /dev/null +++ b/packages/ui/registry/.gitignore @@ -0,0 +1 @@ +lib diff --git a/packages/ui/registry/CHANGELOG.md b/packages/ui/registry/CHANGELOG.md new file mode 100644 index 00000000..eafa28eb --- /dev/null +++ b/packages/ui/registry/CHANGELOG.md @@ -0,0 +1 @@ +# @wallet-standard/ui-registry diff --git a/packages/ui/registry/LICENSE b/packages/ui/registry/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/packages/ui/registry/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/ui/registry/README.md b/packages/ui/registry/README.md new file mode 100644 index 00000000..349b434a --- /dev/null +++ b/packages/ui/registry/README.md @@ -0,0 +1 @@ +# `@wallet-standard/ui-registry` diff --git a/packages/ui/registry/package.json b/packages/ui/registry/package.json new file mode 100644 index 00000000..5c909818 --- /dev/null +++ b/packages/ui/registry/package.json @@ -0,0 +1,42 @@ +{ + "name": "@wallet-standard/ui-registry", + "version": "0.0.0", + "author": "Solana Maintainers ", + "repository": "https://github.com/wallet-standard/wallet-standard", + "license": "Apache-2.0", + "publishConfig": { + "access": "public" + }, + "files": [ + "lib", + "src", + "LICENSE" + ], + "engines": { + "node": ">=16" + }, + "type": "module", + "sideEffects": false, + "main": "./lib/cjs/index.js", + "module": "./lib/esm/index.js", + "types": "./lib/types/index.d.ts", + "exports": { + "require": "./lib/cjs/index.js", + "import": "./lib/esm/index.js", + "types": "./lib/types/index.d.ts" + }, + "scripts": { + "clean": "shx mkdir -p lib && shx rm -rf lib", + "package": "shx mkdir -p lib/cjs && shx echo '{ \"type\": \"commonjs\" }' > lib/cjs/package.json", + "test": "jest -c ../../../node_modules/@wallet-standard/test-config/jest.config.ts --rootDir ." + }, + "dependencies": { + "@wallet-standard/base": "workspace:*", + "@wallet-standard/errors": "workspace:*", + "@wallet-standard/ui-core": "workspace:*" + }, + "devDependencies": { + "@wallet-standard/test-matchers": "workspace:^", + "shx": "^0.3.4" + } +} diff --git a/packages/ui/registry/src/UiWalletAccountRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ts b/packages/ui/registry/src/UiWalletAccountRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ts new file mode 100644 index 00000000..a23820d2 --- /dev/null +++ b/packages/ui/registry/src/UiWalletAccountRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ts @@ -0,0 +1,71 @@ +import type { Wallet, WalletAccount } from '@wallet-standard/base'; +import type { UiWalletAccount } from '@wallet-standard/ui-core'; + +import { + getWalletForHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, + registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, +} from './UiWalletHandleRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.js'; +import { identifierArraysAreDifferent } from './compare.js'; + +const walletAccountsToUiWalletAccounts = new WeakMap(); + +type Mutable = { -readonly [P in keyof T]: T[P] }; + +/** + * DO NOT USE THIS OR YOU WILL BE FIRED + * + * This method is for exclusive use by Wallet Standard UI library authors. Use this if you need to + * create or obtain the existing `UiWalletAccount` object associated with a Wallet Standard + * `WalletAccount`. + * + * @internal + */ +export function getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED< + TWallet extends Wallet, +>(wallet: TWallet, account: WalletAccount): UiWalletAccount { + let existingUiWalletAccount = walletAccountsToUiWalletAccounts.get(account); + if (existingUiWalletAccount) { + try { + if (getWalletForHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(existingUiWalletAccount) !== wallet) { + existingUiWalletAccount = undefined; + } + } catch { + existingUiWalletAccount = undefined; + } + } + const mustInitialize = !existingUiWalletAccount; + let uiWalletAccount: Mutable = existingUiWalletAccount ?? ({} as Mutable); + let isDirty = !existingUiWalletAccount; + function dirtyUiWallet() { + if (!isDirty) { + uiWalletAccount = { ...uiWalletAccount } as Mutable; + isDirty = true; + } + } + if (mustInitialize || identifierArraysAreDifferent(uiWalletAccount.chains, account.chains)) { + dirtyUiWallet(); + uiWalletAccount.chains = Object.freeze([...account.chains]); + } + if (mustInitialize || identifierArraysAreDifferent(uiWalletAccount.features, account.features)) { + dirtyUiWallet(); + uiWalletAccount.features = Object.freeze([...account.features]); + } + if ( + mustInitialize || + uiWalletAccount.address !== account.address || + uiWalletAccount.icon !== account.icon || + uiWalletAccount.label !== account.label || + uiWalletAccount.publicKey !== account.publicKey + ) { + dirtyUiWallet(); + uiWalletAccount.address = account.address; + uiWalletAccount.icon = account.icon; + uiWalletAccount.label = account.label; + uiWalletAccount.publicKey = account.publicKey; + } + if (isDirty) { + walletAccountsToUiWalletAccounts.set(account, uiWalletAccount); + registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(uiWalletAccount, wallet); + } + return Object.freeze(uiWalletAccount); +} diff --git a/packages/ui/registry/src/UiWalletHandleRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ts b/packages/ui/registry/src/UiWalletHandleRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ts new file mode 100644 index 00000000..414d0602 --- /dev/null +++ b/packages/ui/registry/src/UiWalletHandleRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ts @@ -0,0 +1,69 @@ +import type { Wallet, WalletAccount } from '@wallet-standard/base'; +import { + WALLET_STANDARD_ERROR__REGISTRY__WALLET_ACCOUNT_NOT_FOUND, + WALLET_STANDARD_ERROR__REGISTRY__WALLET_NOT_FOUND, + WalletStandardError, + safeCaptureStackTrace, +} from '@wallet-standard/errors'; +import type { UiWalletAccount, UiWalletHandle } from '@wallet-standard/ui-core'; + +const uiWalletHandlesToWallets = new WeakMap(); + +/** + * DO NOT USE THIS OR YOU WILL BE FIRED + * + * This method is for exclusive use by Wallet Standard UI library authors. Use this to associate a + * `UiWallet` or `UiWalletAccount` object with a Wallet Standard `Wallet` in the central registry. + * + * @internal + */ +export function registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + uiWalletHandle: UiWalletHandle, + wallet: Wallet +): void { + uiWalletHandlesToWallets.set(uiWalletHandle, wallet); +} + +/** + * DO NOT USE THIS OR YOU WILL BE FIRED + * + * This method is for exclusive use by Wallet Standard UI library authors. If you are building APIs + * that need to materialize account-based features given a `UiWalletAccount` UI object, this + * function will vend you the underlying `WalletAccount` object associated with it. + * + * @internal + */ +export function getWalletAccountForUiWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + uiWalletAccount: UiWalletAccount +): WalletAccount { + const wallet = getWalletForHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(uiWalletAccount); + const account = wallet.accounts.find(({ address }) => address === uiWalletAccount.address); + if (!account) { + const err = new WalletStandardError(WALLET_STANDARD_ERROR__REGISTRY__WALLET_ACCOUNT_NOT_FOUND, { + address: uiWalletAccount.address, + walletName: wallet.name, + }); + safeCaptureStackTrace(err, getWalletAccountForUiWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED); + throw err; + } + return account; +} + +/** + * DO NOT USE THIS OR YOU WILL BE FIRED + * + * This method is for exclusive use by Wallet Standard UI library authors. If you are building APIs + * that need to materialize wallet-based features given a `UiWalletAccount` UI object, this + * function will vend you the underlying `Wallet` object associated with it. + * + * @internal + */ +export function getWalletForHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(uiWalletHandle: UiWalletHandle): Wallet { + const wallet = uiWalletHandlesToWallets.get(uiWalletHandle); + if (!wallet) { + const err = new WalletStandardError(WALLET_STANDARD_ERROR__REGISTRY__WALLET_NOT_FOUND); + safeCaptureStackTrace(err, getWalletForHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED); + throw err; + } + return wallet; +} diff --git a/packages/ui/registry/src/UiWalletRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ts b/packages/ui/registry/src/UiWalletRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ts new file mode 100644 index 00000000..a7436e9b --- /dev/null +++ b/packages/ui/registry/src/UiWalletRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ts @@ -0,0 +1,105 @@ +import type { IdentifierArray, Wallet } from '@wallet-standard/base'; +import type { UiWalletAccount, UiWallet } from '@wallet-standard/ui-core'; + +import { getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED } from './UiWalletAccountRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.js'; +import { registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED } from './UiWalletHandleRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.js'; +import { identifierArraysAreDifferent } from './compare.js'; + +const walletsToUiWallets = new WeakMap(); + +type Mutable = { -readonly [P in keyof T]: T[P] }; + +/** + * DO NOT USE THIS OR YOU WILL BE FIRED + * + * This method is for exclusive use by Wallet Standard UI library authors. Use this if you need to + * create or obtain the existing `UiWallet` object associated with a Wallet Standard `Wallet`. + * + * @internal + */ +export function getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + wallet: TWallet +): UiWallet { + const existingUiWallet = walletsToUiWallets.get(wallet); + const mustInitialize = !existingUiWallet; + let uiWallet: Mutable = existingUiWallet ?? ({} as Mutable); + let isDirty = !existingUiWallet; + function dirtyUiWallet() { + if (!isDirty) { + uiWallet = { ...uiWallet } as Mutable; + isDirty = true; + } + } + const nextUiWalletAccounts = { + _cache: [] as UiWalletAccount[], + *[Symbol.iterator]() { + if (this._cache.length) { + yield* this._cache; + } + for (const walletAccount of wallet.accounts.slice(this._cache.length)) { + const uiWalletAccount = + getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + wallet, + walletAccount + ); + this._cache.push(uiWalletAccount); + yield uiWalletAccount; + } + }, + some(predicateFn: (uiWalletAccount: UiWalletAccount) => boolean) { + if (this._cache.some(predicateFn)) { + return true; + } + for (const walletAccount of wallet.accounts.slice(this._cache.length)) { + const uiWalletAccount = + getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + wallet, + walletAccount + ); + this._cache.push(uiWalletAccount); + if (predicateFn(uiWalletAccount)) { + return true; + } + } + return false; + }, + get length() { + return wallet.accounts.length; + }, + }; + if ( + mustInitialize || + uiWallet.accounts.length !== wallet.accounts.length || + nextUiWalletAccounts.some((account) => !uiWallet.accounts.includes(account)) + ) { + dirtyUiWallet(); + uiWallet.accounts = Object.freeze(Array.from(nextUiWalletAccounts)); + } + if ( + mustInitialize || + identifierArraysAreDifferent(uiWallet.features, Object.keys(wallet.features) as IdentifierArray) + ) { + dirtyUiWallet(); + uiWallet.features = Object.freeze(Object.keys(wallet.features) as IdentifierArray); + } + if (mustInitialize || identifierArraysAreDifferent(uiWallet.chains, wallet.chains)) { + dirtyUiWallet(); + uiWallet.chains = Object.freeze([...wallet.chains]); + } + if ( + mustInitialize || + uiWallet.icon !== wallet.icon || + uiWallet.name !== wallet.name || + uiWallet.version !== wallet.version + ) { + dirtyUiWallet(); + uiWallet.icon = wallet.icon; + uiWallet.name = wallet.name; + uiWallet.version = wallet.version; + } + if (isDirty) { + walletsToUiWallets.set(wallet, uiWallet); + registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(uiWallet, wallet); + } + return Object.freeze(uiWallet); +} diff --git a/packages/ui/registry/src/__tests__/tsconfig.json b/packages/ui/registry/src/__tests__/tsconfig.json new file mode 100644 index 00000000..5fb5cded --- /dev/null +++ b/packages/ui/registry/src/__tests__/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.all.json", + "compilerOptions": { + "isolatedModules": false + } +} diff --git a/packages/ui/registry/src/__tests__/uiWalletAccountRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED-test.ts b/packages/ui/registry/src/__tests__/uiWalletAccountRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED-test.ts new file mode 100644 index 00000000..a14e0793 --- /dev/null +++ b/packages/ui/registry/src/__tests__/uiWalletAccountRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED-test.ts @@ -0,0 +1,272 @@ +import '@wallet-standard/test-matchers/toBeFrozenObject'; + +import type { Wallet, WalletAccount } from '@wallet-standard/base'; + +import { getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED } from '../UiWalletAccountRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.js'; +import { + getWalletForHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, + registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, +} from '../UiWalletHandleRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.js'; + +jest.mock('../UiWalletHandleRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.js'); + +type Mutable = { -readonly [P in keyof T]: T[P] }; + +describe('getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED', () => { + let mockWallet: Wallet; + let mockWalletAccount: Mutable; + beforeEach(() => { + mockWalletAccount = { + address: 'abc', + chains: ['solana:basednet'], + features: ['feature:b'], + icon: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEAAAAALAAAAAABAAEAAAIBAAA=', + label: 'Mock Account A', + publicKey: new Uint8Array([1, 2, 3]), + }; + mockWallet = { + accounts: [mockWalletAccount], + chains: ['solana:basednet', 'solana:goatnet'], + features: { + 'feature:a': { version: '1.0.0' as const }, + 'feature:b': { version: '1.0.0' as const }, + }, + icon: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEAAAAALAAAAAABAAEAAAIBAAA=', + name: 'Mock wallet', + version: '1.0.0' as const, + }; + }); + it('returns a frozen object', () => { + const uiWalletAccount = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + expect(uiWalletAccount).toBeFrozenObject(); + }); + it('registers the Standard wallet associated with the UI wallet account with the wallet handle registry', () => { + const uiWalletAccount = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + expect(registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).toHaveBeenCalledWith(uiWalletAccount, mockWallet); + }); + it('returns the same UI wallet account given the same underlying Standard wallet account', () => { + jest.mocked(getWalletForHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).mockReturnValue(mockWallet); + const uiWalletAccountA = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + const uiWalletAccountB = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + expect(uiWalletAccountA).toBe(uiWalletAccountB); + }); + it('returns a different UI wallet account given a different underlying Standard wallet account', () => { + jest.mocked(getWalletForHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).mockReturnValue(mockWallet); + const uiWalletAccountA = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + jest.mocked(getWalletForHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).mockReturnValue( + { ...mockWallet } /* a different object */ + ); + const uiWalletAccountB = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + expect(uiWalletAccountA).not.toBe(uiWalletAccountB); + }); + /** + * Address + */ + it('returns a UI wallet account with an address', () => { + const uiWalletAccount = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + expect(uiWalletAccount).toHaveProperty('address', mockWalletAccount.address); + }); + it('returns a new UI wallet account given the same underlying Standard wallet account whose address has been mutated', () => { + const uiWalletAccountA = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + mockWalletAccount.address = 'def'; // As unlikely is it that an account's address would be mutated, we test it none the less. + const uiWalletAccountB = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + expect(uiWalletAccountB).not.toBe(uiWalletAccountA); + expect(registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).toHaveBeenCalledWith(uiWalletAccountB, mockWallet); + }); + /** + * Chains + */ + it('returns a UI wallet account with a frozen chains array', () => { + const uiWalletAccount = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + expect(uiWalletAccount).toHaveProperty('chains', mockWalletAccount.chains); + expect(uiWalletAccount.chains).toBeFrozenObject(); + }); + it('returns a new UI wallet account given the same underlying Standard wallet account that mutated the chains to add one', () => { + const uiWalletAccountA = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + (mockWalletAccount.chains as Mutable).unshift('solana:boomernet'); + const uiWalletAccountB = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + expect(uiWalletAccountB).not.toBe(uiWalletAccountA); + expect(registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).toHaveBeenCalledWith(uiWalletAccountB, mockWallet); + }); + it('returns a new UI wallet account given the same underlying Standard wallet account that mutated the chains to remove one', () => { + const uiWalletAccountA = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + (mockWalletAccount.chains as Mutable).splice(0, 1); + const uiWalletAccountB = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + expect(uiWalletAccountB).not.toBe(uiWalletAccountA); + expect(registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).toHaveBeenCalledWith(uiWalletAccountB, mockWallet); + }); + it('returns a new UI wallet account given the same underlying Standard wallet account whose existing chains have been mutated', () => { + const uiWalletAccountA = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + (mockWalletAccount.chains as Mutable)[0] = 'solana:danknet'; + const uiWalletAccountB = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + expect(uiWalletAccountB).not.toBe(uiWalletAccountA); + expect(registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).toHaveBeenCalledWith(uiWalletAccountB, mockWallet); + }); + /** + * Features + */ + it('returns a UI wallet account with a flat frozen feature names array', () => { + const uiWalletAccount = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + expect(uiWalletAccount).toHaveProperty('features', mockWalletAccount.features); + expect(uiWalletAccount.chains).toBeFrozenObject(); + }); + it('returns a new UI wallet account given the same underlying Standard wallet account that mutated the features to add one', () => { + const uiWalletAccountA = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + (mockWalletAccount.features as Mutable).push('feature:new'); + const uiWalletAccountB = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + expect(uiWalletAccountB).not.toBe(uiWalletAccountA); + expect(registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).toHaveBeenCalledWith(uiWalletAccountB, mockWallet); + }); + it('returns a new UI wallet account given the same underlying Standard wallet account that mutated the features to remove one', () => { + const uiWalletAccountA = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + (mockWalletAccount.features as Mutable).pop(); + const uiWalletAccountB = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + expect(uiWalletAccountB).not.toBe(uiWalletAccountA); + expect(registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).toHaveBeenCalledWith(uiWalletAccountB, mockWallet); + }); + it('returns a new UI wallet given the same underlying Standard wallet whose existing features have been mutated', () => { + const uiWalletAccountA = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + (mockWalletAccount.features as Mutable)[0] = 'feature:z'; + const uiWalletAccountB = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + expect(uiWalletAccountB).not.toBe(uiWalletAccountA); + expect(registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).toHaveBeenCalledWith(uiWalletAccountB, mockWallet); + }); + /** + * Icon + */ + it('returns a UI wallet account with an icon', () => { + const uiWalletAccount = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + expect(uiWalletAccount).toHaveProperty('icon', mockWalletAccount.icon); + }); + it('returns a new UI wallet account given the same underlying Standard wallet account whose icon has been mutated', () => { + const uiWalletAccountA = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + mockWalletAccount.icon = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; + const uiWalletAccountB = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + expect(uiWalletAccountB).not.toBe(uiWalletAccountA); + expect(registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).toHaveBeenCalledWith(uiWalletAccountB, mockWallet); + }); + /** + * Label + */ + it('returns a UI wallet account with a label', () => { + const uiWalletAccount = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + expect(uiWalletAccount).toHaveProperty('label', mockWalletAccount.label); + }); + it('returns a new UI wallet account given the same underlying Standard wallet account whose label has been mutated', () => { + const uiWalletAccountA = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + mockWalletAccount.label = 'Based Account A'; + const uiWalletAccountB = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + expect(uiWalletAccountB).not.toBe(uiWalletAccountA); + expect(registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).toHaveBeenCalledWith(uiWalletAccountB, mockWallet); + }); + /** + * Public key + */ + it('returns a UI wallet account with a public key', () => { + const uiWalletAccount = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + expect(uiWalletAccount).toHaveProperty('publicKey', mockWalletAccount.publicKey); + }); + it('returns a new UI wallet account given the same underlying Standard wallet account whose label has been mutated', () => { + const uiWalletAccountA = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + mockWalletAccount.publicKey = new Uint8Array([4, 5, 6]); + const uiWalletAccountB = getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED( + mockWallet, + mockWalletAccount + ); + expect(uiWalletAccountB).not.toBe(uiWalletAccountA); + expect(registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).toHaveBeenCalledWith(uiWalletAccountB, mockWallet); + }); +}); diff --git a/packages/ui/registry/src/__tests__/uiWalletHandleRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED-test.ts b/packages/ui/registry/src/__tests__/uiWalletHandleRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED-test.ts new file mode 100644 index 00000000..9dfb3de1 --- /dev/null +++ b/packages/ui/registry/src/__tests__/uiWalletHandleRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED-test.ts @@ -0,0 +1,29 @@ +import type { Wallet } from '@wallet-standard/base'; +import type { UiWalletHandle } from '@wallet-standard/ui-core'; + +import { + getWalletForHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, + registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED, +} from '../UiWalletHandleRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.js'; + +describe('the wallet registry', () => { + describe('given a handle against which a Standard wallet is registered', () => { + let mockWallet: Wallet; + let uiWalletHandle: UiWalletHandle; + beforeEach(() => { + mockWallet = {} as Wallet; + uiWalletHandle = {} as UiWalletHandle; + registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(uiWalletHandle, mockWallet); + }); + it('lets you recover a registered wallet by its handle', () => { + const recoveredWallet = getWalletForHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(uiWalletHandle); + expect(recoveredWallet).toBe(mockWallet); + }); + }); + it('throws if there is no registered wallet pertaining to the supplied handle', () => { + const unregisteredHandle = {} as UiWalletHandle; + expect(() => { + getWalletForHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(unregisteredHandle); + }).toThrow(); + }); +}); diff --git a/packages/ui/registry/src/__tests__/uiWalletRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED-test.ts b/packages/ui/registry/src/__tests__/uiWalletRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED-test.ts new file mode 100644 index 00000000..305c44ae --- /dev/null +++ b/packages/ui/registry/src/__tests__/uiWalletRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED-test.ts @@ -0,0 +1,190 @@ +import '@wallet-standard/test-matchers/toBeFrozenObject'; + +import type { Wallet, WalletAccount, WalletVersion } from '@wallet-standard/base'; +import type { UiWalletAccount } from '@wallet-standard/ui-core'; + +import { getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED } from '../UiWalletAccountRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.js'; +import { registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED } from '../UiWalletHandleRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.js'; +import { getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED } from '../UiWalletRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.js'; + +jest.mock('../UiWalletAccountRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.js'); +jest.mock('../UiWalletHandleRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.js'); + +type Mutable = { -readonly [P in keyof T]: T[P] }; + +describe('getOrCreateUiWalletForWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED', () => { + let mockWallet: Mutable; + let mockWalletAccount: WalletAccount; + beforeEach(() => { + mockWalletAccount = { + address: 'abc', + chains: ['solana:basednet'], + features: ['feature:b'], + icon: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEAAAAALAAAAAABAAEAAAIBAAA=', + label: 'Mock Account A', + publicKey: new Uint8Array([1, 2, 3]), + }; + mockWallet = { + accounts: [mockWalletAccount], + chains: ['solana:basednet', 'solana:goatnet'], + features: { + 'feature:a': { version: '1.0.0' as const }, + 'feature:b': { version: '1.0.0' as const }, + }, + icon: 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEAAAAALAAAAAABAAEAAAIBAAA=', + name: 'Mock wallet', + version: '1.0.0' as const, + }; + }); + it('returns a frozen object', () => { + const uiWallet = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + expect(uiWallet).toBeFrozenObject(); + }); + it('registers the Standard wallet associated with the UI wallet account with the wallet handle registry', () => { + const uiWallet = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + expect(registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).toHaveBeenCalledWith(uiWallet, mockWallet); + }); + it('returns the same UI wallet given the same underlying Standard wallet', () => { + const uiWalletA = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + const uiWalletB = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + expect(uiWalletB).toBe(uiWalletA); + expect(registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).toHaveBeenCalledTimes(1); + expect(uiWalletB).toBeFrozenObject(); + }); + /** + * Accounts + */ + it('returns a UI wallet with a frozen list of UI wallet accounts', () => { + const mockUiWalletAccount = {} as UiWalletAccount; + jest.mocked(getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).mockReturnValue( + mockUiWalletAccount + ); + const uiWallet = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + expect(uiWallet).toHaveProperty('accounts', [mockUiWalletAccount]); + expect(uiWallet.accounts).toBeFrozenObject(); + }); + it('returns a new UI wallet given the same underlying Standard wallet that mutated the accounts to add one', () => { + const uiWalletA = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + (mockWallet.accounts as Mutable).unshift({ ...mockWalletAccount, address: 'def' }); + const uiWalletB = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + expect(uiWalletB).not.toBe(uiWalletA); + expect(registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).toHaveBeenCalledWith(uiWalletB, mockWallet); + }); + it('returns a new UI wallet given the same underlying Standard wallet that mutated the accounts to remove one', () => { + const uiWalletA = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + (mockWallet.accounts as Mutable).pop(); + const uiWalletB = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + expect(uiWalletB).not.toBe(uiWalletA); + expect(registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).toHaveBeenCalledWith(uiWalletB, mockWallet); + }); + it('returns a new UI wallet given the same underlying Standard wallet that mutated the accounts to modify one', () => { + const uiWalletA = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + jest.mocked(getOrCreateUiWalletAccountForStandardWalletAccount_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).mockReturnValue( + // Doesn't matter what this is, so long as it's a new object. + {} as UiWalletAccount + ); + const uiWalletB = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + expect(uiWalletB).not.toBe(uiWalletA); + expect(registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).toHaveBeenCalledWith(uiWalletB, mockWallet); + }); + /** + * Chains + */ + it('returns a UI wallet with a frozen chains array', () => { + const uiWallet = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + expect(uiWallet).toHaveProperty('chains', mockWallet.chains); + expect(uiWallet.chains).toBeFrozenObject(); + }); + it('returns a new UI wallet given the same underlying Standard wallet that mutated the chains to add one', () => { + const uiWalletA = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + (mockWallet.chains as Mutable).unshift('solana:boomernet'); + const uiWalletB = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + expect(uiWalletB).not.toBe(uiWalletA); + expect(registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).toHaveBeenCalledWith(uiWalletB, mockWallet); + }); + it('returns a new UI wallet given the same underlying Standard wallet that mutated the chains to remove one', () => { + const uiWalletA = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + (mockWallet.chains as Mutable).splice(0, 1); + const uiWalletB = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + expect(uiWalletB).not.toBe(uiWalletA); + expect(registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).toHaveBeenCalledWith(uiWalletB, mockWallet); + }); + it('returns a new UI wallet given the same underlying Standard wallet whose existing chains have been mutated', () => { + const uiWalletA = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + (mockWallet.chains as Mutable)[0] = 'solana:danknet'; + const uiWalletB = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + expect(uiWalletB).not.toBe(uiWalletA); + expect(registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).toHaveBeenCalledWith(uiWalletB, mockWallet); + }); + /** + * Features + */ + it('returns a UI wallet with a flat frozen feature names array', () => { + const uiWallet = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + expect(uiWallet).toHaveProperty('features', Object.keys(mockWallet.features)); + expect(uiWallet.chains).toBeFrozenObject(); + }); + it('returns a new UI wallet given the same underlying Standard wallet that mutated the features to add one', () => { + const uiWalletA = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + (mockWallet.features as Mutable)['feature:new'] = { version: '1.0.0' as const }; + const uiWalletB = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + expect(uiWalletB).not.toBe(uiWalletA); + expect(registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).toHaveBeenCalledWith(uiWalletB, mockWallet); + }); + it('returns a new UI wallet given the same underlying Standard wallet that mutated the features to remove one', () => { + const uiWalletA = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + delete (mockWallet.features as Mutable)['feature:a']; + const uiWalletB = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + expect(uiWalletB).not.toBe(uiWalletA); + expect(registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).toHaveBeenCalledWith(uiWalletB, mockWallet); + }); + it('returns the same UI wallet given the same underlying Standard wallet whose existing features have been mutated', () => { + const uiWalletA = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + (mockWallet.features as { 'feature:a': { version: string } })['feature:a'].version = '2.0.0' as const; + const uiWalletB = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + expect(uiWalletB).toBe(uiWalletA); + expect(registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).toHaveBeenCalledTimes(1); + }); + /** + * Icon + */ + it('returns a UI wallet with an icon', () => { + const uiWallet = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + expect(uiWallet).toHaveProperty('icon', mockWallet.icon); + }); + it('returns a new UI wallet given the same underlying Standard wallet whose icon has been mutated', () => { + const uiWalletA = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + mockWallet.icon = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'; + const uiWalletB = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + expect(uiWalletB).not.toBe(uiWalletA); + expect(registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).toHaveBeenCalledWith(uiWalletB, mockWallet); + }); + /** + * Name + */ + it('returns a UI wallet with a name', () => { + const uiWallet = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + expect(uiWallet).toHaveProperty('name', mockWallet.name); + }); + it('returns a new UI wallet given the same underlying Standard wallet whose name has been mutated', () => { + const uiWalletA = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + mockWallet.name = 'Based Wallet'; + const uiWalletB = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + expect(uiWalletB).not.toBe(uiWalletA); + expect(registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).toHaveBeenCalledWith(uiWalletB, mockWallet); + }); + /** + * Version + */ + it('returns a UI wallet with a version', () => { + const uiWallet = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + expect(uiWallet).toHaveProperty('version', mockWallet.version); + }); + it('returns a new UI wallet given the same underlying Standard wallet whose version has been mutated', () => { + const uiWalletA = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + mockWallet.version = '2.0.0' as WalletVersion; + const uiWalletB = getOrCreateUiWalletForStandardWallet_DO_NOT_USE_OR_YOU_WILL_BE_FIRED(mockWallet); + expect(uiWalletB).not.toBe(uiWalletA); + expect(registerWalletHandle_DO_NOT_USE_OR_YOU_WILL_BE_FIRED).toHaveBeenCalledWith(uiWalletB, mockWallet); + }); +}); diff --git a/packages/ui/registry/src/compare.ts b/packages/ui/registry/src/compare.ts new file mode 100644 index 00000000..d536fd33 --- /dev/null +++ b/packages/ui/registry/src/compare.ts @@ -0,0 +1,17 @@ +import type { IdentifierArray } from '@wallet-standard/base'; + +export function identifierArraysAreDifferent(a: IdentifierArray, b: IdentifierArray): boolean { + // NOTE: Do not optimize this with an `a.length !== b.length` check. A length check does not + // take into consideration that the array might contain duplicate items. + const itemsSetA = new Set(a); + const itemsSetB = new Set(b); + if (itemsSetA.size !== itemsSetB.size) { + return true; + } + for (const itemFromA of itemsSetA) { + if (!itemsSetB.has(itemFromA)) { + return true; + } + } + return false; +} diff --git a/packages/ui/registry/src/index.ts b/packages/ui/registry/src/index.ts new file mode 100644 index 00000000..9c274bee --- /dev/null +++ b/packages/ui/registry/src/index.ts @@ -0,0 +1,3 @@ +export * from './UiWalletAccountRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.js'; +export * from './UiWalletHandleRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.js'; +export * from './UiWalletRegistry_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.js'; diff --git a/packages/ui/registry/tsconfig.all.json b/packages/ui/registry/tsconfig.all.json new file mode 100644 index 00000000..b47fb78b --- /dev/null +++ b/packages/ui/registry/tsconfig.all.json @@ -0,0 +1,14 @@ +{ + "extends": "../../../tsconfig.root.json", + "references": [ + { + "path": "../core/tsconfig.all.json" + }, + { + "path": "./tsconfig.cjs.json" + }, + { + "path": "./tsconfig.esm.json" + } + ] +} diff --git a/packages/ui/registry/tsconfig.cjs.json b/packages/ui/registry/tsconfig.cjs.json new file mode 100644 index 00000000..ce91f9bd --- /dev/null +++ b/packages/ui/registry/tsconfig.cjs.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.cjs.json", + "include": ["src"], + "compilerOptions": { + "outDir": "lib/cjs", + "jsx": "react" + } +} diff --git a/packages/ui/registry/tsconfig.esm.json b/packages/ui/registry/tsconfig.esm.json new file mode 100644 index 00000000..3d87d964 --- /dev/null +++ b/packages/ui/registry/tsconfig.esm.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.esm.json", + "include": ["src"], + "compilerOptions": { + "outDir": "lib/esm", + "declarationDir": "lib/types", + "jsx": "react" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f12f973f..6d1c795d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -474,6 +474,64 @@ importers: specifier: ^2.7.0 version: 2.7.0(terser@5.15.1) + packages/ui/_: + dependencies: + '@wallet-standard/ui-compare': + specifier: workspace:^ + version: link:../compare + '@wallet-standard/ui-core': + specifier: workspace:^ + version: link:../core + devDependencies: + shx: + specifier: ^0.3.4 + version: 0.3.4 + + packages/ui/compare: + dependencies: + '@wallet-standard/base': + specifier: workspace:* + version: link:../../core/base + '@wallet-standard/ui-core': + specifier: workspace:* + version: link:../core + '@wallet-standard/ui-registry': + specifier: workspace:* + version: link:../registry + devDependencies: + shx: + specifier: ^0.3.4 + version: 0.3.4 + + packages/ui/core: + dependencies: + '@wallet-standard/base': + specifier: workspace:* + version: link:../../core/base + devDependencies: + shx: + specifier: ^0.3.4 + version: 0.3.4 + + packages/ui/registry: + dependencies: + '@wallet-standard/base': + specifier: workspace:* + version: link:../../core/base + '@wallet-standard/errors': + specifier: workspace:* + version: link:../../core/errors + '@wallet-standard/ui-core': + specifier: workspace:* + version: link:../core + devDependencies: + '@wallet-standard/test-matchers': + specifier: workspace:^ + version: link:../../test-matchers/_ + shx: + specifier: ^0.3.4 + version: 0.3.4 + packages: '@ampproject/remapping@2.3.0': @@ -4125,9 +4183,6 @@ packages: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} - scheduler@0.23.0: - resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} - scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} @@ -9475,7 +9530,7 @@ snapshots: dependencies: loose-envify: 1.4.0 react: 18.2.0 - scheduler: 0.23.0 + scheduler: 0.23.2 react-error-boundary@4.0.13(react@18.2.0): dependencies: @@ -9679,10 +9734,6 @@ snapshots: dependencies: xmlchars: 2.2.0 - scheduler@0.23.0: - dependencies: - loose-envify: 1.4.0 - scheduler@0.23.2: dependencies: loose-envify: 1.4.0 diff --git a/tsconfig.all.json b/tsconfig.all.json index 185dc8d6..5a170ae3 100644 --- a/tsconfig.all.json +++ b/tsconfig.all.json @@ -48,6 +48,18 @@ }, { "path": "./packages/react/core/tsconfig.all.json" + }, + { + "path": "./packages/ui/_/tsconfig.all.json" + }, + { + "path": "./packages/ui/core/tsconfig.all.json" + }, + { + "path": "./packages/ui/compare/tsconfig.all.json" + }, + { + "path": "./packages/ui/registry/tsconfig.all.json" } ] }