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",
+ }}
+ />
+
+
+
+ Create
+
+
+
+
+
+
+
+
+
+
+ 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);
+ }}
+ />
+
+
+
+ Parse
+
+
+
+
+
+
+
+
+
+
+ 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
// =============================================================================