diff --git a/apps/playground/src/components/cardano/connect-browser-wallet.tsx b/apps/playground/src/components/cardano/connect-browser-wallet.tsx index 0bddcc518..9175f9288 100644 --- a/apps/playground/src/components/cardano/connect-browser-wallet.tsx +++ b/apps/playground/src/components/cardano/connect-browser-wallet.tsx @@ -45,6 +45,11 @@ export function CommonCardanoWallet() { networkId: 0, provider: provider, }} + // webauthn={{ + // networkId: 0, + // provider: provider, + // url: "http://localhost:3000", + // }} /> ); } diff --git a/apps/playground/src/pages/apis/txbuilder/governance/vote-delegation.tsx b/apps/playground/src/pages/apis/txbuilder/governance/vote-delegation.tsx index c878b6179..83888aed3 100644 --- a/apps/playground/src/pages/apis/txbuilder/governance/vote-delegation.tsx +++ b/apps/playground/src/pages/apis/txbuilder/governance/vote-delegation.tsx @@ -1,6 +1,5 @@ import { useState } from "react"; -import { keepRelevant, Quantity, Unit } from "@meshsdk/core"; import { useWallet } from "@meshsdk/react"; import Input from "~/components/form/input"; @@ -36,8 +35,8 @@ function Left() { codeTx += ` },\n`; codeTx += ` rewardAddress,\n`; codeTx += ` )\n`; - codeTx += ` .changeAddress(changeAddress)`; - codeTx += ` .selectUtxos(utxos, "keepRelevant", "10000000")\n`; + codeTx += ` .changeAddress(changeAddress)\n`; + codeTx += ` .selectUtxos(utxos, "keepRelevant", "10000000")`; let codeBuildSign = ``; codeBuildSign += `const unsignedTx = await txBuilder.complete();\n`; @@ -129,7 +128,7 @@ function Right() { codeSnippet += `txBuilder\n`; codeSnippet += ` .voteDelegationCertificate(\n`; codeSnippet += ` {\n`; - codeSnippet += ` dRepId: drepid,\n`; + codeSnippet += ` dRepId: '${drepid}',\n`; codeSnippet += ` },\n`; codeSnippet += ` rewardAddress,\n`; codeSnippet += ` )\n`; diff --git a/apps/playground/src/pages/apis/wallets/meshwallet/load-wallet.tsx b/apps/playground/src/pages/apis/wallets/meshwallet/load-wallet.tsx index caabb6553..15d9eec39 100644 --- a/apps/playground/src/pages/apis/wallets/meshwallet/load-wallet.tsx +++ b/apps/playground/src/pages/apis/wallets/meshwallet/load-wallet.tsx @@ -242,8 +242,8 @@ function Right( }); setWallet(_wallet); - const address = _wallet.getChangeAddress(); - setResponseAddress(address); + const addresses = _wallet.getAddresses(); + setResponseAddress(JSON.stringify(addresses, null, 2)); } } catch (error) { setResponseError(`${error}`); @@ -262,8 +262,8 @@ function Right( }); setWallet(_wallet); - const address = _wallet.getChangeAddress(); - setResponseAddress(address); + const addresses = _wallet.getAddresses(); + setResponseAddress(JSON.stringify(addresses, null, 2)); } catch (error) { setResponseError(`${error}`); } @@ -283,8 +283,8 @@ function Right( }); setWallet(_wallet); - const address = _wallet.getChangeAddress(); - setResponseAddress(address); + const addresses = _wallet.getAddresses(); + setResponseAddress(JSON.stringify(addresses, null, 2)); } catch (error) { setResponseError(`${error}`); } @@ -302,8 +302,8 @@ function Right( }); setWallet(_wallet); - const address = _wallet.getChangeAddress(); - setResponseAddress(address); + const addresses = _wallet.getAddresses(); + setResponseAddress(JSON.stringify(addresses, null, 2)); } catch (error) { setResponseError(`${error}`); } @@ -362,7 +362,7 @@ function Right( response={responseAddress} label="Load wallet and get address" /> - + > diff --git a/apps/playground/src/pages/sitemap.xml.ts b/apps/playground/src/pages/sitemap.xml.ts index 3504d3393..1fb2acb75 100644 --- a/apps/playground/src/pages/sitemap.xml.ts +++ b/apps/playground/src/pages/sitemap.xml.ts @@ -30,15 +30,22 @@ function SiteMap() {} function addLinks(pagesUrls: string[], pages: MenuItem[]) { pages.forEach((api) => { - pagesUrls.push(api.link); + pushLink(pagesUrls, api.link); if (api.items) { api.items.forEach((item) => { - pagesUrls.push(item.link); + pushLink(pagesUrls, item.link); }); } }); } +function pushLink(pagesUrls: string[], link: string) { + if (link.includes("http") && !link.includes("meshjs.dev")) { + return; + } + pagesUrls.push(link); +} + export async function getServerSideProps({ res }: { res: any }) { const pagesUrls: string[] = []; diff --git a/package-lock.json b/package-lock.json index b0e32e558..623ac6e4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13429,6 +13429,29 @@ } } }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.1.tgz", + "integrity": "sha512-UUw5E4e/2+4kFMH7+YxORXGWggtY6sM8WIwh5RZchhLuUg2H1hc98Py+pr8HMz6rdaYrK2t296ZEjYLOCO5uUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-menu": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.4.tgz", @@ -14529,6 +14552,12 @@ "node": ">=10.0.0" } }, + "node_modules/@simplewebauthn/browser": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.0.0.tgz", + "integrity": "sha512-7d/+gxoFoDQxq2EkLl/PuTIQ/rnSrA3bmr8L2Ij7bRyicJoCJX/NDGUNExyctB9nSDrEkkcrJMDkwpCYOGU3Lg==", + "license": "MIT" + }, "node_modules/@sinclair/typebox": { "version": "0.27.8", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", @@ -18424,6 +18453,7 @@ "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, "funding": [ { "type": "github", @@ -22492,6 +22522,7 @@ "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true, "license": "ISC" }, "node_modules/html-escaper": { @@ -24304,6 +24335,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, "license": "MIT" }, "node_modules/json-schema-traverse": { @@ -25923,6 +25955,7 @@ "version": "4.2.8", "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "dev": true, "license": "ISC", "engines": { "node": ">=8" @@ -26357,6 +26390,7 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "hosted-git-info": "^2.1.4", @@ -26369,6 +26403,7 @@ "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver" @@ -31912,6 +31947,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, "license": "Apache-2.0", "dependencies": { "spdx-expression-parse": "^3.0.0", @@ -31922,12 +31958,14 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, "license": "MIT", "dependencies": { "spdx-exceptions": "^2.1.0", @@ -31938,6 +31976,7 @@ "version": "3.0.20", "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", + "dev": true, "license": "CC0-1.0" }, "node_modules/speed-limiter": { @@ -34340,6 +34379,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, "license": "Apache-2.0", "dependencies": { "spdx-correct": "^3.0.0", @@ -35322,6 +35362,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", @@ -35335,6 +35376,7 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, "license": "ISC" }, "node_modules/ws": { @@ -35719,6 +35761,7 @@ "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-icons": "^1.3.2", + "@radix-ui/react-label": "^2.1.1", "@radix-ui/react-tooltip": "^1.1.4", "class-variance-authority": "^0.7.1", "tailwind-merge": "^2.6.0", @@ -35804,7 +35847,8 @@ "@meshsdk/core-cst": "1.8.4", "@meshsdk/transaction": "1.8.4", "@nufi/dapp-client-cardano": "0.3.5", - "@nufi/dapp-client-core": "0.3.5" + "@nufi/dapp-client-core": "0.3.5", + "@simplewebauthn/browser": "^13.0.0" }, "devDependencies": { "@meshsdk/configs": "*", diff --git a/packages/mesh-provider/src/hydra/transactions/commit-utxo.ts b/packages/mesh-provider/src/hydra/transactions/commit-utxo.ts deleted file mode 100644 index 71011f8f3..000000000 --- a/packages/mesh-provider/src/hydra/transactions/commit-utxo.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Asset, UTxO } from "@meshsdk/core"; -import { csl } from "@meshsdk/core-csl"; - -export async function commitUtxo(utxo: UTxO) { - const txBuilder = csl.TransactionBuilder.new( - csl.TransactionBuilderConfigBuilder.new() - .fee_algo(csl.LinearFee.new(csl.BigNum.zero(), csl.BigNum.zero())) - .pool_deposit(csl.BigNum.from_str("500000000")) - .key_deposit(csl.BigNum.from_str("2000000")) - .max_value_size(5000) - .max_tx_size(16384) - .coins_per_utxo_byte(csl.BigNum.zero()) - .ex_unit_prices( - csl.ExUnitPrices.new( - csl.UnitInterval.new(csl.BigNum.zero(), csl.BigNum.one()), - csl.UnitInterval.new(csl.BigNum.zero(), csl.BigNum.one()), - ), - ) - .ref_script_coins_per_byte( - csl.UnitInterval.new(csl.BigNum.zero(), csl.BigNum.one()), - ) - .deduplicate_explicit_ref_inputs_with_regular_inputs(true) - .build(), - ); -} diff --git a/packages/mesh-react/package.json b/packages/mesh-react/package.json index 94a042891..9e321b16f 100644 --- a/packages/mesh-react/package.json +++ b/packages/mesh-react/package.json @@ -36,6 +36,7 @@ "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-icons": "^1.3.2", + "@radix-ui/react-label": "^2.1.1", "@radix-ui/react-tooltip": "^1.1.4", "class-variance-authority": "^0.7.1", "tailwindcss-animate": "^1.0.7", diff --git a/packages/mesh-react/src/cardano-wallet/data.ts b/packages/mesh-react/src/cardano-wallet/data.ts index c03e80e1b..9e5ee1547 100644 --- a/packages/mesh-react/src/cardano-wallet/data.ts +++ b/packages/mesh-react/src/cardano-wallet/data.ts @@ -11,4 +11,9 @@ export const screens = { subtitle: "Instantly create a new burner wallet. No seed phrase required, keys are generated on your device.", }, + webauthn: { + title: "Passkey", + subtitle: + "Derive self-custody wallet on Chrome, Safari, or Firefox browsers on Android, iOS, macOS, and Windows devices, or using password managers.", + }, }; diff --git a/packages/mesh-react/src/cardano-wallet/index.tsx b/packages/mesh-react/src/cardano-wallet/index.tsx index e79bbc5fd..383c99ecf 100644 --- a/packages/mesh-react/src/cardano-wallet/index.tsx +++ b/packages/mesh-react/src/cardano-wallet/index.tsx @@ -19,6 +19,7 @@ import { screens } from "./data"; import ScreenBurner from "./screen-burner"; import ScreenMain from "./screen-main"; import ScreenP2P from "./screen-p2p"; +import ScreenWebauthn from "./screen-webauthn"; interface ButtonProps { label?: string; @@ -39,6 +40,11 @@ interface ButtonProps { networkId: 0 | 1; provider: IFetcher & ISubmitter; }; + webauthn?: { + networkId: 0 | 1; + provider: IFetcher & ISubmitter; + url: string; + }; } export const CardanoWallet = ({ @@ -49,13 +55,14 @@ export const CardanoWallet = ({ metamask = undefined, cardanoPeerConnect = undefined, burnerWallet = undefined, + webauthn = undefined, }: ButtonProps) => { const [open, setOpen] = useState(false); const [screen, setScreen] = useState("main"); const { wallet, connected } = useWallet(); useEffect(() => { - if (connected && wallet) { + if (connected) { if (onConnected) onConnected(); } }, [connected, wallet]); @@ -86,6 +93,7 @@ export const CardanoWallet = ({ setScreen={setScreen} cardanoPeerConnect={cardanoPeerConnect != undefined} burnerWallet={burnerWallet != undefined} + webauthn={webauthn != undefined} /> )} {screen == "p2p" && ( @@ -101,7 +109,14 @@ export const CardanoWallet = ({ setOpen={setOpen} /> )} - + {screen == "webauthn" && webauthn && ( + + )} diff --git a/packages/mesh-react/src/cardano-wallet/screen-main.tsx b/packages/mesh-react/src/cardano-wallet/screen-main.tsx index d4aba8ca3..707cad5d4 100644 --- a/packages/mesh-react/src/cardano-wallet/screen-main.tsx +++ b/packages/mesh-react/src/cardano-wallet/screen-main.tsx @@ -1,5 +1,6 @@ import IconBookDashed from "../common/icons/icon-book-dashed"; import IconDownload from "../common/icons/icon-download"; +import IconFingerprint from "../common/icons/icon-fingerprint"; import IconMonitorSmartphone from "../common/icons/icon-monitor-smartphone"; import { TooltipProvider } from "../common/tooltip"; import { useWallet, useWalletList } from "../hooks"; @@ -13,6 +14,7 @@ export default function ScreenMain({ setScreen, cardanoPeerConnect, burnerWallet, + webauthn, }: { metamask?: { network: string; @@ -22,6 +24,7 @@ export default function ScreenMain({ setScreen: Function; cardanoPeerConnect: boolean; burnerWallet: boolean; + webauthn: boolean; }) { const wallets = useWalletList({ metamask }); const { connect } = useWallet(); @@ -41,6 +44,15 @@ export default function ScreenMain({ /> ))} + {webauthn && ( + { + setScreen("webauthn"); + }} + /> + )} {cardanoPeerConnect && ( )} - {burnerWallet && ( (""); + const [password, setPassword] = useState(""); + const { setWallet } = useWallet(); + + function createWallet(root: string) { + setTimeout(() => { + const wallet = new MeshWallet({ + networkId: networkId, + fetcher: provider, + submitter: provider, + key: { + type: "root", + bech32: root, + }, + }); + setWallet(wallet, screens.webauthn.title); + setLoading(false); + setOpen(false); + }, 500); + } + + async function handleConnect() { + setLoading(true); + const res = await connect({ username: userName, password, serverUrl: url }); + if (res.success && res.wallet) { + createWallet(res.wallet.bech32PrivateKey); + } + } + + return ( + + {loading ? ( + <>Connecting wallet...> + ) : ( + <> + + + Username + setUserName(e.target.value)} + /> + + Unique to the application you are connecting. + + + + + Unique Code + + setPassword(e.target.value)} + /> + + Additional security to derive your wallet. + + + handleConnect()} + disabled={!userName || userName.length < 6} + > + Connect + + + > + )} + + ); +} diff --git a/packages/mesh-react/src/common/icons/icon-fingerprint.tsx b/packages/mesh-react/src/common/icons/icon-fingerprint.tsx new file mode 100644 index 000000000..dae5b3290 --- /dev/null +++ b/packages/mesh-react/src/common/icons/icon-fingerprint.tsx @@ -0,0 +1,32 @@ +export default function IconFingerprint() { + return ( + + + + + + + + + + + + ); +} diff --git a/packages/mesh-react/src/common/input.tsx b/packages/mesh-react/src/common/input.tsx new file mode 100644 index 000000000..ee2ee5965 --- /dev/null +++ b/packages/mesh-react/src/common/input.tsx @@ -0,0 +1,25 @@ +import * as React from "react"; + +import { cn } from "./cn"; + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ); + }, +); +Input.displayName = "Input"; + +export { Input }; diff --git a/packages/mesh-react/src/common/label.tsx b/packages/mesh-react/src/common/label.tsx new file mode 100644 index 000000000..f3de2ce8c --- /dev/null +++ b/packages/mesh-react/src/common/label.tsx @@ -0,0 +1,25 @@ +import type { VariantProps } from "class-variance-authority"; +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { cva } from "class-variance-authority"; + +import { cn } from "./cn"; + +const labelVariants = cva( + "mesh-text-sm mesh-font-medium mesh-leading-none peer-disabled:mesh-cursor-not-allowed peer-disabled:mesh-opacity-70", +); + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)); +Label.displayName = LabelPrimitive.Root.displayName; + +export { Label }; diff --git a/packages/mesh-wallet/package.json b/packages/mesh-wallet/package.json index 802b2ec3a..7e4eca82a 100644 --- a/packages/mesh-wallet/package.json +++ b/packages/mesh-wallet/package.json @@ -40,7 +40,8 @@ "@meshsdk/core-cst": "1.8.5", "@meshsdk/transaction": "1.8.5", "@nufi/dapp-client-cardano": "0.3.5", - "@nufi/dapp-client-core": "0.3.5" + "@nufi/dapp-client-core": "0.3.5", + "@simplewebauthn/browser": "^13.0.0" }, "prettier": "@meshsdk/configs/prettier", "publishConfig": { diff --git a/packages/mesh-wallet/src/browser/browser-wallet.ts b/packages/mesh-wallet/src/browser/browser-wallet.ts new file mode 100644 index 000000000..da0045284 --- /dev/null +++ b/packages/mesh-wallet/src/browser/browser-wallet.ts @@ -0,0 +1,669 @@ +import { + Asset, + AssetExtended, + DataSignature, + DEFAULT_PROTOCOL_PARAMETERS, + fromUTF8, + IWallet, + POLICY_ID_LENGTH, + resolveFingerprint, + UTxO, + Wallet, +} from "@meshsdk/common"; +import { csl } from "@meshsdk/core-csl"; +import { + Address, + addressToBech32, + CardanoSDKUtil, + deserializeAddress, + deserializeTx, + deserializeTxUnspentOutput, + deserializeValue, + Ed25519KeyHashHex, + Ed25519PublicKey, + Ed25519PublicKeyHex, + fromTxUnspentOutput, + fromValue, + Serialization, + toAddress, + Transaction, + TransactionUnspentOutput, + VkeyWitness, +} from "@meshsdk/core-cst"; + +import { Cardano, WalletInstance } from "../types"; +import { checkIfMetamaskInstalled } from "./metamask"; + +declare global { + interface Window { + cardano: Cardano; + } +} + +/** + * Browser Wallet provides a set of APIs to interact with the blockchain. This wallet is compatible with Mesh transaction builders. + * + * These wallets APIs are in accordance to CIP-30, which defines the API for dApps to communicate with the user's wallet. Additional utility functions provided for developers that are useful for building dApps. + * ```javascript + * import { BrowserWallet } from '@meshsdk/core'; + * + * const wallet = await BrowserWallet.enable('eternl'); + * ``` + */ +export class BrowserWallet implements IWallet { + walletInstance: WalletInstance; + + private constructor( + readonly _walletInstance: WalletInstance, + readonly _walletName: string, + ) { + this.walletInstance = { ..._walletInstance }; + } + + /** + * Returns a list of wallets installed on user's device. Each wallet is an object with the following properties: + * - A name is provided to display wallet's name on the user interface. + * - A version is provided to display wallet's version on the user interface. + * - An icon is provided to display wallet's icon on the user interface. + * + * @returns a list of wallet names + */ + static async getAvailableWallets({ + metamask = undefined, + }: { + metamask?: { + network: string; + }; + } = {}): Promise { + if (window === undefined) return []; + + if (metamask) await checkIfMetamaskInstalled(metamask.network); + + return BrowserWallet.getInstalledWallets(); + } + + /** + * Returns a list of wallets installed on user's device. Each wallet is an object with the following properties: + * - A name is provided to display wallet's name on the user interface. + * - A version is provided to display wallet's version on the user interface. + * - An icon is provided to display wallet's icon on the user interface. + * + * @returns a list of wallet names + */ + static getInstalledWallets(): Wallet[] { + if (window === undefined) return []; + if (window.cardano === undefined) return []; + + let wallets: Wallet[] = []; + for (const key in window.cardano) { + try { + const _wallet = window.cardano[key]; + if (_wallet === undefined) continue; + if (_wallet.name === undefined) continue; + if (_wallet.icon === undefined) continue; + if (_wallet.apiVersion === undefined) continue; + wallets.push({ + id: key, + name: key == "nufiSnap" ? "MetaMask" : _wallet.name, + icon: _wallet.icon, + version: _wallet.apiVersion, + }); + } catch (e) {} + } + + return wallets; + } + + /** + * This is the entrypoint to start communication with the user's wallet. The wallet should request the user's permission to connect the web page to the user's wallet, and if permission has been granted, the wallet will be returned and exposing the full API for the dApp to use. + * + * Query BrowserWallet.getInstalledWallets() to get a list of available wallets, then provide the wallet name for which wallet the user would like to connect with. + * + * @param walletName - the name of the wallet to enable (e.g. "eternl", "begin", "nufiSnap") + * @param extensions - optional, a list of CIPs that the wallet should support + * @returns WalletInstance + */ + static async enable( + walletName: string, + extensions: number[] = [], + ): Promise { + try { + const walletInstance = await BrowserWallet.resolveInstance( + walletName, + extensions, + ); + + if (walletInstance !== undefined) + return new BrowserWallet(walletInstance, walletName); + + throw new Error(`Couldn't create an instance of wallet: ${walletName}`); + } catch (error) { + throw new Error( + `[BrowserWallet] An error occurred during enable: ${JSON.stringify( + error, + )}.`, + ); + } + } + + /** + * Returns a list of assets in the wallet. This API will return every assets in the wallet. Each asset is an object with the following properties: + * - A unit is provided to display asset's name on the user interface. + * - A quantity is provided to display asset's quantity on the user interface. + * + * @returns a list of assets and their quantities + */ + async getBalance(): Promise { + const balance = await this._walletInstance.getBalance(); + return fromValue(deserializeValue(balance)); + } + + /** + * Returns an address owned by the wallet that should be used as a change address to return leftover assets during transaction creation back to the connected wallet. + * + * @returns an address + */ + async getChangeAddress(): Promise { + const changeAddress = await this._walletInstance.getChangeAddress(); + return addressToBech32(deserializeAddress(changeAddress)); + } + + /** + * This function shall return a list of one or more UTXOs (unspent transaction outputs) controlled by the wallet that are required to reach AT LEAST the combined ADA value target specified in amount AND the best suitable to be used as collateral inputs for transactions with plutus script inputs (pure ADA-only UTXOs). + * + * If this cannot be attained, an error message with an explanation of the blocking problem shall be returned. NOTE: wallets are free to return UTXOs that add up to a greater total ADA value than requested in the amount parameter, but wallets must never return any result where UTXOs would sum up to a smaller total ADA value, instead in a case like that an error message must be returned. + * + * @param limit + * @returns a list of UTXOs + */ + async getCollateral(): Promise { + const deserializedCollateral = await this.getCollateralUnspentOutput(); + return deserializedCollateral.map((dc) => fromTxUnspentOutput(dc)); + } + + /** + * Return a list of supported CIPs of the wallet. + * + * @returns a list of CIPs + */ + async getExtensions(): Promise { + try { + const _extensions: { cip: number }[] = + await this._walletInstance.getExtensions(); + return _extensions.map((e) => e.cip); + } catch (e) { + return []; + } + } + + /** + * Returns the network ID of the currently connected account. 0 is testnet and 1 is mainnet but other networks can possibly be returned by wallets. Those other network ID values are not governed by CIP-30. This result will stay the same unless the connected account has changed. + * + * @returns network ID + */ + getNetworkId(): Promise { + return this._walletInstance.getNetworkId(); + } + + /** + * Returns a list of reward addresses owned by the wallet. A reward address is a stake address that is used to receive rewards from staking, generally starts from `stake` prefix. + * + * @returns a list of reward addresses + */ + async getRewardAddresses(): Promise { + const rewardAddresses = await this._walletInstance.getRewardAddresses(); + return rewardAddresses.map((ra) => addressToBech32(deserializeAddress(ra))); + } + + /** + * Returns a list of unused addresses controlled by the wallet. + * + * @returns a list of unused addresses + */ + async getUnusedAddresses(): Promise { + const unusedAddresses = await this._walletInstance.getUnusedAddresses(); + return unusedAddresses.map((una) => + addressToBech32(deserializeAddress(una)), + ); + } + + /** + * Returns a list of used addresses controlled by the wallet. + * + * @returns a list of used addresses + */ + async getUsedAddresses(): Promise { + const usedAddresses = await this._walletInstance.getUsedAddresses(); + return usedAddresses.map((usa) => addressToBech32(deserializeAddress(usa))); + } + + /** + * Return a list of all UTXOs (unspent transaction outputs) controlled by the wallet. + * + * @returns a list of UTXOs + */ + async getUtxos(): Promise { + const deserializedUTxOs = await this.getUsedUTxOs(); + return deserializedUTxOs.map((du) => fromTxUnspentOutput(du)); + } + + /** + * This endpoint utilizes the [CIP-8 - Message Signing](https://cips.cardano.org/cips/cip8/) to sign arbitrary data, to verify the data was signed by the owner of the private key. + * + * @param payload - the data to be signed + * @param address - optional, if not provided, the first staking address will be used + * @returns a signature + */ + async signData(payload: string, address?: string): Promise { + if (address === undefined) { + address = (await this.getUsedAddresses())[0]!; + if (address === undefined) { + address = await this.getChangeAddress(); + } + } + + if (address.startsWith("drep1")) { + return this._walletInstance.cip95!.signData(address, fromUTF8(payload)); + } + + const signerAddress = toAddress(address).toBytes().toString(); + return this._walletInstance.signData(signerAddress, fromUTF8(payload)); + } + + /** + * Requests user to sign the provided transaction (tx). The wallet should ask the user for permission, and if given, try to sign the supplied body and return a signed transaction. partialSign should be true if the transaction provided requires multiple signatures. + * + * @param unsignedTx - a transaction in CBOR + * @param partialSign - if the transaction is signed partially + * @returns a signed transaction in CBOR + */ + async signTx(unsignedTx: string, partialSign = false): Promise { + const witness = await this._walletInstance.signTx(unsignedTx, partialSign); + if (witness === "") { + return unsignedTx; + } + return BrowserWallet.addBrowserWitnesses(unsignedTx, witness); + } + + /** + * Experimental feature - sign multiple transactions at once (Supported wallet(s): Typhon) + * + * @param unsignedTxs - array of unsigned transactions in CborHex string + * @param partialSign - if the transactions are signed partially + * @returns array of signed transactions CborHex string + */ + async signTxs(unsignedTxs: string[], partialSign = false): Promise { + let witnessSets: string[] | undefined = undefined; + // Hardcoded behavior customized for different wallet for now as there is no standard confirmed + switch (this._walletName) { + case "Typhon Wallet": + if (this._walletInstance.signTxs) { + witnessSets = await this._walletInstance.signTxs( + unsignedTxs, + partialSign, + ); + } + break; + default: + if (this._walletInstance.signTxs) { + witnessSets = await this._walletInstance.signTxs( + unsignedTxs.map((cbor) => ({ + cbor, + partialSign, + })), + ); + } else if (this._walletInstance.experimental.signTxs) { + witnessSets = await this._walletInstance.experimental.signTxs( + unsignedTxs.map((cbor) => ({ + cbor, + partialSign, + })), + ); + } + break; + } + + if (!witnessSets) throw new Error("Wallet does not support signTxs"); + + const signedTxs: string[] = []; + for (let i = 0; i < witnessSets.length; i++) { + const unsignedTx = unsignedTxs[i]!; + const cWitness = witnessSets[i]!; + if (cWitness === "") { + // It's possible that txs are signed just to give + // browser wallet the tx context + signedTxs.push(unsignedTx); + } else { + const signedTx = BrowserWallet.addBrowserWitnesses( + unsignedTx, + cWitness, + ); + signedTxs.push(signedTx); + } + } + + return signedTxs; + } + + /** + * Submits the signed transaction to the blockchain network. + * + * As wallets should already have this ability to submit transaction, we allow dApps to request that a transaction be sent through it. If the wallet accepts the transaction and tries to send it, it shall return the transaction ID for the dApp to track. The wallet can return error messages or failure if there was an error in sending it. + * + * @param tx + * @returns a transaction hash + */ + submitTx(tx: string): Promise { + return this._walletInstance.submitTx(tx); + } + + /** + * Get a used address of type Address from the wallet. + * + * This is used in transaction building. + * + * @returns an Address object + */ + async getUsedAddress(): Promise { + const usedAddresses = await this._walletInstance.getUsedAddresses(); + if (usedAddresses.length === 0) throw new Error("No used addresses found"); + return deserializeAddress(usedAddresses[0]!); + } + + /** + * Get a list of UTXOs to be used as collateral inputs for transactions with plutus script inputs. + * + * This is used in transaction building. + * + * @returns a list of UTXOs + */ + async getCollateralUnspentOutput( + limit = DEFAULT_PROTOCOL_PARAMETERS.maxCollateralInputs, + ): Promise { + let collateral: string[] = []; + try { + collateral = (await this._walletInstance.getCollateral()) ?? []; + } catch (e) { + try { + collateral = + (await this._walletInstance.experimental.getCollateral()) ?? []; + } catch (e) { + console.error(e); + } + } + return collateral.map((c) => deserializeTxUnspentOutput(c)).slice(0, limit); + } + + /** + * Get a list of UTXOs to be used for transaction building. + * + * This is used in transaction building. + * + * @returns a list of UTXOs + */ + async getUsedUTxOs(): Promise { + const utxos = (await this._walletInstance.getUtxos()) ?? []; + return utxos.map((u) => deserializeTxUnspentOutput(u)); + } + + /** + * A helper function to get the assets in the wallet. + * + * @returns a list of assets + */ + async getAssets(): Promise { + const balance = await this.getBalance(); + return balance + .filter((v) => v.unit !== "lovelace") + .map((v) => { + const policyId = v.unit.slice(0, POLICY_ID_LENGTH); + const assetName = v.unit.slice(POLICY_ID_LENGTH); + const fingerprint = resolveFingerprint(policyId, assetName); + + return { + unit: v.unit, + policyId, + assetName, + fingerprint, + quantity: v.quantity, + }; + }); + } + + /** + * A helper function to get the lovelace balance in the wallet. + * + * @returns lovelace balance + */ + async getLovelace(): Promise { + const balance = await this.getBalance(); + const nativeAsset = balance.find((v) => v.unit === "lovelace"); + + return nativeAsset !== undefined ? nativeAsset.quantity : "0"; + } + + /** + * A helper function to get the assets of a specific policy ID in the wallet. + * + * @param policyId + * @returns a list of assets + */ + async getPolicyIdAssets(policyId: string): Promise { + const assets = await this.getAssets(); + return assets.filter((v) => v.policyId === policyId); + } + + /** + * A helper function to get the policy IDs of all the assets in the wallet. + * + * @returns a list of policy IDs + */ + async getPolicyIds(): Promise { + const balance = await this.getBalance(); + return Array.from( + new Set(balance.map((v) => v.unit.slice(0, POLICY_ID_LENGTH))), + ).filter((p) => p !== "lovelace"); + } + + /** + * The connected wallet account provides the account's public DRep Key, derivation as described in CIP-0105. + * These are used by the client to identify the user's on-chain CIP-1694 interactions, i.e. if a user has registered to be a DRep. + * + * @returns wallet account's public DRep Key + */ + async getDRep(): Promise< + | { + publicKey: string; + publicKeyHash: string; + dRepIDCip105: string; + } + | undefined + > { + try { + if (this._walletInstance.cip95 === undefined) return undefined; + + const dRepKey = await this._walletInstance.cip95.getPubDRepKey(); + const { dRepIDHash } = await BrowserWallet.dRepKeyToDRepID(dRepKey); + + // const networkId = await this.getNetworkId(); + // const dRepId = buildDRepID(dRepKey, networkId); // todo: this is not correct + + const csldRepIdKeyHash = csl.PublicKey.from_hex(dRepKey).hash(); // todo: need to replace CST + const dRepId = csldRepIdKeyHash.to_bech32("drep"); + + return { + publicKey: dRepKey, + publicKeyHash: dRepIDHash, + dRepIDCip105: dRepId, + }; + } catch (e) { + console.error(e); + return undefined; + } + } + + /** + * The connected wallet account provides the account's public DRep Key, derivation as described in CIP-0105. + * These are used by the client to identify the user's on-chain CIP-1694 interactions, i.e. if a user has registered to be a DRep. + * + * @returns wallet account's public DRep Key + */ + async getPubDRepKey(): Promise< + | { + pubDRepKey: string; + dRepIDHash: string; + dRepIDBech32: string; + } + | undefined + > { + try { + if (this._walletInstance.cip95 === undefined) return undefined; + + const dRepKey = await this._walletInstance.cip95.getPubDRepKey(); + const { dRepIDHash } = await BrowserWallet.dRepKeyToDRepID(dRepKey); + + // const networkId = await this.getNetworkId(); + // const dRepId = buildDRepID(dRepKey, networkId); // todo: this is not correct + + const csldRepIdKeyHash = csl.PublicKey.from_hex(dRepKey).hash(); // todo: need to replace CST + const dRepId = csldRepIdKeyHash.to_bech32("drep"); + + return { + pubDRepKey: dRepKey, + dRepIDHash: dRepIDHash, + dRepIDBech32: dRepId, + }; + } catch (e) { + console.error(e); + return undefined; + } + } + + async getRegisteredPubStakeKeys(): Promise< + | { + pubStakeKeys: string[]; + pubStakeKeyHashes: string[]; + } + | undefined + > { + try { + if (this._walletInstance.cip95 === undefined) return undefined; + + const pubStakeKeys = + await this._walletInstance.cip95.getRegisteredPubStakeKeys(); + + const pubStakeKeyHashes = await Promise.all( + pubStakeKeys.map(async (pubStakeKey) => { + const { dRepIDHash } = + await BrowserWallet.dRepKeyToDRepID(pubStakeKey); + return dRepIDHash; + }), + ); + + return { + pubStakeKeys: pubStakeKeys, + pubStakeKeyHashes: pubStakeKeyHashes, + }; + } catch (e) { + console.error(e); + return undefined; + } + } + + async getUnregisteredPubStakeKeys(): Promise< + | { + pubStakeKeys: string[]; + pubStakeKeyHashes: string[]; + } + | undefined + > { + try { + if (this._walletInstance.cip95 === undefined) return undefined; + + const pubStakeKeys = + await this._walletInstance.cip95.getUnregisteredPubStakeKeys(); + + const pubStakeKeyHashes = await Promise.all( + pubStakeKeys.map(async (pubStakeKey) => { + const { dRepIDHash } = + await BrowserWallet.dRepKeyToDRepID(pubStakeKey); + return dRepIDHash; + }), + ); + + return { + pubStakeKeys: pubStakeKeys, + pubStakeKeyHashes: pubStakeKeyHashes, + }; + } catch (e) { + console.error(e); + return undefined; + } + } + + private static async dRepKeyToDRepID(dRepKey: string): Promise<{ + dRepKeyHex: Ed25519PublicKeyHex; + dRepID: Ed25519PublicKey; + dRepIDHash: Ed25519KeyHashHex; + }> { + const dRepKeyHex = Ed25519PublicKeyHex(dRepKey); + const dRepID = Ed25519PublicKey.fromHex(dRepKeyHex); + const dRepIDHash = (await dRepID.hash()).hex(); + return { + dRepKeyHex, + dRepID, + dRepIDHash, + }; + } + + private static resolveInstance( + walletName: string, + extensions: number[] = [], + ) { + if (window.cardano === undefined) return undefined; + if (window.cardano[walletName] === undefined) return undefined; + + const wallet = window.cardano[walletName]; + + if (extensions.length > 0) { + const _extensions = extensions.map((e) => ({ cip: e })); + return wallet.enable({ extensions: _extensions }); + } else { + return wallet?.enable(); + } + } + + static addBrowserWitnesses(unsignedTx: string, witnesses: string) { + const cWitness = Serialization.TransactionWitnessSet.fromCbor( + CardanoSDKUtil.HexBlob(witnesses), + ) + .vkeys() + ?.values(); + + if (cWitness === undefined) { + return unsignedTx; + } + + let tx = deserializeTx(unsignedTx); + // let tx = Transaction.fromCbor(CardanoSDK.TxCBOR(txHex)); + let witnessSet = tx.witnessSet(); + let witnessSetVkeys = witnessSet.vkeys(); + let witnessSetVkeysValues: Serialization.VkeyWitness[] = witnessSetVkeys + ? [...witnessSetVkeys.values(), ...cWitness] + : [...cWitness]; + witnessSet.setVkeys( + Serialization.CborSet.fromCore( + witnessSetVkeysValues.map((vkw) => vkw.toCore()), + VkeyWitness.fromCore, + ), + ); + + return new Transaction(tx.body(), witnessSet, tx.auxiliaryData()).toCbor(); + } + + static getSupportedExtensions(wallet: string) { + const _supportedExtensions = window?.cardano?.[wallet]?.supportedExtensions; + if (_supportedExtensions) return _supportedExtensions; + else return []; + } +} diff --git a/packages/mesh-wallet/src/browser/index.ts b/packages/mesh-wallet/src/browser/index.ts index da0045284..2bd9b6539 100644 --- a/packages/mesh-wallet/src/browser/index.ts +++ b/packages/mesh-wallet/src/browser/index.ts @@ -1,669 +1,2 @@ -import { - Asset, - AssetExtended, - DataSignature, - DEFAULT_PROTOCOL_PARAMETERS, - fromUTF8, - IWallet, - POLICY_ID_LENGTH, - resolveFingerprint, - UTxO, - Wallet, -} from "@meshsdk/common"; -import { csl } from "@meshsdk/core-csl"; -import { - Address, - addressToBech32, - CardanoSDKUtil, - deserializeAddress, - deserializeTx, - deserializeTxUnspentOutput, - deserializeValue, - Ed25519KeyHashHex, - Ed25519PublicKey, - Ed25519PublicKeyHex, - fromTxUnspentOutput, - fromValue, - Serialization, - toAddress, - Transaction, - TransactionUnspentOutput, - VkeyWitness, -} from "@meshsdk/core-cst"; - -import { Cardano, WalletInstance } from "../types"; -import { checkIfMetamaskInstalled } from "./metamask"; - -declare global { - interface Window { - cardano: Cardano; - } -} - -/** - * Browser Wallet provides a set of APIs to interact with the blockchain. This wallet is compatible with Mesh transaction builders. - * - * These wallets APIs are in accordance to CIP-30, which defines the API for dApps to communicate with the user's wallet. Additional utility functions provided for developers that are useful for building dApps. - * ```javascript - * import { BrowserWallet } from '@meshsdk/core'; - * - * const wallet = await BrowserWallet.enable('eternl'); - * ``` - */ -export class BrowserWallet implements IWallet { - walletInstance: WalletInstance; - - private constructor( - readonly _walletInstance: WalletInstance, - readonly _walletName: string, - ) { - this.walletInstance = { ..._walletInstance }; - } - - /** - * Returns a list of wallets installed on user's device. Each wallet is an object with the following properties: - * - A name is provided to display wallet's name on the user interface. - * - A version is provided to display wallet's version on the user interface. - * - An icon is provided to display wallet's icon on the user interface. - * - * @returns a list of wallet names - */ - static async getAvailableWallets({ - metamask = undefined, - }: { - metamask?: { - network: string; - }; - } = {}): Promise { - if (window === undefined) return []; - - if (metamask) await checkIfMetamaskInstalled(metamask.network); - - return BrowserWallet.getInstalledWallets(); - } - - /** - * Returns a list of wallets installed on user's device. Each wallet is an object with the following properties: - * - A name is provided to display wallet's name on the user interface. - * - A version is provided to display wallet's version on the user interface. - * - An icon is provided to display wallet's icon on the user interface. - * - * @returns a list of wallet names - */ - static getInstalledWallets(): Wallet[] { - if (window === undefined) return []; - if (window.cardano === undefined) return []; - - let wallets: Wallet[] = []; - for (const key in window.cardano) { - try { - const _wallet = window.cardano[key]; - if (_wallet === undefined) continue; - if (_wallet.name === undefined) continue; - if (_wallet.icon === undefined) continue; - if (_wallet.apiVersion === undefined) continue; - wallets.push({ - id: key, - name: key == "nufiSnap" ? "MetaMask" : _wallet.name, - icon: _wallet.icon, - version: _wallet.apiVersion, - }); - } catch (e) {} - } - - return wallets; - } - - /** - * This is the entrypoint to start communication with the user's wallet. The wallet should request the user's permission to connect the web page to the user's wallet, and if permission has been granted, the wallet will be returned and exposing the full API for the dApp to use. - * - * Query BrowserWallet.getInstalledWallets() to get a list of available wallets, then provide the wallet name for which wallet the user would like to connect with. - * - * @param walletName - the name of the wallet to enable (e.g. "eternl", "begin", "nufiSnap") - * @param extensions - optional, a list of CIPs that the wallet should support - * @returns WalletInstance - */ - static async enable( - walletName: string, - extensions: number[] = [], - ): Promise { - try { - const walletInstance = await BrowserWallet.resolveInstance( - walletName, - extensions, - ); - - if (walletInstance !== undefined) - return new BrowserWallet(walletInstance, walletName); - - throw new Error(`Couldn't create an instance of wallet: ${walletName}`); - } catch (error) { - throw new Error( - `[BrowserWallet] An error occurred during enable: ${JSON.stringify( - error, - )}.`, - ); - } - } - - /** - * Returns a list of assets in the wallet. This API will return every assets in the wallet. Each asset is an object with the following properties: - * - A unit is provided to display asset's name on the user interface. - * - A quantity is provided to display asset's quantity on the user interface. - * - * @returns a list of assets and their quantities - */ - async getBalance(): Promise { - const balance = await this._walletInstance.getBalance(); - return fromValue(deserializeValue(balance)); - } - - /** - * Returns an address owned by the wallet that should be used as a change address to return leftover assets during transaction creation back to the connected wallet. - * - * @returns an address - */ - async getChangeAddress(): Promise { - const changeAddress = await this._walletInstance.getChangeAddress(); - return addressToBech32(deserializeAddress(changeAddress)); - } - - /** - * This function shall return a list of one or more UTXOs (unspent transaction outputs) controlled by the wallet that are required to reach AT LEAST the combined ADA value target specified in amount AND the best suitable to be used as collateral inputs for transactions with plutus script inputs (pure ADA-only UTXOs). - * - * If this cannot be attained, an error message with an explanation of the blocking problem shall be returned. NOTE: wallets are free to return UTXOs that add up to a greater total ADA value than requested in the amount parameter, but wallets must never return any result where UTXOs would sum up to a smaller total ADA value, instead in a case like that an error message must be returned. - * - * @param limit - * @returns a list of UTXOs - */ - async getCollateral(): Promise { - const deserializedCollateral = await this.getCollateralUnspentOutput(); - return deserializedCollateral.map((dc) => fromTxUnspentOutput(dc)); - } - - /** - * Return a list of supported CIPs of the wallet. - * - * @returns a list of CIPs - */ - async getExtensions(): Promise { - try { - const _extensions: { cip: number }[] = - await this._walletInstance.getExtensions(); - return _extensions.map((e) => e.cip); - } catch (e) { - return []; - } - } - - /** - * Returns the network ID of the currently connected account. 0 is testnet and 1 is mainnet but other networks can possibly be returned by wallets. Those other network ID values are not governed by CIP-30. This result will stay the same unless the connected account has changed. - * - * @returns network ID - */ - getNetworkId(): Promise { - return this._walletInstance.getNetworkId(); - } - - /** - * Returns a list of reward addresses owned by the wallet. A reward address is a stake address that is used to receive rewards from staking, generally starts from `stake` prefix. - * - * @returns a list of reward addresses - */ - async getRewardAddresses(): Promise { - const rewardAddresses = await this._walletInstance.getRewardAddresses(); - return rewardAddresses.map((ra) => addressToBech32(deserializeAddress(ra))); - } - - /** - * Returns a list of unused addresses controlled by the wallet. - * - * @returns a list of unused addresses - */ - async getUnusedAddresses(): Promise { - const unusedAddresses = await this._walletInstance.getUnusedAddresses(); - return unusedAddresses.map((una) => - addressToBech32(deserializeAddress(una)), - ); - } - - /** - * Returns a list of used addresses controlled by the wallet. - * - * @returns a list of used addresses - */ - async getUsedAddresses(): Promise { - const usedAddresses = await this._walletInstance.getUsedAddresses(); - return usedAddresses.map((usa) => addressToBech32(deserializeAddress(usa))); - } - - /** - * Return a list of all UTXOs (unspent transaction outputs) controlled by the wallet. - * - * @returns a list of UTXOs - */ - async getUtxos(): Promise { - const deserializedUTxOs = await this.getUsedUTxOs(); - return deserializedUTxOs.map((du) => fromTxUnspentOutput(du)); - } - - /** - * This endpoint utilizes the [CIP-8 - Message Signing](https://cips.cardano.org/cips/cip8/) to sign arbitrary data, to verify the data was signed by the owner of the private key. - * - * @param payload - the data to be signed - * @param address - optional, if not provided, the first staking address will be used - * @returns a signature - */ - async signData(payload: string, address?: string): Promise { - if (address === undefined) { - address = (await this.getUsedAddresses())[0]!; - if (address === undefined) { - address = await this.getChangeAddress(); - } - } - - if (address.startsWith("drep1")) { - return this._walletInstance.cip95!.signData(address, fromUTF8(payload)); - } - - const signerAddress = toAddress(address).toBytes().toString(); - return this._walletInstance.signData(signerAddress, fromUTF8(payload)); - } - - /** - * Requests user to sign the provided transaction (tx). The wallet should ask the user for permission, and if given, try to sign the supplied body and return a signed transaction. partialSign should be true if the transaction provided requires multiple signatures. - * - * @param unsignedTx - a transaction in CBOR - * @param partialSign - if the transaction is signed partially - * @returns a signed transaction in CBOR - */ - async signTx(unsignedTx: string, partialSign = false): Promise { - const witness = await this._walletInstance.signTx(unsignedTx, partialSign); - if (witness === "") { - return unsignedTx; - } - return BrowserWallet.addBrowserWitnesses(unsignedTx, witness); - } - - /** - * Experimental feature - sign multiple transactions at once (Supported wallet(s): Typhon) - * - * @param unsignedTxs - array of unsigned transactions in CborHex string - * @param partialSign - if the transactions are signed partially - * @returns array of signed transactions CborHex string - */ - async signTxs(unsignedTxs: string[], partialSign = false): Promise { - let witnessSets: string[] | undefined = undefined; - // Hardcoded behavior customized for different wallet for now as there is no standard confirmed - switch (this._walletName) { - case "Typhon Wallet": - if (this._walletInstance.signTxs) { - witnessSets = await this._walletInstance.signTxs( - unsignedTxs, - partialSign, - ); - } - break; - default: - if (this._walletInstance.signTxs) { - witnessSets = await this._walletInstance.signTxs( - unsignedTxs.map((cbor) => ({ - cbor, - partialSign, - })), - ); - } else if (this._walletInstance.experimental.signTxs) { - witnessSets = await this._walletInstance.experimental.signTxs( - unsignedTxs.map((cbor) => ({ - cbor, - partialSign, - })), - ); - } - break; - } - - if (!witnessSets) throw new Error("Wallet does not support signTxs"); - - const signedTxs: string[] = []; - for (let i = 0; i < witnessSets.length; i++) { - const unsignedTx = unsignedTxs[i]!; - const cWitness = witnessSets[i]!; - if (cWitness === "") { - // It's possible that txs are signed just to give - // browser wallet the tx context - signedTxs.push(unsignedTx); - } else { - const signedTx = BrowserWallet.addBrowserWitnesses( - unsignedTx, - cWitness, - ); - signedTxs.push(signedTx); - } - } - - return signedTxs; - } - - /** - * Submits the signed transaction to the blockchain network. - * - * As wallets should already have this ability to submit transaction, we allow dApps to request that a transaction be sent through it. If the wallet accepts the transaction and tries to send it, it shall return the transaction ID for the dApp to track. The wallet can return error messages or failure if there was an error in sending it. - * - * @param tx - * @returns a transaction hash - */ - submitTx(tx: string): Promise { - return this._walletInstance.submitTx(tx); - } - - /** - * Get a used address of type Address from the wallet. - * - * This is used in transaction building. - * - * @returns an Address object - */ - async getUsedAddress(): Promise { - const usedAddresses = await this._walletInstance.getUsedAddresses(); - if (usedAddresses.length === 0) throw new Error("No used addresses found"); - return deserializeAddress(usedAddresses[0]!); - } - - /** - * Get a list of UTXOs to be used as collateral inputs for transactions with plutus script inputs. - * - * This is used in transaction building. - * - * @returns a list of UTXOs - */ - async getCollateralUnspentOutput( - limit = DEFAULT_PROTOCOL_PARAMETERS.maxCollateralInputs, - ): Promise { - let collateral: string[] = []; - try { - collateral = (await this._walletInstance.getCollateral()) ?? []; - } catch (e) { - try { - collateral = - (await this._walletInstance.experimental.getCollateral()) ?? []; - } catch (e) { - console.error(e); - } - } - return collateral.map((c) => deserializeTxUnspentOutput(c)).slice(0, limit); - } - - /** - * Get a list of UTXOs to be used for transaction building. - * - * This is used in transaction building. - * - * @returns a list of UTXOs - */ - async getUsedUTxOs(): Promise { - const utxos = (await this._walletInstance.getUtxos()) ?? []; - return utxos.map((u) => deserializeTxUnspentOutput(u)); - } - - /** - * A helper function to get the assets in the wallet. - * - * @returns a list of assets - */ - async getAssets(): Promise { - const balance = await this.getBalance(); - return balance - .filter((v) => v.unit !== "lovelace") - .map((v) => { - const policyId = v.unit.slice(0, POLICY_ID_LENGTH); - const assetName = v.unit.slice(POLICY_ID_LENGTH); - const fingerprint = resolveFingerprint(policyId, assetName); - - return { - unit: v.unit, - policyId, - assetName, - fingerprint, - quantity: v.quantity, - }; - }); - } - - /** - * A helper function to get the lovelace balance in the wallet. - * - * @returns lovelace balance - */ - async getLovelace(): Promise { - const balance = await this.getBalance(); - const nativeAsset = balance.find((v) => v.unit === "lovelace"); - - return nativeAsset !== undefined ? nativeAsset.quantity : "0"; - } - - /** - * A helper function to get the assets of a specific policy ID in the wallet. - * - * @param policyId - * @returns a list of assets - */ - async getPolicyIdAssets(policyId: string): Promise { - const assets = await this.getAssets(); - return assets.filter((v) => v.policyId === policyId); - } - - /** - * A helper function to get the policy IDs of all the assets in the wallet. - * - * @returns a list of policy IDs - */ - async getPolicyIds(): Promise { - const balance = await this.getBalance(); - return Array.from( - new Set(balance.map((v) => v.unit.slice(0, POLICY_ID_LENGTH))), - ).filter((p) => p !== "lovelace"); - } - - /** - * The connected wallet account provides the account's public DRep Key, derivation as described in CIP-0105. - * These are used by the client to identify the user's on-chain CIP-1694 interactions, i.e. if a user has registered to be a DRep. - * - * @returns wallet account's public DRep Key - */ - async getDRep(): Promise< - | { - publicKey: string; - publicKeyHash: string; - dRepIDCip105: string; - } - | undefined - > { - try { - if (this._walletInstance.cip95 === undefined) return undefined; - - const dRepKey = await this._walletInstance.cip95.getPubDRepKey(); - const { dRepIDHash } = await BrowserWallet.dRepKeyToDRepID(dRepKey); - - // const networkId = await this.getNetworkId(); - // const dRepId = buildDRepID(dRepKey, networkId); // todo: this is not correct - - const csldRepIdKeyHash = csl.PublicKey.from_hex(dRepKey).hash(); // todo: need to replace CST - const dRepId = csldRepIdKeyHash.to_bech32("drep"); - - return { - publicKey: dRepKey, - publicKeyHash: dRepIDHash, - dRepIDCip105: dRepId, - }; - } catch (e) { - console.error(e); - return undefined; - } - } - - /** - * The connected wallet account provides the account's public DRep Key, derivation as described in CIP-0105. - * These are used by the client to identify the user's on-chain CIP-1694 interactions, i.e. if a user has registered to be a DRep. - * - * @returns wallet account's public DRep Key - */ - async getPubDRepKey(): Promise< - | { - pubDRepKey: string; - dRepIDHash: string; - dRepIDBech32: string; - } - | undefined - > { - try { - if (this._walletInstance.cip95 === undefined) return undefined; - - const dRepKey = await this._walletInstance.cip95.getPubDRepKey(); - const { dRepIDHash } = await BrowserWallet.dRepKeyToDRepID(dRepKey); - - // const networkId = await this.getNetworkId(); - // const dRepId = buildDRepID(dRepKey, networkId); // todo: this is not correct - - const csldRepIdKeyHash = csl.PublicKey.from_hex(dRepKey).hash(); // todo: need to replace CST - const dRepId = csldRepIdKeyHash.to_bech32("drep"); - - return { - pubDRepKey: dRepKey, - dRepIDHash: dRepIDHash, - dRepIDBech32: dRepId, - }; - } catch (e) { - console.error(e); - return undefined; - } - } - - async getRegisteredPubStakeKeys(): Promise< - | { - pubStakeKeys: string[]; - pubStakeKeyHashes: string[]; - } - | undefined - > { - try { - if (this._walletInstance.cip95 === undefined) return undefined; - - const pubStakeKeys = - await this._walletInstance.cip95.getRegisteredPubStakeKeys(); - - const pubStakeKeyHashes = await Promise.all( - pubStakeKeys.map(async (pubStakeKey) => { - const { dRepIDHash } = - await BrowserWallet.dRepKeyToDRepID(pubStakeKey); - return dRepIDHash; - }), - ); - - return { - pubStakeKeys: pubStakeKeys, - pubStakeKeyHashes: pubStakeKeyHashes, - }; - } catch (e) { - console.error(e); - return undefined; - } - } - - async getUnregisteredPubStakeKeys(): Promise< - | { - pubStakeKeys: string[]; - pubStakeKeyHashes: string[]; - } - | undefined - > { - try { - if (this._walletInstance.cip95 === undefined) return undefined; - - const pubStakeKeys = - await this._walletInstance.cip95.getUnregisteredPubStakeKeys(); - - const pubStakeKeyHashes = await Promise.all( - pubStakeKeys.map(async (pubStakeKey) => { - const { dRepIDHash } = - await BrowserWallet.dRepKeyToDRepID(pubStakeKey); - return dRepIDHash; - }), - ); - - return { - pubStakeKeys: pubStakeKeys, - pubStakeKeyHashes: pubStakeKeyHashes, - }; - } catch (e) { - console.error(e); - return undefined; - } - } - - private static async dRepKeyToDRepID(dRepKey: string): Promise<{ - dRepKeyHex: Ed25519PublicKeyHex; - dRepID: Ed25519PublicKey; - dRepIDHash: Ed25519KeyHashHex; - }> { - const dRepKeyHex = Ed25519PublicKeyHex(dRepKey); - const dRepID = Ed25519PublicKey.fromHex(dRepKeyHex); - const dRepIDHash = (await dRepID.hash()).hex(); - return { - dRepKeyHex, - dRepID, - dRepIDHash, - }; - } - - private static resolveInstance( - walletName: string, - extensions: number[] = [], - ) { - if (window.cardano === undefined) return undefined; - if (window.cardano[walletName] === undefined) return undefined; - - const wallet = window.cardano[walletName]; - - if (extensions.length > 0) { - const _extensions = extensions.map((e) => ({ cip: e })); - return wallet.enable({ extensions: _extensions }); - } else { - return wallet?.enable(); - } - } - - static addBrowserWitnesses(unsignedTx: string, witnesses: string) { - const cWitness = Serialization.TransactionWitnessSet.fromCbor( - CardanoSDKUtil.HexBlob(witnesses), - ) - .vkeys() - ?.values(); - - if (cWitness === undefined) { - return unsignedTx; - } - - let tx = deserializeTx(unsignedTx); - // let tx = Transaction.fromCbor(CardanoSDK.TxCBOR(txHex)); - let witnessSet = tx.witnessSet(); - let witnessSetVkeys = witnessSet.vkeys(); - let witnessSetVkeysValues: Serialization.VkeyWitness[] = witnessSetVkeys - ? [...witnessSetVkeys.values(), ...cWitness] - : [...cWitness]; - witnessSet.setVkeys( - Serialization.CborSet.fromCore( - witnessSetVkeysValues.map((vkw) => vkw.toCore()), - VkeyWitness.fromCore, - ), - ); - - return new Transaction(tx.body(), witnessSet, tx.auxiliaryData()).toCbor(); - } - - static getSupportedExtensions(wallet: string) { - const _supportedExtensions = window?.cardano?.[wallet]?.supportedExtensions; - if (_supportedExtensions) return _supportedExtensions; - else return []; - } -} +export * from "./browser-wallet"; +export * from "./webauthn"; diff --git a/packages/mesh-wallet/src/browser/webauthn/auth/connect.ts b/packages/mesh-wallet/src/browser/webauthn/auth/connect.ts new file mode 100644 index 000000000..4839d3771 --- /dev/null +++ b/packages/mesh-wallet/src/browser/webauthn/auth/connect.ts @@ -0,0 +1,49 @@ +import { buildWalletFromPasskey } from "../cardano/build-wallet-from-passkey"; +import { ERRORCODES } from "../common"; +import { login } from "./login"; +import { register } from "./register"; + +export async function connect({ + username, + password, + serverUrl, +}: { + username: string; + password: string; + serverUrl: string; +}) { + const responseRegister = await register({ serverUrl, username }); + + if ( + responseRegister.success || + responseRegister.errorCode == ERRORCODES.USEREXISTS + ) { + const loginRes = await handleLogin({ serverUrl, username }); + if (loginRes.success && loginRes.authJSON) { + const wallet = await buildWalletFromPasskey( + loginRes.authJSON.rawId, + password, + ); + return { success: true, wallet: wallet }; + } + } else { + return { success: false, error: responseRegister.error }; + } + return { success: false, error: "Fail to connect" }; +} + +async function handleLogin({ + serverUrl, + username, +}: { + serverUrl: string; + username: string; +}) { + const responseLogin = await login({ serverUrl, username }); + if (responseLogin.success && responseLogin.authJSON) { + const authJSON = responseLogin.authJSON; + return { success: true, authJSON: authJSON }; + } else { + return { success: false, error: responseLogin.error }; + } +} diff --git a/packages/mesh-wallet/src/browser/webauthn/auth/index.ts b/packages/mesh-wallet/src/browser/webauthn/auth/index.ts new file mode 100644 index 000000000..b545ce418 --- /dev/null +++ b/packages/mesh-wallet/src/browser/webauthn/auth/index.ts @@ -0,0 +1,3 @@ +export * from "./connect"; +export * from "./login"; +export * from "./register"; diff --git a/packages/mesh-wallet/src/browser/webauthn/auth/login.ts b/packages/mesh-wallet/src/browser/webauthn/auth/login.ts new file mode 100644 index 000000000..2982871ed --- /dev/null +++ b/packages/mesh-wallet/src/browser/webauthn/auth/login.ts @@ -0,0 +1,56 @@ +import { startAuthentication } from "@simplewebauthn/browser"; + +export async function login({ + serverUrl, + username, +}: { + serverUrl: string; + username: string; +}) { + // 1. Get challenge from server + const initAuthRes = await fetch(`${serverUrl}/init-auth`, { + credentials: "include", + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + username, + }), + }); + + const initAuth = await initAuthRes.json(); + if (!initAuth.success) { + return { + success: false, + error: initAuth.error, + errorCode: initAuth.errorCode, + }; + } + + const optionsJSON = initAuth.optionsJSON; + + // 2. Get passkey + const authJSON = await startAuthentication({ optionsJSON }); + + // 3. Verify passkey with DB + const verifyAuthRes = await fetch(`${serverUrl}/verify-auth`, { + credentials: "include", + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(authJSON), + }); + + const verifyAuth = await verifyAuthRes.json(); + + if (!verifyAuthRes.ok) { + return { success: false, error: verifyAuth.error }; + } + if (verifyAuth.verified) { + return { success: true, authJSON: authJSON }; + } else { + return { success: false, error: "Failed to log in" }; + } +} diff --git a/packages/mesh-wallet/src/browser/webauthn/auth/register.ts b/packages/mesh-wallet/src/browser/webauthn/auth/register.ts new file mode 100644 index 000000000..25f87784e --- /dev/null +++ b/packages/mesh-wallet/src/browser/webauthn/auth/register.ts @@ -0,0 +1,60 @@ +import { startRegistration } from "@simplewebauthn/browser"; + +export async function register({ + serverUrl, + username, +}: { + serverUrl: string; + username: string; +}) { + // 1. Get challenge from server + const initRegisterRes = await fetch(`${serverUrl}/init-register`, { + credentials: "include", + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + username, + }), + }); + const initRegister = await initRegisterRes.json(); + if (!initRegister.success) { + return { + success: false, + error: initRegister.error, + errorCode: initRegister.errorCode, + }; + } + + // 2. Create passkey + const optionsJSON = initRegister.optionsJSON; + const registrationJSON = await startRegistration({ optionsJSON }); + + // 3. Save passkey in DB + const verifyResponse = await fetch(`${serverUrl}/verify-register`, { + credentials: "include", + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(registrationJSON), + }); + + const verifyData = await verifyResponse.json(); + if (!verifyResponse.ok) { + console.error(verifyData.error); + return { + success: false, + error: verifyData.error, + errorCode: verifyData.errorCode, + }; + } + if (verifyData.verified) { + console.log(`Successfully registered ${username}`); + return { success: true }; + } else { + console.error(`Failed to register`); + return { success: false, error: "Failed to register", errorCode: 1 }; + } +} diff --git a/packages/mesh-wallet/src/browser/webauthn/cardano/build-wallet-from-passkey.ts b/packages/mesh-wallet/src/browser/webauthn/cardano/build-wallet-from-passkey.ts new file mode 100644 index 000000000..729187937 --- /dev/null +++ b/packages/mesh-wallet/src/browser/webauthn/cardano/build-wallet-from-passkey.ts @@ -0,0 +1,39 @@ +import base32 from "base32-encoding"; +import { bech32 } from "bech32"; + +import { Crypto } from "@meshsdk/core-cst"; + +export async function buildWalletFromPasskey( + rawId: string, + password: string, + appSalt = "appSalt", +) { + const entropy = await createEntropy(rawId, appSalt); + return buildKey(Buffer.from(entropy), password); +} + +function buildKey(entropy: Buffer, password: string) { + const bip32Key = Crypto.Bip32PrivateKey.fromBip39Entropy(entropy, password); + + const bytes = base32.encode(bip32Key.bytes()); + const bech32PrivateKey = bech32.encode("xprv", bytes, 1023); + + return { + bech32PrivateKey: bech32PrivateKey, + }; +} + +async function createEntropy(rawId: string, appSalt: string) { + // The rawId is inherently unique for each Passkey, ensuring no two users have the same derived entropy. + const rawIdBytes = new Uint8Array(new TextEncoder().encode(rawId)); + + // Combine with app-specific salt, using an app-specific salt ensures that the same rawId cannot be reused across different applications. + const saltBytes = new TextEncoder().encode(appSalt); + + const entropyBuffer = new Uint8Array([...rawIdBytes, ...saltBytes]); + + // Hash the combined data to produce 256-bit entropy + const entropy = await crypto.subtle.digest("SHA-256", entropyBuffer); + + return entropy; +} diff --git a/packages/mesh-wallet/src/browser/webauthn/common/error-codes/index.ts b/packages/mesh-wallet/src/browser/webauthn/common/error-codes/index.ts new file mode 100644 index 000000000..8554271a9 --- /dev/null +++ b/packages/mesh-wallet/src/browser/webauthn/common/error-codes/index.ts @@ -0,0 +1,3 @@ +export const ERRORCODES = { + USEREXISTS: 1, +}; diff --git a/packages/mesh-wallet/src/browser/webauthn/common/index.ts b/packages/mesh-wallet/src/browser/webauthn/common/index.ts new file mode 100644 index 000000000..f08f5aecd --- /dev/null +++ b/packages/mesh-wallet/src/browser/webauthn/common/index.ts @@ -0,0 +1 @@ +export * from "./error-codes"; diff --git a/packages/mesh-wallet/src/browser/webauthn/index.ts b/packages/mesh-wallet/src/browser/webauthn/index.ts new file mode 100644 index 000000000..97ccf7649 --- /dev/null +++ b/packages/mesh-wallet/src/browser/webauthn/index.ts @@ -0,0 +1 @@ +export * from "./auth"; diff --git a/packages/mesh-wallet/src/embedded/index.ts b/packages/mesh-wallet/src/embedded/index.ts index 7fec8afb5..d51f521ef 100644 --- a/packages/mesh-wallet/src/embedded/index.ts +++ b/packages/mesh-wallet/src/embedded/index.ts @@ -1,4 +1,6 @@ import * as BaseEncoding from "@scure/base"; +import base32 from "base32-encoding"; +import { bech32 } from "bech32"; import { bytesToHex, @@ -61,6 +63,10 @@ export type EmbeddedWalletKeyType = | { type: "mnemonic"; words: string[]; + } + | { + type: "bip32Bytes"; + bip32Bytes: Uint8Array; }; export type CreateEmbeddedWalletOptions = { @@ -69,8 +75,8 @@ export type CreateEmbeddedWalletOptions = { }; export class WalletStaticMethods { - static privateKeyToEntropy(bech32: string): string { - const bech32DecodedBytes = BaseEncoding.bech32.decodeToBytes(bech32).bytes; + static privateKeyToEntropy(_bech32: string): string { + const bech32DecodedBytes = BaseEncoding.bech32.decodeToBytes(_bech32).bytes; const bip32PrivateKey = Bip32PrivateKey.fromBytes(bech32DecodedBytes); return bytesToHex(bip32PrivateKey.bytes()); } @@ -91,6 +97,11 @@ export class WalletStaticMethods { ]; } + static bip32BytesToEntropy(bip32Bytes: Uint8Array): string { + const bip32PrivateKey = Bip32PrivateKey.fromBytes(bip32Bytes); + return bytesToHex(bip32PrivateKey.bytes()); + } + static getAddresses( paymentKey: StricaPrivateKey, stakingKey: StricaPrivateKey, @@ -204,6 +215,11 @@ export class EmbeddedWallet extends WalletStaticMethods { options.key.stake ?? "f0".repeat(32), ); break; + case "bip32Bytes": + this._entropy = WalletStaticMethods.bip32BytesToEntropy( + options.key.bip32Bytes, + ); + break; } } diff --git a/packages/mesh-wallet/src/mesh/index.ts b/packages/mesh-wallet/src/mesh/index.ts index 213b483ee..6378545a2 100644 --- a/packages/mesh-wallet/src/mesh/index.ts +++ b/packages/mesh-wallet/src/mesh/index.ts @@ -50,6 +50,10 @@ export type CreateMeshWalletOptions = { type: "mnemonic"; words: string[]; } + | { + type: "bip32Bytes"; + bip32Bytes: Uint8Array; + } | { type: "address"; address: string; @@ -142,6 +146,16 @@ export class MeshWallet implements IWallet { }); this.getAddressesFromWallet(this._wallet); break; + case "bip32Bytes": + this._wallet = new EmbeddedWallet({ + networkId: options.networkId, + key: { + type: "bip32Bytes", + bip32Bytes: options.key.bip32Bytes, + }, + }); + this.getAddressesFromWallet(this._wallet); + break; case "address": this._wallet = null; this.buildAddressFromBech32Address(options.key.address); @@ -149,6 +163,14 @@ export class MeshWallet implements IWallet { } } + /** + * Returns all derived addresses from the wallet. + * @returns a list of addresses + */ + getAddresses() { + return this.addresses; + } + /** * Returns a list of assets in the wallet. This API will return every assets in the wallet. Each asset is an object with the following properties: * - A unit is provided to display asset's name on the user interface.
+ Unique to the application you are connecting. +
+ Additional security to derive your wallet. +