diff --git a/src/app/(sidebar)/account/create/page.tsx b/src/app/(sidebar)/account/create/page.tsx index a8cc01fa..ac4c03be 100644 --- a/src/app/(sidebar)/account/create/page.tsx +++ b/src/app/(sidebar)/account/create/page.tsx @@ -20,7 +20,7 @@ export default function CreateAccount() { const generateKeypair = () => { let keypair = Keypair.random(); - account.update(keypair.publicKey()); + account.updatePublicKey(keypair.publicKey()); setSecretKey(keypair.secret()); }; @@ -54,10 +54,12 @@ export default function CreateAccount() { - +
+ +
diff --git a/src/app/(sidebar)/account/fund/page.tsx b/src/app/(sidebar)/account/fund/page.tsx index 84c7a536..d7e5e6e3 100644 --- a/src/app/(sidebar)/account/fund/page.tsx +++ b/src/app/(sidebar)/account/fund/page.tsx @@ -4,10 +4,11 @@ import { useEffect, useState } from "react"; import { Alert, Card, Input, Text, Button } from "@stellar/design-system"; import { shortenStellarAddress } from "@/helpers/shortenStellarAddress"; -import { validatePublicKey } from "@/helpers/validatePublicKey"; import { useFriendBot } from "@/query/useFriendBot"; import { useStore } from "@/store/useStore"; +import { validate } from "@/validate"; + import "../styles.scss"; export default function FundAccount() { @@ -52,8 +53,8 @@ export default function FundAccount() { onChange={(e) => { setGeneratedPublicKey(e.target.value); - const error = validatePublicKey(e.target.value); - setInlineErrorMessage(error); + const error = validate.publicKey(e.target.value); + setInlineErrorMessage(error || ""); }} placeholder="Ex: GCEXAMPLE5HWNK4AYSTEQ4UWDKHTCKADVS2AHF3UI2ZMO3DPUSM6Q4UG" error={inlineErrorMessage} diff --git a/src/app/(sidebar)/account/muxed-create/page.tsx b/src/app/(sidebar)/account/muxed-create/page.tsx index 8a92f33e..a0cf9c16 100644 --- a/src/app/(sidebar)/account/muxed-create/page.tsx +++ b/src/app/(sidebar)/account/muxed-create/page.tsx @@ -1,5 +1,184 @@ "use client"; +import { useState } from "react"; +import { Alert, Button, Card, Input, Text } from "@stellar/design-system"; + +import { useStore } from "@/store/useStore"; + +import { ExpandBox } from "@/components/ExpandBox"; +import { MuxedAccountResult } from "@/components/MuxedAccountResult"; +import { PubKeyPicker } from "@/components/FormElements/PubKeyPicker"; +import { SdsLink } from "@/components/SdsLink"; + +import { muxedAccount } from "@/helpers/muxedAccount"; + +import { validate } from "@/validate"; + +import "../styles.scss"; + export default function CreateMuxedAccount() { - return
Create Muxed Account
; + const { account } = useStore(); + + const [baseAddress, setBaseAddress] = useState( + account.generatedMuxedAccountInput?.baseAddress || "", + ); + const [muxedId, setMuxedId] = useState( + account.generatedMuxedAccountInput?.id || "", + ); + const [baseFieldErrorMessage, setBaseFieldErrorMessage] = + useState(""); + const [muxedFieldError, setMuxedFieldError] = useState(""); + const [sdkError, setSdkError] = useState(""); + + const [isReset, setReset] = useState(false); + + const generateMuxedAccount = () => { + const result = muxedAccount.generate({ + baseAddress, + muxedAccountId: muxedId, + }); + + const { error, muxedAddress } = result; + + if (muxedAddress) { + setReset(false); + account.updateGeneratedMuxedAccount({ + id: muxedId, + baseAddress, + muxedAddress, + }); + + account.updateGeneratedMuxedAccountInput({ + id: muxedId, + baseAddress, + }); + + setSdkError(""); + return; + } + + if (error) { + setSdkError(error); + return; + } + }; + + return ( +
+ +
+
+ + Create Multiplexed Account + + + + A muxed (or multiplexed) account (defined in{" "} + + CAP-27 + {" "} + and briefly{" "} + + SEP-23 + + ) is one that resolves a single Stellar G...account to many + different underlying IDs. + +
+ + { + setReset(true); + setBaseAddress(e.target.value); + + let error = ""; + + if (!e.target.value.startsWith("G")) { + error = "Base account address should start with G"; + } else { + error = validate.publicKey(e.target.value) || ""; + } + + setBaseFieldErrorMessage(error); + }} + error={baseFieldErrorMessage} + copyButton={{ + position: "right", + }} + /> + + { + setReset(true); + setMuxedId(e.target.value); + + const error = validate.positiveInt(e.target.value); + setMuxedFieldError(error || ""); + }} + error={muxedFieldError} + copyButton={{ + position: "right", + }} + /> + +
+ +
+ + + + +
+
+ + + Don’t use in a production environment unless you know what you’re doing. + + + {Boolean(sdkError) && ( + { + setSdkError(""); + }} + title={sdkError} + > + {""} + + )} +
+ ); } diff --git a/src/app/(sidebar)/account/muxed-parse/page.tsx b/src/app/(sidebar)/account/muxed-parse/page.tsx index 609fba8a..3f236f08 100644 --- a/src/app/(sidebar)/account/muxed-parse/page.tsx +++ b/src/app/(sidebar)/account/muxed-parse/page.tsx @@ -1,5 +1,145 @@ "use client"; +import { useState } from "react"; +import { Alert, Card, Text, Button } from "@stellar/design-system"; + +import { useStore } from "@/store/useStore"; + +import { ExpandBox } from "@/components/ExpandBox"; +import { PubKeyPicker } from "@/components/FormElements/PubKeyPicker"; +import { MuxedAccountResult } from "@/components/MuxedAccountResult"; + +import { muxedAccount } from "@/helpers/muxedAccount"; + +import { validate } from "@/validate"; + +import "../styles.scss"; + export default function ParseMuxedAccount() { - return
Parse Muxed Account
; + const { account } = useStore(); + const parsedMuxedAccount = account.parsedMuxedAccount; + + const [muxedAddress, setMuxedAddress] = useState( + account.parsedMuxedAccountInput || "", + ); + + const [muxedFieldError, setMuxedFieldError] = useState(""); + const [sdkError, setSdkError] = useState(""); + + const [isReset, setReset] = useState(false); + + const parseMuxedAccount = () => { + const result = muxedAccount.parse({ + muxedAddress, + }); + + const { error, id, baseAddress } = result; + + if (baseAddress && id) { + setReset(false); + account.updateParsedMuxedAccount({ + id, + baseAddress, + muxedAddress, + }); + account.updateParsedMuxedAccountInput(muxedAddress); + + setSdkError(""); + return; + } + + if (error) { + setSdkError(error); + return; + } + }; + + return ( +
+ +
+
+ + Get Muxed Account from M address + +
+ + { + setReset(true); + setMuxedAddress(e.target.value); + + let error = ""; + + if (!e.target.value.startsWith("M")) { + error = "Muxed account address should start with M"; + } else { + error = validate.publicKey(e.target.value) || ""; + } + + setMuxedFieldError(error); + }} + /> + +
+ +
+ + + + +
+
+ + + Don’t use in a production environment unless you know what you’re doing. + + + {Boolean(sdkError) && ( + { + setSdkError(""); + }} + title={sdkError} + > + {""} + + )} +
+ ); } diff --git a/src/app/(sidebar)/account/styles.scss b/src/app/(sidebar)/account/styles.scss index 6270f70a..98ee1395 100644 --- a/src/app/(sidebar)/account/styles.scss +++ b/src/app/(sidebar)/account/styles.scss @@ -30,4 +30,13 @@ cursor: pointer; } } + + &__result { + display: flex; + flex-direction: column; + gap: pxToRem(16px); + background-color: var(--sds-clr-gray-03); + border-radius: pxToRem(8px); + padding: pxToRem(16px); + } } diff --git a/src/components/FormElements/PubKeyPicker.tsx b/src/components/FormElements/PubKeyPicker.tsx index 8fc26ee5..ed701e0b 100644 --- a/src/components/FormElements/PubKeyPicker.tsx +++ b/src/components/FormElements/PubKeyPicker.tsx @@ -7,10 +7,11 @@ interface PubKeyPickerProps extends Omit { label: string; labelSuffix?: string | React.ReactNode; placeholder?: string; + readOnly?: boolean; value: string; error: string | undefined; // eslint-disable-next-line no-unused-vars - onChange: (e: React.ChangeEvent) => void; + onChange?: (e: React.ChangeEvent) => void; } export const PubKeyPicker = ({ @@ -18,23 +19,21 @@ export const PubKeyPicker = ({ fieldSize = "md", label, labelSuffix, - placeholder = "Example: GCEXAMPLE5HWNK4AYSTEQ4UWDKHTCKADVS2AHF3UI2ZMO3DPUSM6Q4UG", + placeholder = "Ex: GCEXAMPLE5HWNK4AYSTEQ4UWDKHTCKADVS2AHF3UI2ZMO3DPUSM6Q4UG", value, error, onChange, ...props -}: PubKeyPickerProps) => { - return ( - - ); -}; +}: PubKeyPickerProps) => ( + +); diff --git a/src/components/MuxedAccountResult.tsx b/src/components/MuxedAccountResult.tsx new file mode 100644 index 00000000..f21e9045 --- /dev/null +++ b/src/components/MuxedAccountResult.tsx @@ -0,0 +1,50 @@ +import { Input } from "@stellar/design-system"; + +import { PubKeyPicker } from "@/components/FormElements/PubKeyPicker"; + +export const MuxedAccountResult = ({ + baseAddress, + muxedId, + muxedAddress, +}: { + baseAddress: string; + muxedId: string; + muxedAddress: string; +}) => ( +
+ + + + + +
+); diff --git a/src/helpers/muxedAccount.ts b/src/helpers/muxedAccount.ts new file mode 100644 index 00000000..cbeb9bc4 --- /dev/null +++ b/src/helpers/muxedAccount.ts @@ -0,0 +1,57 @@ +import { MuxedAccountFieldType } from "@/types/types"; + +import { Account, MuxedAccount } from "@stellar/stellar-sdk"; + +export const muxedAccount = { + generate: ({ + baseAddress, + muxedAccountId, + }: { + baseAddress: string; + muxedAccountId: string; + }): Partial => { + let muxedAddress = ""; + let error = ""; + + try { + const muxedAccount = new MuxedAccount( + new Account(baseAddress, "0"), + muxedAccountId, + ); + + muxedAddress = muxedAccount.accountId(); + } catch (e: any) { + error = `Something went wrong. ${e.toString()}`; + } + + return { muxedAddress, error }; + }, + parse: ({ + muxedAddress, + }: { + muxedAddress: string; + }): Partial => { + let baseAddress = ""; + let muxedAccountId = ""; + let error = ""; + + try { + const muxedAccount = MuxedAccount.fromAddress(muxedAddress, "0"); + baseAddress = muxedAccount.baseAccount().accountId(); + muxedAccountId = muxedAccount.id(); + + if (!baseAddress) { + throw new Error("Base account for this muxed account was not found."); + } + + if (!muxedAccountId) { + throw new Error( + "Muxed account ID for this muxed account was not found.", + ); + } + } catch (e: any) { + error = `Something went wrong. ${e.toString()}`; + } + return { id: muxedAccountId, baseAddress, error }; + }, +}; diff --git a/src/helpers/validatePublicKey.ts b/src/helpers/validatePublicKey.ts deleted file mode 100644 index 0ea08873..00000000 --- a/src/helpers/validatePublicKey.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { StrKey } from "@stellar/stellar-sdk"; - -export const validatePublicKey = (issuer: string) => { - if (!issuer) { - return "Asset issuer is required."; - } - - if (issuer.startsWith("M")) { - if (!StrKey.isValidMed25519PublicKey(issuer)) { - return "Muxed account address is invalid."; - } - } else if (!StrKey.isValidEd25519PublicKey(issuer)) { - return "Public key is invalid."; - } - - return ""; -}; diff --git a/src/store/createStore.ts b/src/store/createStore.ts index 3731f440..53858c3a 100644 --- a/src/store/createStore.ts +++ b/src/store/createStore.ts @@ -3,7 +3,7 @@ import { immer } from "zustand/middleware/immer"; import { querystring } from "zustand-querystring"; import { sanitizeObject } from "@/helpers/sanitizeObject"; -import { AnyObject, EmptyObj, Network } from "@/types/types"; +import { AnyObject, EmptyObj, Network, MuxedAccount } from "@/types/types"; export interface Store { // Shared @@ -15,8 +15,20 @@ export interface Store { // Account account: { publicKey: string; + generatedMuxedAccountInput: Partial | EmptyObj; + parsedMuxedAccountInput: string | undefined; + generatedMuxedAccount: MuxedAccount | EmptyObj; + parsedMuxedAccount: MuxedAccount | EmptyObj; // eslint-disable-next-line no-unused-vars - update: (value: string) => void; + updatePublicKey: (value: string) => void; + // eslint-disable-next-line no-unused-vars + updateGeneratedMuxedAccountInput: (value: Partial) => void; + // eslint-disable-next-line no-unused-vars + updateParsedMuxedAccountInput: (value: string) => void; + // eslint-disable-next-line no-unused-vars + updateGeneratedMuxedAccount: (value: MuxedAccount) => void; + // eslint-disable-next-line no-unused-vars + updateParsedMuxedAccount: (value: MuxedAccount) => void; reset: () => void; }; @@ -70,7 +82,36 @@ export const createStore = (options: CreateStoreOptions) => // Account account: { publicKey: "", - update: (value: string) => + generatedMuxedAccountInput: {}, + parsedMuxedAccountInput: undefined, + generatedMuxedAccount: {}, + parsedMuxedAccount: {}, + updateGeneratedMuxedAccountInput: (value: Partial) => + set((state) => { + state.account.generatedMuxedAccountInput = { + ...state.account.generatedMuxedAccountInput, + ...value, + }; + }), + updateParsedMuxedAccountInput: (value: string) => + set((state) => { + state.account.parsedMuxedAccountInput = value; + }), + updateGeneratedMuxedAccount: (value: MuxedAccount) => + set((state) => { + state.account.generatedMuxedAccount = { + ...state.account.generatedMuxedAccount, + ...value, + }; + }), + updateParsedMuxedAccount: (value: MuxedAccount) => + set((state) => { + state.account.parsedMuxedAccount = { + ...state.account.parsedMuxedAccount, + ...value, + }; + }), + updatePublicKey: (value: string) => set((state) => { state.account.publicKey = value; }), diff --git a/src/types/types.ts b/src/types/types.ts index c14691f5..8268d9d9 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -41,6 +41,19 @@ export type StatusPageScheduled = { incident_updates: StatusPageIncident[]; }; +// ============================================================================= +// Account +// ============================================================================= +export type MuxedAccount = { + id: string | undefined; + baseAddress: string | undefined; + muxedAddress: string | undefined; +}; + +export type MuxedAccountFieldType = MuxedAccount & { + error: string; +}; + // ============================================================================= // Asset // =============================================================================