From 4c5a2c7afdf07b572e97d74b517bf23d51981029 Mon Sep 17 00:00:00 2001 From: "Hong Jing (Jingles)" Date: Sun, 11 Aug 2024 17:53:40 +0800 Subject: [PATCH 01/21] fix docs build --- apps/docs/package.json | 3 +- .../src/app/[package]/classes/[name]/page.tsx | 2 +- apps/docs/src/components/Code.tsx | 4 +-- apps/docs/src/components/SectionProvider.tsx | 2 +- apps/docs/tsconfig.json | 30 ++++++------------- apps/playground/tsconfig.json | 5 ---- package-lock.json | 28 ++++++++++------- packages/configs/typescript/nextjs.json | 3 +- 8 files changed, 34 insertions(+), 43 deletions(-) diff --git a/apps/docs/package.json b/apps/docs/package.json index c0a400dc..91e1f9c6 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -27,13 +27,14 @@ "simple-functional-loader": "1.2.1", "unist-util-filter": "5.0.1", "unist-util-visit": "5.0.0", - "uuid": "10.0.0", + "uuid": "^10.0.0", "zustand": "4.5.2" }, "devDependencies": { "@types/node": "20.14.2", "@types/react": "18.3.3", "@types/react-dom": "18.3.0", + "@types/uuid": "^10.0.0", "autoprefixer": "10.4.19", "postcss": "8.4.38", "tailwindcss": "3.4.4", diff --git a/apps/docs/src/app/[package]/classes/[name]/page.tsx b/apps/docs/src/app/[package]/classes/[name]/page.tsx index bf723f92..5c81db57 100644 --- a/apps/docs/src/app/[package]/classes/[name]/page.tsx +++ b/apps/docs/src/app/[package]/classes/[name]/page.tsx @@ -23,7 +23,7 @@ export default function Page({ params }: { params: { name: string } }) { - {meshGroup.map((group, i) => { + {meshGroup.map((group: any) => { return ; })} diff --git a/apps/docs/src/components/Code.tsx b/apps/docs/src/components/Code.tsx index 5cd59f66..3a5535c1 100644 --- a/apps/docs/src/components/Code.tsx +++ b/apps/docs/src/components/Code.tsx @@ -285,7 +285,7 @@ function useTabGroupProps(availableLanguages: Array) { let activeLanguage = [...availableLanguages].sort( (a, z) => preferredLanguages.indexOf(z) - preferredLanguages.indexOf(a) )[0]; - let languageIndex = availableLanguages.indexOf(activeLanguage); + let languageIndex = availableLanguages.indexOf(activeLanguage!); let newSelectedIndex = languageIndex === -1 ? selectedIndex : languageIndex; if (newSelectedIndex !== selectedIndex) { setSelectedIndex(newSelectedIndex); @@ -299,7 +299,7 @@ function useTabGroupProps(availableLanguages: Array) { selectedIndex, onChange: (newSelectedIndex: number) => { preventLayoutShift(() => - addPreferredLanguage(availableLanguages[newSelectedIndex]) + addPreferredLanguage(availableLanguages[newSelectedIndex]!) ); }, }; diff --git a/apps/docs/src/components/SectionProvider.tsx b/apps/docs/src/components/SectionProvider.tsx index 1da9634e..e3417d3e 100644 --- a/apps/docs/src/components/SectionProvider.tsx +++ b/apps/docs/src/components/SectionProvider.tsx @@ -76,7 +76,7 @@ function useVisibleSections(sectionStore: StoreApi) { sectionIndex < sections.length; sectionIndex++ ) { - let { id, headingRef, offsetRem = 0 } = sections[sectionIndex] + let { id, headingRef, offsetRem = 0 } = sections[sectionIndex]! if (!headingRef?.current) { continue diff --git a/apps/docs/tsconfig.json b/apps/docs/tsconfig.json index d92effdd..51401540 100644 --- a/apps/docs/tsconfig.json +++ b/apps/docs/tsconfig.json @@ -1,28 +1,16 @@ { + "extends": "@meshsdk/configs/typescript/nextjs.json", "compilerOptions": { - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], "paths": { "@/*": ["./src/*"] - }, - "noImplicitAny": false, - "strictNullChecks": false, + } }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "include": [ + "next-env.d.ts", + "next.config.js", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], "exclude": ["node_modules"] } diff --git a/apps/playground/tsconfig.json b/apps/playground/tsconfig.json index 4c7de83d..39055e28 100644 --- a/apps/playground/tsconfig.json +++ b/apps/playground/tsconfig.json @@ -1,11 +1,6 @@ { "extends": "@meshsdk/configs/typescript/nextjs.json", "compilerOptions": { - "plugins": [ - { - "name": "next" - } - ], "paths": { "~/*": ["./src/*"] } diff --git a/package-lock.json b/package-lock.json index 23360cfe..e7b61917 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,13 +49,14 @@ "simple-functional-loader": "1.2.1", "unist-util-filter": "5.0.1", "unist-util-visit": "5.0.0", - "uuid": "10.0.0", + "uuid": "^10.0.0", "zustand": "4.5.2" }, "devDependencies": { "@types/node": "20.14.2", "@types/react": "18.3.3", "@types/react-dom": "18.3.0", + "@types/uuid": "^10.0.0", "autoprefixer": "10.4.19", "postcss": "8.4.38", "tailwindcss": "3.4.4", @@ -7297,6 +7298,12 @@ "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==", "license": "MIT" }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -21677,7 +21684,6 @@ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], - "license": "MIT", "bin": { "uuid": "dist/bin/uuid" } @@ -22286,7 +22292,7 @@ }, "packages/mesh-common": { "name": "@meshsdk/common", - "version": "1.6.3", + "version": "1.6.4", "license": "Apache-2.0", "dependencies": { "@emurgo/cip14-js": "3.0.1", @@ -22301,7 +22307,7 @@ }, "packages/mesh-contract": { "name": "@meshsdk/contract", - "version": "1.6.3", + "version": "1.6.4", "license": "Apache-2.0", "dependencies": { "@meshsdk/common": "*", @@ -22319,7 +22325,7 @@ }, "packages/mesh-core": { "name": "@meshsdk/core", - "version": "1.6.3", + "version": "1.6.4", "license": "Apache-2.0", "dependencies": { "@meshsdk/common": "*", @@ -22339,7 +22345,7 @@ }, "packages/mesh-core-csl": { "name": "@meshsdk/core-csl", - "version": "1.6.3", + "version": "1.6.4", "license": "Apache-2.0", "dependencies": { "@meshsdk/common": "*", @@ -22358,7 +22364,7 @@ }, "packages/mesh-core-cst": { "name": "@meshsdk/core-cst", - "version": "1.6.3", + "version": "1.6.4", "license": "Apache-2.0", "dependencies": { "@cardano-sdk/core": "^0.35.4", @@ -22382,7 +22388,7 @@ }, "packages/mesh-provider": { "name": "@meshsdk/provider", - "version": "1.6.3", + "version": "1.6.4", "license": "Apache-2.0", "dependencies": { "@meshsdk/common": "*", @@ -22398,7 +22404,7 @@ }, "packages/mesh-react": { "name": "@meshsdk/react", - "version": "1.6.3", + "version": "1.6.4", "license": "Apache-2.0", "dependencies": { "@meshsdk/common": "*", @@ -22418,7 +22424,7 @@ }, "packages/mesh-transaction": { "name": "@meshsdk/transaction", - "version": "1.6.3", + "version": "1.6.4", "license": "Apache-2.0", "dependencies": { "@meshsdk/common": "*", @@ -22436,7 +22442,7 @@ }, "packages/mesh-wallet": { "name": "@meshsdk/wallet", - "version": "1.6.3", + "version": "1.6.4", "license": "Apache-2.0", "dependencies": { "@meshsdk/common": "*", diff --git a/packages/configs/typescript/nextjs.json b/packages/configs/typescript/nextjs.json index 05834a7b..271c0665 100644 --- a/packages/configs/typescript/nextjs.json +++ b/packages/configs/typescript/nextjs.json @@ -9,6 +9,7 @@ "allowJs": true, "jsx": "preserve", "noEmit": true, - "noUnusedLocals": false + "noUnusedLocals": false, + "noImplicitAny": false } } From 90d7edebc788ae3651a1f8f565cfe4b88fce6e88 Mon Sep 17 00:00:00 2001 From: "Hong Jing (Jingles)" Date: Sun, 11 Aug 2024 18:20:25 +0800 Subject: [PATCH 02/21] add getUnregisteredPubStakeKeys --- .../get-unregisteredpubstakekeys.tsx | 45 ++++++++++++++ .../apis/wallets/browserwallet/index.tsx | 6 ++ packages/mesh-wallet/src/browser/index.ts | 60 ++++++++++++++++--- packages/mesh-wallet/src/types/index.ts | 1 + 4 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 apps/playground/src/pages/apis/wallets/browserwallet/get-unregisteredpubstakekeys.tsx diff --git a/apps/playground/src/pages/apis/wallets/browserwallet/get-unregisteredpubstakekeys.tsx b/apps/playground/src/pages/apis/wallets/browserwallet/get-unregisteredpubstakekeys.tsx new file mode 100644 index 00000000..81525ff5 --- /dev/null +++ b/apps/playground/src/pages/apis/wallets/browserwallet/get-unregisteredpubstakekeys.tsx @@ -0,0 +1,45 @@ +import { useWallet } from "@meshsdk/react"; + +import LiveCodeDemo from "~/components/sections/live-code-demo"; +import TwoColumnsScroll from "~/components/sections/two-columns-scroll"; + +export default function BrowserWalletGetUnregisteredPubStakeKeys() { + return ( + + ); +} + +function Left() { + return ( + <> +

Get a list of unregistered public stake keys.

+ + ); +} + +function Right() { + const { wallet, connected } = useWallet(); + + async function runDemo() { + const results = await wallet.getUnregisteredPubStakeKeys(); + return results; + } + return ( + + ); +} diff --git a/apps/playground/src/pages/apis/wallets/browserwallet/index.tsx b/apps/playground/src/pages/apis/wallets/browserwallet/index.tsx index 964945c5..e15d2eec 100644 --- a/apps/playground/src/pages/apis/wallets/browserwallet/index.tsx +++ b/apps/playground/src/pages/apis/wallets/browserwallet/index.tsx @@ -20,6 +20,7 @@ import BrowserWalletGetPubdrepkey from "./get-pubdrepkey"; import BrowserWalletGetRegisteredpubstakekeys from "./get-registeredpubstakekeys"; import BrowserWalletGetRewardAddresses from "./get-reward-addresses"; import BrowserWalletGetSupportedExtensions from "./get-supported-extensions"; +import BrowserWalletGetUnregisteredPubStakeKeys from "./get-unregisteredpubstakekeys"; import BrowserWalletGetUnusedAddresses from "./get-unused-addresses"; import BrowserWalletGetUsedAddresses from "./get-used-addresses"; import BrowserWalletGetUtxos from "./get-utxos"; @@ -51,6 +52,10 @@ const ReactPage: NextPage = () => { { label: "Get extensions", to: "getExtensions" }, { label: "Get DRep ID Key", to: "getPubdrepkey" }, { label: "Get pub stake keys", to: "getRegisteredpubstakekeys" }, + { + label: "Get unregistered stakekeys", + to: "getUnregisteredPubStakeKeys", + }, ]; return ( @@ -103,6 +108,7 @@ const ReactPage: NextPage = () => { + ); diff --git a/packages/mesh-wallet/src/browser/index.ts b/packages/mesh-wallet/src/browser/index.ts index 1abe139e..8a689c1a 100644 --- a/packages/mesh-wallet/src/browser/index.ts +++ b/packages/mesh-wallet/src/browser/index.ts @@ -21,6 +21,7 @@ import { deserializeTx, deserializeTxUnspentOutput, deserializeValue, + Ed25519KeyHashHex, Ed25519PublicKey, Ed25519PublicKeyHex, fromTxUnspentOutput, @@ -459,10 +460,8 @@ export class BrowserWallet implements IInitiator, ISigner, ISubmitter { if (this._walletInstance.cip95 === undefined) return undefined; const dRepKey = await this._walletInstance.cip95.getPubDRepKey(); - - const dRepKeyHex = Ed25519PublicKeyHex(dRepKey); - const dRepID = Ed25519PublicKey.fromHex(dRepKeyHex); - const dRepIDHex = (await dRepID.hash()).hex(); + const { dRepKeyHex, dRepIDHex } = + await BrowserWallet.dRepKeyToDRepID(dRepKey); const networkId = await this.getNetworkId(); const dRepId = buildDRepID(dRepKeyHex, networkId); @@ -493,10 +492,40 @@ export class BrowserWallet implements IInitiator, ISigner, ISubmitter { const pubStakeKeyHashes = await Promise.all( pubStakeKeys.map(async (pubStakeKey) => { - const pubStakeKeyHex = Ed25519PublicKeyHex(pubStakeKey); - const pubStakeKeyPubKey = Ed25519PublicKey.fromHex(pubStakeKeyHex); - const pubStakeKeyHash = (await pubStakeKeyPubKey.hash()).hex(); - return pubStakeKeyHash.toString(); + const { dRepIDHex } = + await BrowserWallet.dRepKeyToDRepID(pubStakeKey); + return dRepIDHex; + }), + ); + + return { + pubStakeKeys: pubStakeKeys, + pubStakeKeyHashes: pubStakeKeyHashes, + }; + } catch (e) { + console.log(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 { dRepIDHex } = + await BrowserWallet.dRepKeyToDRepID(pubStakeKey); + return dRepIDHex; }), ); @@ -510,6 +539,21 @@ export class BrowserWallet implements IInitiator, ISigner, ISubmitter { } } + private static async dRepKeyToDRepID(dRepKey: string): Promise<{ + dRepKeyHex: Ed25519PublicKeyHex; + dRepID: Ed25519PublicKey; + dRepIDHex: Ed25519KeyHashHex; + }> { + const dRepKeyHex = Ed25519PublicKeyHex(dRepKey); + const dRepID = Ed25519PublicKey.fromHex(dRepKeyHex); + const dRepIDHex = (await dRepID.hash()).hex(); + return { + dRepKeyHex, + dRepID, + dRepIDHex, + }; + } + private static resolveInstance( walletName: string, extensions: number[] = [], diff --git a/packages/mesh-wallet/src/types/index.ts b/packages/mesh-wallet/src/types/index.ts index 9cbe4014..12ad15ce 100644 --- a/packages/mesh-wallet/src/types/index.ts +++ b/packages/mesh-wallet/src/types/index.ts @@ -38,6 +38,7 @@ export type WalletInstance = { export type WalletInstanceCip95 = { getPubDRepKey(): Promise; getRegisteredPubStakeKeys(): Promise; + getUnregisteredPubStakeKeys(): Promise; }; type ExperimentalFeatures = { From e445658d13d40d15b427b4a7dd71872354c2ce3e Mon Sep 17 00:00:00 2001 From: "Hong Jing (Jingles)" Date: Mon, 12 Aug 2024 00:03:52 +0800 Subject: [PATCH 03/21] load mesh wallet with address --- .../apis/wallets/meshwallet/load-wallet.tsx | 97 +++++++++++-- packages/mesh-wallet/src/mesh/index.ts | 133 ++++++++++-------- packages/mesh-wallet/test/mesh.test.ts | 30 +++- 3 files changed, 185 insertions(+), 75 deletions(-) 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 258f5515..f6dfe431 100644 --- a/apps/playground/src/pages/apis/wallets/meshwallet/load-wallet.tsx +++ b/apps/playground/src/pages/apis/wallets/meshwallet/load-wallet.tsx @@ -13,7 +13,12 @@ import DemoResult from "~/components/sections/demo-result"; import TwoColumnsScroll from "~/components/sections/two-columns-scroll"; import Codeblock from "~/components/text/codeblock"; import useMeshWallet from "~/contexts/mesh-wallet"; -import { demoCLIKey, demoMnemonic, demoPrivateKey } from "~/data/cardano"; +import { + demoAddresses, + demoCLIKey, + demoMnemonic, + demoPrivateKey, +} from "~/data/cardano"; export default function MeshWalletLoadWallet() { const [demoMethod, setDemoMethod] = useState(0); @@ -26,12 +31,22 @@ export default function MeshWalletLoadWallet() { demoCLIKey.paymentSkey, ); const [stakeSkey, setStakeSkey] = useState(demoCLIKey.stakeSkey); + const [walletAddress, setWalletAddress] = useState( + demoAddresses.testnetPayment, + ); return ( ); @@ -56,6 +73,7 @@ function Left( privatekey: string, paymentSkey: string, stakeSkey: string, + walletAddress: string, ) { let _mnemonic = JSON.stringify(demoMnemonic); try { @@ -102,6 +120,17 @@ function Left( code4 += ` },\n`; code4 += `});\n`; + let code5 = codeCommon; + code5 += `const wallet = new MeshWallet({\n`; + code5 += ` networkId: ${network}, // 0: testnet, 1: mainnet\n`; + code5 += ` fetcher: blockchainProvider,\n`; + code5 += ` submitter: blockchainProvider,\n`; + code5 += ` key: {\n`; + code5 += ` type: 'address',\n`; + code5 += ` bech32: '${walletAddress}',\n`; + code5 += ` },\n`; + code5 += `});\n`; + return ( <>

You can initialize Mesh Wallet with:

@@ -109,6 +138,7 @@ function Left(
  • mnemonic phrases
  • private keys
  • Cardano CLI generated keys
  • +
  • address (read only wallet)
  • @@ -151,6 +181,15 @@ function Left( optional, but without it, you cannot sign staking transactions.

    + +

    Address

    +

    + We can load wallet with address, this is useful for read-only wallets. A + read-only wallet can only query the blockchain, it cannot sign + transactions. This is useful for monitoring wallets. We can load wallet + with the address type: +

    + ); } @@ -168,6 +207,8 @@ function Right( setPaymentSkey: (paymentSkey: string) => void, stakeSkey: string, setStakeSkey: (stakeSkey: string) => void, + walletAddress: string, + setWalletAddress: (address: string) => void, ) { const { setWallet } = useMeshWallet(); const [loading, setLoading] = useState(false); @@ -249,6 +290,25 @@ function Right( setResponseError(`${error}`); } } + if (demoMethod == 3) { + try { + const _wallet = new MeshWallet({ + networkId: network as 0 | 1, + fetcher: blockchainProvider, + submitter: blockchainProvider, + key: { + type: "address", + address: walletAddress, + }, + }); + setWallet(_wallet); + + const address = _wallet.getChangeAddress(); + setResponseAddress(address); + } catch (error) { + setResponseError(`${error}`); + } + } setLoading(false); } @@ -260,7 +320,7 @@ function Right( items={[ { key: 0, - label: "Mnemonic phrases", + label: "Mnemonic", onClick: () => setDemoMethod(0), }, { @@ -273,6 +333,11 @@ function Right( label: "CLI keys", onClick: () => setDemoMethod(2), }, + { + key: 3, + label: "Address", + onClick: () => setDemoMethod(3), + }, ]} currentSelected={demoMethod} /> @@ -289,6 +354,8 @@ function Right( setPaymentSkey={setPaymentSkey} stakeSkey={stakeSkey} setStakeSkey={setStakeSkey} + walletAddress={walletAddress} + setWalletAddress={setWalletAddress} /> void; stakeSkey: string; setStakeSkey: (stakeSkey: string) => void; + walletAddress: string; + setWalletAddress: (address: string) => void; }) { return (
    @@ -335,17 +406,17 @@ function InputTable({ Load wallet with {demoMethod == 0 && "mnemonic phrases"} {demoMethod == 1 && "private keys"} {demoMethod == 2 && "CLI generated keys"} + {demoMethod == 3 && "address"}

    Provide the {demoMethod == 0 && "mnemonic phrases"} {demoMethod == 1 && "private keys"} - {demoMethod == 2 && "CLI generated keys"} to recover your wallet. - After initializing the MeshWallet, we will get the - wallet's payment address. + {demoMethod == 2 && "CLI generated keys"} + {demoMethod == 3 && "address"} to recover your wallet. After + initializing the MeshWallet, we will get the wallet's + payment address.

    - Note: Mesh Playground is safe if you really have to recover your - Mainnet wallet, but recovering your testing wallet on Mesh - Playground is recommended. + Note: Trying these demo with your Testnet wallet is recommended.

    @@ -387,6 +458,14 @@ function InputTable({ /> )} + {demoMethod == 3 && ( + setWalletAddress(e.target.value)} + placeholder="Wallet address" + label="Wallet address" + /> + )} setNetwork(parseInt(e.target.value))} diff --git a/packages/mesh-wallet/src/mesh/index.ts b/packages/mesh-wallet/src/mesh/index.ts index 01eca263..249a5e07 100644 --- a/packages/mesh-wallet/src/mesh/index.ts +++ b/packages/mesh-wallet/src/mesh/index.ts @@ -19,7 +19,6 @@ import { buildBaseAddress, buildEnterpriseAddress, buildRewardAddress, - CardanoSDKSerializer, deserializeTx, Ed25519KeyHashHex, fromTxUnspentOutput, @@ -50,11 +49,11 @@ export type CreateMeshWalletOptions = { | { type: "mnemonic"; words: string[]; + } + | { + type: "address"; + address: string; }; - // | { - // type: "address"; - // address: string; - // } accountIndex?: number; keyIndex?: number; }; @@ -62,10 +61,14 @@ export type CreateMeshWalletOptions = { /** * Mesh Wallet provides a set of APIs to interact with the blockchain. This wallet is compatible with Mesh transaction builders. * - * It is a single address wallet, a wrapper around the AppWallet class. + * There are 4 types of keys that can be used to create a wallet: + * - root: A private key in bech32 format, generally starts with `xprv1` + * - cli: CLI generated keys starts with `5820`. Payment key is required, and the stake key is optional. + * - mnemonic: A list of 24 words + * - address: A bech32 address that can be used to create a read-only wallet, generally starts with `addr` or `addr_test1` * * ```javascript - * import { MeshWallet, BlockfrostProvider } from '@meshsdksdk/core'; + * import { MeshWallet, BlockfrostProvider } from '@meshsdk/core'; * * const blockchainProvider = new BlockfrostProvider(''); * @@ -82,13 +85,12 @@ export type CreateMeshWalletOptions = { */ export class MeshWallet implements IInitiator, ISigner, ISubmitter { private readonly _wallet: EmbeddedWallet | null; - // private readonly _account: Account; private readonly _accountIndex: number = 0; private readonly _keyIndex: number = 0; private readonly _fetcher?: IFetcher; private readonly _submitter?: ISubmitter; private readonly _networkId: 0 | 1; - private _addresses: { + addresses: { baseAddress?: Address; enterpriseAddress?: Address; rewardAddress?: Address; @@ -98,6 +100,8 @@ export class MeshWallet implements IInitiator, ISigner, ISubmitter { } = {}; constructor(options: CreateMeshWalletOptions) { + this._networkId = options.networkId; + switch (options.key.type) { case "root": this._wallet = new EmbeddedWallet({ @@ -130,14 +134,12 @@ export class MeshWallet implements IInitiator, ISigner, ISubmitter { }); this.getAddressesFromWallet(this._wallet); break; - // case "address": - // this._wallet = null; - // this.buildAddressFromBech32Address(options.key.address); - // break; + case "address": + this._wallet = null; + this.buildAddressFromBech32Address(options.key.address); + break; } - this._networkId = options.networkId; - if (options.fetcher) this._fetcher = options.fetcher; if (options.submitter) this._submitter = options.submitter; if (options.accountIndex) this._accountIndex = options.accountIndex; @@ -183,7 +185,9 @@ export class MeshWallet implements IInitiator, ISigner, ISubmitter { * @returns an address */ getChangeAddress(): string { - return this._addresses.baseAddressBech32!; + return this.addresses.baseAddressBech32 + ? this.addresses.baseAddressBech32 + : this.addresses.enterpriseAddressBech32!; } /** @@ -254,7 +258,7 @@ export class MeshWallet implements IInitiator, ISigner, ISubmitter { * @returns a list of reward addresses */ getRewardAddresses(): string[] { - return [this._addresses.rewardAddressBech32!]; + return [this.addresses.rewardAddressBech32!]; } /** @@ -301,7 +305,7 @@ export class MeshWallet implements IInitiator, ISigner, ISubmitter { * @returns a list of UTXOs */ async getUsedUTxOs( - addressType?: GetAddressType, + addressType: GetAddressType = "payment", ): Promise { return await this.getUnspentOutputs(addressType); } @@ -312,7 +316,7 @@ export class MeshWallet implements IInitiator, ISigner, ISubmitter { * @param addressType - the type of address to fetch UTXOs from (default: payment) * @returns a list of UTXOs */ - async getUtxos(addressType?: GetAddressType): Promise { + async getUtxos(addressType: GetAddressType = "payment"): Promise { const utxos = await this.getUsedUTxOs(addressType); return utxos.map((c) => fromTxUnspentOutput(c)); } @@ -378,6 +382,12 @@ export class MeshWallet implements IInitiator, ISigner, ISubmitter { * @returns array of signed transactions CborHex string */ signTxs(unsignedTxs: string[], partialSign = false): string[] { + if (!this._wallet) { + throw new Error( + "[MeshWallet] Read only wallet does not support signing data.", + ); + } + const signedTxs: string[] = []; for (const unsignedTx of unsignedTxs) { @@ -399,7 +409,7 @@ export class MeshWallet implements IInitiator, ISigner, ISubmitter { async submitTx(tx: string): Promise { if (!this._submitter) { throw new Error( - "[AppWallet] Submitter is required to submit transactions. Please provide a submitter.", + "[MeshWallet] Submitter is required to submit transactions. Please provide a submitter.", ); } return this._submitter.submitTx(tx); @@ -413,11 +423,11 @@ export class MeshWallet implements IInitiator, ISigner, ISubmitter { * @param addressType - the type of address to fetch UTXOs from (default: payment) * @returns an Address object */ - getUsedAddress(addressType?: GetAddressType): Address { - if (addressType === "enterprise") { - return toAddress(this._addresses.enterpriseAddressBech32!); + getUsedAddress(addressType: GetAddressType = "payment"): Address { + if (this.addresses.baseAddressBech32 && addressType === "payment") { + return toAddress(this.addresses.baseAddressBech32); } else { - return toAddress(this._addresses.baseAddressBech32!); + return toAddress(this.addresses.enterpriseAddressBech32!); } } @@ -430,18 +440,18 @@ export class MeshWallet implements IInitiator, ISigner, ISubmitter { * @returns a list of UTXOs */ async getUnspentOutputs( - addressType?: GetAddressType, + addressType: GetAddressType = "payment", ): Promise { if (!this._fetcher) { throw new Error( - "[AppWallet] Fetcher is required to fetch UTxOs. Please provide a fetcher.", + "[MeshWallet] Fetcher is required to fetch UTxOs. Please provide a fetcher.", ); } const utxos = await this._fetcher.fetchAddressUTxOs( - addressType == "enterprise" - ? this._addresses.enterpriseAddressBech32! - : this._addresses.baseAddressBech32!, + this.addresses.baseAddressBech32 && addressType == "payment" + ? this.addresses.baseAddressBech32! + : this.addresses.enterpriseAddressBech32!, ); return utxos.map((utxo) => toTxUnspentOutput(utxo)); @@ -539,7 +549,7 @@ export class MeshWallet implements IInitiator, ISigner, ISubmitter { getAddressesFromWallet(wallet: EmbeddedWallet) { const account = wallet.getAccount(this._accountIndex, this._keyIndex); - this._addresses = { + this.addresses = { baseAddress: account.baseAddress, enterpriseAddress: account.enterpriseAddress, rewardAddress: account.rewardAddress, @@ -549,52 +559,53 @@ export class MeshWallet implements IInitiator, ISigner, ISubmitter { }; } - buildAddressFromBech32Address(address: string) { - const serializer = new CardanoSDKSerializer(); + private buildAddressFromBech32Address(address: string) { + let pubKeyHash = undefined; + let stakeKeyHash = undefined; - const deserializedAddress = - serializer.deserializer.key.deserializeAddress(address); + const baseAddress = Address.fromBech32(address).asBase(); + if (baseAddress) { + pubKeyHash = baseAddress.getPaymentCredential().hash; + stakeKeyHash = baseAddress.getStakeCredential().hash; + } + const enterpriseAddress = Address.fromBech32(address).asEnterprise(); + if (enterpriseAddress) { + pubKeyHash = enterpriseAddress.getPaymentCredential().hash; + } - if ( - deserializedAddress.pubKeyHash && - deserializedAddress.stakeCredentialHash - ) { - this._addresses.baseAddress = buildBaseAddress( + const rewardAddress = Address.fromBech32(address).asReward(); + if (rewardAddress) { + stakeKeyHash = rewardAddress.getPaymentCredential().hash; + } + + if (pubKeyHash && stakeKeyHash) { + this.addresses.baseAddress = buildBaseAddress( this._networkId, + Hash28ByteBase16.fromEd25519KeyHashHex(Ed25519KeyHashHex(pubKeyHash)), Hash28ByteBase16.fromEd25519KeyHashHex( - Ed25519KeyHashHex(deserializedAddress.pubKeyHash), - ), - Hash28ByteBase16.fromEd25519KeyHashHex( - Ed25519KeyHashHex( - Ed25519KeyHashHex(deserializedAddress.stakeCredentialHash), - ), + Ed25519KeyHashHex(Ed25519KeyHashHex(stakeKeyHash)), ), ).toAddress(); - this._addresses.baseAddressBech32 = - this._addresses.baseAddress.toBech32(); + this.addresses.baseAddressBech32 = this.addresses.baseAddress.toBech32(); } - if (deserializedAddress.pubKeyHash) { - this._addresses.enterpriseAddress = buildEnterpriseAddress( + if (pubKeyHash) { + this.addresses.enterpriseAddress = buildEnterpriseAddress( this._networkId, - Hash28ByteBase16.fromEd25519KeyHashHex( - Ed25519KeyHashHex(deserializedAddress.pubKeyHash), - ), + Hash28ByteBase16.fromEd25519KeyHashHex(Ed25519KeyHashHex(pubKeyHash)), ).toAddress(); - this._addresses.enterpriseAddressBech32 = - this._addresses.enterpriseAddress.toBech32(); + this.addresses.enterpriseAddressBech32 = + this.addresses.enterpriseAddress.toBech32(); } - if (deserializedAddress.stakeCredentialHash) { - this._addresses.rewardAddress = buildRewardAddress( + if (stakeKeyHash) { + this.addresses.rewardAddress = buildRewardAddress( this._networkId, - Hash28ByteBase16.fromEd25519KeyHashHex( - Ed25519KeyHashHex(deserializedAddress.stakeCredentialHash), - ), + Hash28ByteBase16.fromEd25519KeyHashHex(Ed25519KeyHashHex(stakeKeyHash)), ).toAddress(); - this._addresses.rewardAddressBech32 = - this._addresses.rewardAddress.toBech32(); + this.addresses.rewardAddressBech32 = + this.addresses.rewardAddress.toBech32(); } } } diff --git a/packages/mesh-wallet/test/mesh.test.ts b/packages/mesh-wallet/test/mesh.test.ts index 486ca81a..b2933da4 100644 --- a/packages/mesh-wallet/test/mesh.test.ts +++ b/packages/mesh-wallet/test/mesh.test.ts @@ -10,7 +10,7 @@ describe("MeshWallet", () => { }); it("private keys", () => { - const wallet = new MeshWallet({ + const _wallet = new MeshWallet({ networkId: 0, key: { type: "root", @@ -18,13 +18,13 @@ describe("MeshWallet", () => { "xprv1cqa46gk29plgkg98upclnjv5t425fcpl4rgf9mq2txdxuga7jfq5shk7np6l55nj00sl3m4syzna3uwgrwppdm0azgy9d8zahyf32s62klfyhe0ayyxkc7x92nv4s77fa0v25tufk9tnv7x6dgexe9kdz5gpeqgu", }, }); - expect(wallet.getChangeAddress()).toEqual( + expect(_wallet.getChangeAddress()).toEqual( "addr_test1qpvx0sacufuypa2k4sngk7q40zc5c4npl337uusdh64kv0uafhxhu32dys6pvn6wlw8dav6cmp4pmtv7cc3yel9uu0nq93swx9", ); }); it("cli keys", () => { - const wallet = new MeshWallet({ + const _wallet = new MeshWallet({ networkId: 0, key: { type: "cli", @@ -34,14 +34,34 @@ describe("MeshWallet", () => { "5820b810d6398db44f380a9ab279f63950c4b95432f44fafb5a6f026afe23bbe9241", }, }); - expect(wallet.getChangeAddress()).toEqual( + expect(_wallet.getChangeAddress()).toEqual( "addr_test1qqdy60gf798xrl20wwvapvsxj3kr8yz8ac6zfmgwg6c5g9p3x07mt562mneg8jxgj03p2uvmhyfyvktjn259mws8e6wq3cdn8p", ); - expect(wallet.getRewardAddresses()[0]).toEqual( + expect(_wallet.getRewardAddresses()[0]).toEqual( "stake_test1uqcn8ld46d9deu5reryf8cs4wxdmjyjxt9ef42zahgrua8qctnd74", ); }); + it("init with address", () => { + const _wallet = new MeshWallet({ + networkId: 0, + key: { + type: "address", + address: + "addr_test1qpvx0sacufuypa2k4sngk7q40zc5c4npl337uusdh64kv0uafhxhu32dys6pvn6wlw8dav6cmp4pmtv7cc3yel9uu0nq93swx9", + }, + }); + expect(_wallet.addresses.baseAddressBech32).toEqual( + "addr_test1qpvx0sacufuypa2k4sngk7q40zc5c4npl337uusdh64kv0uafhxhu32dys6pvn6wlw8dav6cmp4pmtv7cc3yel9uu0nq93swx9", + ); + expect(_wallet.addresses.enterpriseAddressBech32).toEqual( + "addr_test1vpvx0sacufuypa2k4sngk7q40zc5c4npl337uusdh64kv0c7e4cxr", + ); + expect(_wallet.addresses.rewardAddressBech32).toEqual( + "stake_test1uzw5mnt7g4xjgdqkfa80hrk7kdvds6sa4k0vvgjvlj7w8eskffj2n", + ); + }); + it("getChangeAddress", () => { const changeAddress = wallet.getChangeAddress(); expect(changeAddress).toEqual( From 24224452b8178a0c8ec314c1d63dbbba24f54bdb Mon Sep 17 00:00:00 2001 From: "Hong Jing (Jingles)" Date: Mon, 12 Aug 2024 12:23:17 +0800 Subject: [PATCH 04/21] add mutlisig minting demo --- apps/playground/src/data/cardano.ts | 4 +- apps/playground/src/data/links-guides.ts | 2 +- .../src/pages/guides/multisig-minting.mdx | 168 ------------------ .../pages/guides/multisig-minting/demo.tsx | 108 +++++++++++ .../pages/guides/multisig-minting/index.mdx | 160 +++++++++++++++++ .../src/pages/guides/multisig-tx/demo.tsx | 112 ++++++++++++ .../guides/prove-wallet-ownership/demo.tsx | 41 +++++ .../index.mdx} | 44 ++--- 8 files changed, 437 insertions(+), 202 deletions(-) delete mode 100644 apps/playground/src/pages/guides/multisig-minting.mdx create mode 100644 apps/playground/src/pages/guides/multisig-minting/demo.tsx create mode 100644 apps/playground/src/pages/guides/multisig-minting/index.mdx create mode 100644 apps/playground/src/pages/guides/multisig-tx/demo.tsx create mode 100644 apps/playground/src/pages/guides/prove-wallet-ownership/demo.tsx rename apps/playground/src/pages/guides/{prove-wallet-ownership.mdx => prove-wallet-ownership/index.mdx} (91%) diff --git a/apps/playground/src/data/cardano.ts b/apps/playground/src/data/cardano.ts index 401a8a56..7f7cc4bf 100644 --- a/apps/playground/src/data/cardano.ts +++ b/apps/playground/src/data/cardano.ts @@ -5,9 +5,9 @@ export const demoAddresses = { mainnet: "addr1v9vx0sacufuypa2k4sngk7q40zc5c4npl337uusdh64kv0c93pyfx", testnet: "addr_test1vpvx0sacufuypa2k4sngk7q40zc5c4npl337uusdh64kv0c7e4cxr", testnetPayment: - "addr_test1qzl2r3fpmav0fmh0vrry0e0tmzxxqwv32sylnlty2jj8dwg636sfudakhsh65qggs4ttjjsk8fuu3fkd65uaxcxv0tfqv3z0y3", + "addr_test1qpvx0sacufuypa2k4sngk7q40zc5c4npl337uusdh64kv0uafhxhu32dys6pvn6wlw8dav6cmp4pmtv7cc3yel9uu0nq93swx9", testnetStake: - "stake_test1uqdgagy7x7mtcta2qyyg244efgtr57wg5mxa2wwnvrx845s4sa2vp", + "stake_test1uzw5mnt7g4xjgdqkfa80hrk7kdvds6sa4k0vvgjvlj7w8eskffj2n", }; export const demoMnemonic = "solution,".repeat(24).split(",").slice(0, 24); diff --git a/apps/playground/src/data/links-guides.ts b/apps/playground/src/data/links-guides.ts index 5e58b74b..257dd1b7 100644 --- a/apps/playground/src/data/links-guides.ts +++ b/apps/playground/src/data/links-guides.ts @@ -18,7 +18,7 @@ export const guidenodejs = { }; export const guideminting = { title: "Multi-Signatures Transaction", - desc: "Learn about multi-sig transaction, build a minting transaction involving AppWallet and BrowserWallet.", + desc: "Learn about multi-sig transaction, build a minting transaction involving MeshWallet and BrowserWallet.", link: "/guides/multisig-minting", thumbnail: "/guides/multi-signatures-transaction.png", image: "/guides/keys-g25a80b203_1280.jpg", diff --git a/apps/playground/src/pages/guides/multisig-minting.mdx b/apps/playground/src/pages/guides/multisig-minting.mdx deleted file mode 100644 index 7466fd0c..00000000 --- a/apps/playground/src/pages/guides/multisig-minting.mdx +++ /dev/null @@ -1,168 +0,0 @@ -import LayoutImageHeaderAndBody from "~/components/layouts/image-header-and-body"; -import { guideminting } from "~/data/links-guides"; - -export default function MDXPage({ children }) { - return ( - - {children} - - ); -} - -A multi-signature (multi-sig) transaction requires more than one user to sign a transaction prior to the transaction being broadcast on a blockchain. You can think of it like a husband and wife savings account, where both signatures are required to spend the funds, preventing one spouse from spending the money without the approval of the other. For a multi-sig transaction, you can include 2 or more required signers, these signers can be wallets ([Browser Wallet](https://meshjs.dev/apis/wallets/browserwallet) or [Mesh Wallet](https://meshjs.dev/apis/wallets/meshwallet)) or Plutus script. - -In this guide, we will build a multi-sig transaction for minting. There are 2 wallets involved, - -1. client wallet belonging to the user who wishes to buy a native asset, and -2. application wallet that holds the forging script. - -## See it in action - -In this guide, we will connect our CIP wallet (**BrowserWallet**) to request for a minting transaction. Then, the backend application wallet (**MeshWallet**) will build the transaction, and we will sign it with our wallet. Finally, the application wallet will sign the transaction and submit it to the blockchain. Note: this demo is on **preprod** network only. - -Let's see it in action. - -Connect Wallet - -## Connect wallet (client) - -In this section, we will connect client's wallet and obtain their wallet address and UTXO. - -Users can connect their wallet with **BrowserWallet**: - -``` -import { BrowserWallet } from '@meshsdk/core'; -const wallet = await BrowserWallet.enable(walletName); -``` - -Then, we get client's wallet address and UTXOs: - -``` -const recipientAddress = await wallet.getChangeAddress(); -const utxos = await wallet.getUtxos(); -``` - -The change address will be the address receiving the minted NFTs and the transaction's change. Additionally, we will need the client's wallet UTXOs to build the minting transaction. - -## Build transaction (application) - -In this section, we will build the minting transaction. - -In this guide, we won't be showing how to set up RESTful APIs and backend servers. There are thousands of tutorials on YouTube, we recommend building your backend server with [Vercel API](https://vercel.com/docs/rest-api) or [NestJs](https://www.youtube.com/results?search_query=nestjs). - -First, we initialize the [blockchain provider](https://meshjs.dev/apis/providers) and **MeshWallet**. In this example, we use mnemonic to restore our wallet, but you can initialize a wallet with mnemonic phrases, private keys, and Cardano CLI generated keys, see [Mesh Wallet](https://meshjs.dev/apis/wallets/meshwallet). - -``` -const blockchainProvider = new BlockfrostProvider( - '' -); - -const meshWallet = new MeshWallet({ - networkId: 0, - fetcher: blockchainProvider, - submitter: blockchainProvider, - key: { - type: 'mnemonic', - words: yourMnemonic, - }, -}); -``` - -Next, let's define the forging script, here we used the first wallet address, but you can also define using **NativeScript**, see [Transaction - Minting assets](https://meshjs.dev/apis/transaction): - -``` -const meshWalletAddress = meshWallet.getPaymentAddress(); -const forgingScript = ForgeScript.withOneSignature(meshWalletAddress); -``` - -Then, we define the **AssetMetadata** which contains the NFT metadata. In a NFT collection mint, you would need a selection algorithm and a database to select available NFTs. - -``` -const assetName = 'MeshToken'; - -const assetMetadata: AssetMetadata = { - name: 'Mesh Token', - image: 'ipfs://QmRzicpReutwCkM6aotuKjErFCUD213DpwPq6ByuzMJaua', - mediaType: 'image/jpg', - description: 'This NFT was minted by Mesh (https://meshjs.dev/).', -}; -``` - -After that, we create the **Mint** object: - -``` -const asset: Mint = { - assetName: assetName, - assetQuantity: '1', - metadata: assetMetadata, - label: '721', - recipient: recipientAddress, -}; -``` - -Finally, we are ready to create the transaction. Instead of using every UTXOs from the client's wallet as transaction's inputs, we can use **largestFirst** to get the UTXOs required for this transaction. In this transaction, we send the payment to a predefined wallet address (**bankWalletAddress**). - -``` -const costLovelace = '10000000'; -const selectedUtxos = largestFirst(costLovelace, utxos, true); -const bankWalletAddress = 'addr_test1qzmwuzc0qjenaljs2ytquyx8y8x02en3qxswlfcldwetaeuvldqg2n2p8y4kyjm8sqfyg0tpq9042atz0fr8c3grjmysm5e6yx'; -``` - -Let's create the transaction. - -``` -const tx = new Transaction({ initiator: meshWallet }); -tx.setTxInputs(selectedUtxos); -tx.mintAsset(forgingScript, asset); -tx.sendLovelace(bankWalletAddress, costLovelace); -tx.setChangeAddress(recipientAddress); -const unsignedTx = await tx.build(); -``` - -Instead of sending the transaction containing the actual metadata, we will mask the metadata so clients do not know the content of the NFT. First we extract the original metadata's CBOR with **Transaction.readMetadata**, and execute **Transaction.maskMetadata** to create a masked transaction. - -``` -const originalMetadata = Transaction.readMetadata(unsignedTx); -// you want to store 'assetName' and 'originalMetadata' into the database so you can retrive it later -const maskedTx = Transaction.maskMetadata(unsignedTx); -``` - -We will send the transaction CBOR (**maskedTx**) to the client for signing. - -## Sign transaction (client) - -In this section, we need the client's signature to send the payment to the **bankWalletAddress**. The client's wallet will open and prompts for payment password. Note that the partial sign is set to **true**. - -``` -const signedTx = await wallet.signTx(maskedTx, true); -``` - -We will send the **signedTx** to the backend to complete the transaction. - -## Sign transaction (application) - -In this section, we will update the asset's metadata with the actual metadata, and the application wallet will counter sign the transaction. - -Let's update the metadata to the actual asset's metadata. We retrieve the **originalMetadata** from the database and update the metadata with **Transaction.writeMetadata**. - -``` -// here you want to retrieve the 'originalMetadata' from the database -const signedOriginalTx = Transaction.writeMetadata( - signedTx, - originalMetadata -); -``` - -Sign the transaction with the application wallet and submit the transaction: - -``` -const meshWalletSignedTx = await meshWallet.signTx(signedOriginalTx, true); -const txHash = await meshWallet.submitTx(meshWalletSignedTx); -``` - -Voila! You can build any multi-sig transactions! diff --git a/apps/playground/src/pages/guides/multisig-minting/demo.tsx b/apps/playground/src/pages/guides/multisig-minting/demo.tsx new file mode 100644 index 00000000..fb063334 --- /dev/null +++ b/apps/playground/src/pages/guides/multisig-minting/demo.tsx @@ -0,0 +1,108 @@ +import { useState } from "react"; + +import { + experimentalSelectUtxos, + ForgeScript, + MeshWallet, + Mint, + Quantity, + Transaction, + Unit, + UTxO, +} from "@meshsdk/core"; +import { CardanoWallet, useWallet } from "@meshsdk/react"; + +import Button from "~/components/button/button"; +import { getProvider } from "~/components/cardano/mesh-wallet"; +import DemoResult from "~/components/sections/demo-result"; +import { demoAddresses, demoAssetMetadata, demoMnemonic } from "~/data/cardano"; + +const walletSystemMnemonic = demoMnemonic; +const walletAccountAddress = demoAddresses.testnetPayment; +const mintingFee = "10000000"; + +export default function Demo() { + const { wallet, connected } = useWallet(); + const [loading, setLoading] = useState(false); + const [response, setResponse] = useState(undefined); + + async function runDemo() { + if (!connected) return; + setLoading(true); + + // get utxos to pay for minting fee + const utxos = await wallet.getUtxos(); + const assetMap = new Map(); + assetMap.set("lovelace", mintingFee); + const selectedUtxos = experimentalSelectUtxos(assetMap, utxos, "5000000"); + + if (selectedUtxos.length === 0) { + setResponse("Not enough funds to mint"); + setLoading(false); + return; + } + + const recipientAddress = await wallet.getChangeAddress(); + + // backend to build tx + const unsignedTx = await backendBuildTx(selectedUtxos, recipientAddress); + const signedTx = await wallet.signTx(unsignedTx, true); + const txHash = await wallet.submitTx(signedTx); + + setResponse(txHash); + setLoading(false); + } + + async function backendBuildTx(userUtxos: UTxO[], recipientAddress: string) { + const blockchainProvider = getProvider(); + + const systemWallet = new MeshWallet({ + networkId: 0, + fetcher: blockchainProvider, + submitter: blockchainProvider, + key: { + type: "mnemonic", + words: walletSystemMnemonic, + }, + }); + + const systemWalletAddress = systemWallet.getChangeAddress(); + const forgingScript = ForgeScript.withOneSignature(systemWalletAddress); + const assetName = "MeshToken"; + + const asset: Mint = { + assetName: assetName, + assetQuantity: "1", + metadata: demoAssetMetadata, + label: "721", + recipient: recipientAddress, + }; + + const tx = new Transaction({ initiator: systemWallet }); + tx.setTxInputs(userUtxos); + tx.mintAsset(forgingScript, asset); + tx.sendLovelace(walletAccountAddress, mintingFee); + tx.setChangeAddress(recipientAddress); + const unsignedTx = await tx.build(); + + const meshWalletSignedTx = await systemWallet.signTx(unsignedTx, true); + + return meshWalletSignedTx; + } + + return ( + <> +

    Demo

    +

    Connect your wallet and click on the button to mint a token.

    + + + + + ); +} diff --git a/apps/playground/src/pages/guides/multisig-minting/index.mdx b/apps/playground/src/pages/guides/multisig-minting/index.mdx new file mode 100644 index 00000000..d1ac331e --- /dev/null +++ b/apps/playground/src/pages/guides/multisig-minting/index.mdx @@ -0,0 +1,160 @@ +import LayoutImageHeaderAndBody from "~/components/layouts/image-header-and-body"; +import { guideminting } from "~/data/links-guides"; +import Demo from "./demo"; + +export default function MDXPage({ children }) { + return ( + + <> + {children} + + + + ); +} + +A multi-signature (multi-sig) transaction requires more than one user to sign a transaction prior to the transaction being broadcast on a blockchain. You can think of it like a husband and wife savings account, where both signatures are required to spend the funds, preventing one spouse from spending the money without the approval of the other. For a multi-sig transaction, you can include 2 or more required signers, these signers can be wallets ([Browser Wallet](https://meshjs.dev/apis/wallets/browserwallet) or [Mesh Wallet](https://meshjs.dev/apis/wallets/meshwallet)) or Plutus script. + +In this guide, we will build a multi-sig transaction for minting. There are 2 wallets involved, + +1. client wallet belonging to the user who wishes to buy a native asset +2. application wallet that holds the forging script + +## See it in action + +In this guide, we will connect to user's CIP30 wallet (`BrowserWallet`) to request for a minting transaction. Then, the backend application wallet (`MeshWallet`) will build the transaction, and we will sign it with our wallet. + +## Connect wallet (client) + +In this section, we will connect client's wallet and obtain their wallet address and UTXO. + +Users can connect their wallet with `BrowserWallet`: + +``` +import { BrowserWallet } from '@meshsdk/core'; +const wallet = await BrowserWallet.enable(walletName); +``` + +Alternatively, you can use the `CardanoWallet` component to connect to the user's wallet: + +``` +import { CardanoWallet, useWallet } from "@meshsdk/react"; + +export default function Page() { + const { wallet, connected } = useWallet(); + + return +} +``` + +Then, we get client's wallet address and UTXOs: + +``` +const recipientAddress = await wallet.getChangeAddress(); +const utxos = await wallet.getUtxos(); +``` + +The change address will be the address receiving the minted NFTs and the transaction's change. Additionally, we will need the client's wallet UTXOs to build the minting transaction. + +We can search for the UTXOs required for the transaction with one of the coin selection algorithms, such as `largestFirst` or `experimentalSelectUtxos`. In this example, we use `experimentalSelectUtxos` to select the UTXOs required for the transaction.: + +``` +const assetMap = new Map(); +assetMap.set("lovelace", mintingFee); +const selectedUtxos = experimentalSelectUtxos(assetMap, utxos, "5000000"); +``` + +The `experimentalSelectUtxos` function will return the UTXOs required for the transaction. The `mintingFee` is the cost of minting the NFT, and the `5000000` is an amount that creates a buffer for the transaction fee. + +Next, we will send the `selectedUtxos` and `recipientAddress` to the backend to build the minting transaction. + +## Build transaction (application) + +In this section, we will build the minting transaction. + +In this guide, we won't be showing how to set up RESTful APIs and backend servers. There are thousands of tutorials on YouTube, we recommend building your backend server with [Vercel API](https://vercel.com/docs/rest-api) or [NestJs](https://www.youtube.com/results?search_query=nestjs) or [ExpressJs](https://www.youtube.com/results?search_query=expressjs). + +First, we initialize a [blockchain provider](https://meshjs.dev/providers) and [`Mesh Wallet`](https://meshjs.dev/apis/wallets/meshwallet). In this example, we use mnemonic to restore our wallet, but you can initialize a wallet with mnemonic phrases, private keys, and Cardano CLI generated keys. + +``` +const blockchainProvider = new BlockfrostProvider( + '' +); + +const meshWallet = new MeshWallet({ + networkId: 0, + fetcher: blockchainProvider, + submitter: blockchainProvider, + key: { + type: 'mnemonic', + words: yourMnemonic, + }, +}); +``` + +Next, let's define the forging script, here we used the first wallet address, but you can also define using `NativeScript`, see [Transaction - Minting assets](https://meshjs.dev/apis/transaction/minting): + +``` +const meshWalletAddress = meshWallet.getChangeAddress(); +const forgingScript = ForgeScript.withOneSignature(meshWalletAddress); +``` + +Then, we define the `AssetMetadata` which contains the NFT metadata. In a NFT collection mint, you would need a selection algorithm and a database to select available NFTs. + +``` +const assetName = 'MeshToken'; + +const assetMetadata: AssetMetadata = { + name: 'Mesh Token', + image: 'ipfs://QmRzicpReutwCkM6aotuKjErFCUD213DpwPq6ByuzMJaua', + mediaType: 'image/jpg', + description: 'This NFT was minted by Mesh (https://meshjs.dev/).', +}; +``` + +After that, we create the `Mint` object: + +``` +const asset: Mint = { + assetName: assetName, + assetQuantity: '1', + metadata: assetMetadata, + label: '721', + recipient: recipientAddress, +}; +``` + +Finally, we are ready to create the transaction. We set the transaction inputs, mint the asset, send the lovelace to the bank wallet, set the change address, and build the transaction: + +``` +const tx = new Transaction({ initiator: meshWallet }); +tx.setTxInputs(userUtxos); +tx.mintAsset(forgingScript, asset); +tx.sendLovelace(bankWalletAddress, mintingFee); +tx.setChangeAddress(recipientAddress); +const unsignedTx = await tx.build(); +``` + +The backend will sign the transaction with the application wallet: + +``` +const meshWalletSignedTx = await systemWallet.signTx(unsignedTx, true); +``` + +And send the signed transaction to the client for their signature. + +## Sign transaction (client) + +In this section, we need the client's signature to send the payment to the `bankWalletAddress`. The client's wallet will open and prompts for payment password. Note that the partial sign is set to `true`. + +``` +const signedTx = await wallet.signTx(unsignedTx, true); +const txHash = await wallet.submitTx(signedTx); +``` + +Voila! You can build any multi-sig transactions! diff --git a/apps/playground/src/pages/guides/multisig-tx/demo.tsx b/apps/playground/src/pages/guides/multisig-tx/demo.tsx new file mode 100644 index 00000000..75beebc1 --- /dev/null +++ b/apps/playground/src/pages/guides/multisig-tx/demo.tsx @@ -0,0 +1,112 @@ +import { useState } from "react"; + +import { + experimentalSelectUtxos, + MeshTxBuilder, + MeshWallet, + Quantity, + Unit, + UTxO, +} from "@meshsdk/core"; +import { CardanoWallet, useWallet } from "@meshsdk/react"; + +import Button from "~/components/button/button"; +import { getProvider } from "~/components/cardano/mesh-wallet"; +import DemoResult from "~/components/sections/demo-result"; +import { demoAddresses, demoAsset, demoMnemonic } from "~/data/cardano"; + +const walletSystemMnemonic = demoMnemonic; +const walletAccountAddress = demoAddresses.testnetPayment; +const mintingFee = "10000000"; +const assetToSend = demoAsset; +const quantityAssetToSend = "1"; + +export default function Demo() { + const { wallet, connected } = useWallet(); + const [loading, setLoading] = useState(false); + const [response, setResponse] = useState(undefined); + + async function runDemo() { + if (!connected) return; + setLoading(true); + + const recipientAddress = await wallet.getChangeAddress(); + + // get utxos to pay for minting fee + const utxos = await wallet.getUtxos(); + const assetMap = new Map(); + assetMap.set("lovelace", mintingFee); + const selectedUtxos = experimentalSelectUtxos(assetMap, utxos, "5000000"); + + if (selectedUtxos.length === 0) { + setResponse("Not enough funds to mint"); + setLoading(false); + return; + } + + // backend to build tx + const unsignedTx = await backendBuildTx(selectedUtxos, recipientAddress); + const signedTx = await wallet.signTx(unsignedTx, true); + const txHash = await wallet.submitTx(signedTx); + + setResponse(txHash); + setLoading(false); + } + + async function backendBuildTx(userUtxos: UTxO[], recipientAddress: string) { + const blockchainProvider = getProvider(); + + const systemWallet = new MeshWallet({ + networkId: 0, + fetcher: blockchainProvider, + submitter: blockchainProvider, + key: { + type: "mnemonic", + words: walletSystemMnemonic, + }, + }); + const systemWalletAddress = systemWallet.getChangeAddress(); + + // get utxos to send the asset + const utxos = await systemWallet.getUtxos(); + const assetMap = new Map(); + assetMap.set(assetToSend, quantityAssetToSend); + const systemUtxos = experimentalSelectUtxos(assetMap, utxos, "0"); + + // determine outputs, need to reduce the assets for each wallet to return the balance back to each wallet + console.log("userUtxos", userUtxos); + console.log("systemUtxos", systemUtxos); + const outputSystem = []; // todo + const outputUser = []; // todo + + // build tx + const txBuilder = new MeshTxBuilder({ fetcher: blockchainProvider }); + const txHex = await txBuilder + .selectUtxosFrom([...userUtxos, ...systemUtxos]) + + .txOut(walletAccountAddress, [{ unit: "lovelace", quantity: mintingFee }]) + .txOut(recipientAddress, outputUser) + .txOut(systemWalletAddress, outputSystem) + .changeAddress(recipientAddress) + .complete(); + + const unsignedTx = wallet.signTx(txHex, true); + return unsignedTx; + } + + return ( + <> +

    Demo

    +

    Connect your wallet and click on the button to get tokens.

    + + + + + ); +} diff --git a/apps/playground/src/pages/guides/prove-wallet-ownership/demo.tsx b/apps/playground/src/pages/guides/prove-wallet-ownership/demo.tsx new file mode 100644 index 00000000..57bb622f --- /dev/null +++ b/apps/playground/src/pages/guides/prove-wallet-ownership/demo.tsx @@ -0,0 +1,41 @@ +import { useState } from "react"; + +import { checkSignature, generateNonce } from "@meshsdk/core"; +import { CardanoWallet, useWallet } from "@meshsdk/react"; + +import Button from "~/components/button/button"; +import DemoResult from "~/components/sections/demo-result"; + +export default function Demo() { + const { wallet, connected } = useWallet(); + const [loading, setLoading] = useState(false); + const [response, setResponse] = useState(undefined); + + async function runDemo() { + if (!connected) return; + + setLoading(true); + const nonce = generateNonce("Sign to login in to Mesh: "); + const signature = await wallet.signData(nonce); + + const result = checkSignature(nonce, signature); + setResponse(result.toString()); + setLoading(false); + } + + return ( + <> +

    Demo

    +

    Connect your wallet and click on the button to sign a message.

    + + + + + ); +} diff --git a/apps/playground/src/pages/guides/prove-wallet-ownership.mdx b/apps/playground/src/pages/guides/prove-wallet-ownership/index.mdx similarity index 91% rename from apps/playground/src/pages/guides/prove-wallet-ownership.mdx rename to apps/playground/src/pages/guides/prove-wallet-ownership/index.mdx index 99bb7829..76dd0fb1 100644 --- a/apps/playground/src/pages/guides/prove-wallet-ownership.mdx +++ b/apps/playground/src/pages/guides/prove-wallet-ownership/index.mdx @@ -6,39 +6,21 @@ import { CardanoWallet, useWallet } from "@meshsdk/react"; import Button from "~/components/button/button"; import LayoutImageHeaderAndBody from "~/components/layouts/image-header-and-body"; import { guideownership } from "~/data/links-guides"; +import Demo from "./demo"; export default function MDXPage({ children }) { -const { wallet, connected } = useWallet(); - const [loading, setLoading] = useState(false); - -async function runDemo() { -if(!connected) return; - -setLoading(true); -const nonce = generateNonce('Sign to login in to Mesh: '); -const signature = await wallet.signData(nonce); -console.log(2, signature) - -const result = checkSignature(nonce, signature); -console.log(4, result); -setLoading(false); -} - -return ( - - <> - {children} -

    Demo

    -

    Connect your wallet and click on the button to sign a message.

    - - - -
    + return ( + + <> + {children} + + + ); } From fa0ebcc7354c8d95ca776b2e4e26350e94a71d8e Mon Sep 17 00:00:00 2001 From: "Hong Jing (Jingles)" Date: Mon, 12 Aug 2024 15:57:52 +0800 Subject: [PATCH 05/21] update docs --- apps/docs/src/components/Logo.tsx | 4 +- .../src/components/layouts/sidebar/index.tsx | 2 +- .../txbuilder/basics/build-with-object.tsx | 6 +- .../apis/txbuilder/basics/coin-selection.tsx | 65 +++++++++++ .../src/pages/apis/txbuilder/basics/index.tsx | 3 + .../pages/apis/txbuilder/basics/multisig.tsx | 3 +- .../apis/txbuilder/basics/send-values.tsx | 1 - .../minting/minting-plutus-script.tsx | 62 +++++----- .../txbuilder/smart-contract/lock-assets.tsx | 4 +- .../smart-contract/send-reference-script.tsx | 5 +- .../smart-contract/unlock-assets.tsx | 107 ++++++++---------- .../apis/wallets/meshwallet/load-wallet.tsx | 2 +- 12 files changed, 155 insertions(+), 109 deletions(-) create mode 100644 apps/playground/src/pages/apis/txbuilder/basics/coin-selection.tsx diff --git a/apps/docs/src/components/Logo.tsx b/apps/docs/src/components/Logo.tsx index c3c1623d..e38adfee 100644 --- a/apps/docs/src/components/Logo.tsx +++ b/apps/docs/src/components/Logo.tsx @@ -8,8 +8,8 @@ export function Logo(props: React.ComponentPropsWithoutRef<'svg'>) { Mesh SDK Docs