Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Import Multisig] Added import multisig data logic #35

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
},
"dependencies": {
"@ant-design/colors": "^7.0.0",
"@apollo/client": "^3.8.1",
"@emotion/cache": "^11.10.5",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
Expand All @@ -29,6 +30,7 @@
"bn.js": "^5.2.1",
"dexie": "^3.2.4",
"fs-extra": "^11.1.1",
"graphql": "^16.8.0",
"next": "13.4.4",
"react": "18.2.0",
"react-dom": "18.2.0",
Expand Down
27 changes: 15 additions & 12 deletions src/components/NetworkBadge/styled.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import isPropValid from "@emotion/is-prop-valid";
import styled from "@emotion/styled";
import { Stack, StackProps } from "@mui/material";

export const StyledStack = styled(Stack)<
StackProps & { logoSize?: { width: number; height: number } }
>(({ logoSize }) => ({
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: ".25rem",
"& img": {
width: logoSize?.width ?? "auto",
height: logoSize?.height ?? "auto",
},
}));
export const StyledStack = styled(Stack, {
shouldForwardProp: (prop) => isPropValid(prop) && prop !== "logoSize",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is definitely the way to go, however when there are several properties it becomes very tedious. But for one or 2 properties let's go this way and lastly try with ownerState or lowercase 😅

})<StackProps & { logoSize?: { width: number; height: number } }>(
({ logoSize }) => ({
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: ".25rem",
"& img": {
width: logoSize?.width ?? "auto",
height: logoSize?.height ?? "auto",
},
})
);
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { Box, Link, TextField, Typography } from "@mui/material";
import {
Box,
CircularProgress,
Link,
TextField,
Typography,
} from "@mui/material";
import { useEffect, useState } from "react";
import { ArrayOneOrMore } from "useink/dist/core";

Expand All @@ -15,6 +21,7 @@ function WalletImportStep({
handleOwners,
handleThreshold,
errors,
setErrors,
step,
walletName,
address,
Expand All @@ -26,11 +33,15 @@ function WalletImportStep({
handleThreshold: (threshold: number) => void;
handleAddress: (address: string, step: number, field?: number) => void;
errors: Array<ValidationError[]>;
setErrors: React.Dispatch<React.SetStateAction<ValidationError[][]>>;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use SetState for better readability

step: number;
address: string;
}) {
const { network } = usePolkadotContext();
const { data, error, isLoading } = useFetchSignersAccount({ address });
const { data, error, isLoading } = useFetchSignersAccount({
address,
walletName,
});
const [tempAddress, setTempAddress] = useState(address);
const { logo, name: networkName } = getChain(network);

Expand All @@ -42,6 +53,20 @@ function WalletImportStep({
[tempAddress]
);

useEffect(() => {
if (!isLoading && address) {
if (error || !data) {
setErrors((prev: Array<Array<ValidationError>>) => {
const newErrors = [...prev];
newErrors[step][0] = {
error: true,
message: "Multisig not found.",
};
return newErrors;
});
}
}
}, [error, isLoading, data, address, setErrors, step]);
useEffect(() => {
handleAddress(address, step);
}, []);
Expand Down Expand Up @@ -75,6 +100,9 @@ function WalletImportStep({
margin="normal"
error={errors[step][0]?.error}
helperText={errors[step][0]?.message}
InputProps={{
endAdornment: isLoading && <CircularProgress size={20} />,
}}
/>
<TextField
label="Name"
Expand Down
92 changes: 49 additions & 43 deletions src/hooks/signatoriesAccount/useFetchSignersAccount.ts
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this logic focuses on getting the owners given an xsignerAccount, a name like useFindXsignerOwners is better.

And in case you imagine it will have more than one hook, its own xsignerOwners folder.

Original file line number Diff line number Diff line change
@@ -1,53 +1,59 @@
import { useEffect, useState } from "react";
import { useQuery } from "@apollo/client";
import gql from "graphql-tag";
import { ArrayOneOrMore } from "useink/dist/core";

import { usePolkadotContext } from "@/context/usePolkadotContext";
import { SignatoriesAccount } from "@/domain/SignatoriesAccount";
import { Owner } from "@/domain/SignatoriesAccount";
import { customReportError } from "@/utils/error";

interface Props {
address: string;
walletName: string;
}

export function useFetchSignersAccount({ address }: Props) {
const [data, setData] = useState<SignatoriesAccount | undefined>(undefined);
const { accountConnected } = usePolkadotContext();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
if (!address) return;

const fetchData = async () => {
setIsLoading(true);
setError(null);
try {
// mock fake promise - This will be replaced once we have the API
const result = await new Promise<SignatoriesAccount>((resolve) => {
setTimeout(() => {
resolve({
address,
name: "My-imported-wallet",
threshold: 1,
owners: [
{
address: accountConnected?.address ?? "",
name: "Signer 1",
},
],
} as SignatoriesAccount);
}, 1000);
});
setData(result);
} catch (err) {
const errorFormated = customReportError(err);
setError(errorFormated);
} finally {
setIsLoading(false);
}
};
interface MultisigData {
id: string;
addressHex: string;
addressSS58: string;
owners: string[];
threshold: number;
}

interface MyQueryResponse {
multisigs: MultisigData[];
}

fetchData();
}, [accountConnected?.address, address]);
const FETCH_MULTISIG = gql`
query MyQuery($address: String!) {
multisigs(where: { addressSS58_eq: $address }) {
owners
threshold
addressSS58
addressHex
}
}
`;

export function useFetchSignersAccount({ address, walletName }: Props) {
const { loading, error, data } = useQuery<MyQueryResponse>(FETCH_MULTISIG, {
variables: { address },
Copy link
Collaborator

@henrypalacios henrypalacios Aug 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would to keep the third parties logic on a repository pattern like the others cases (Index DB, LocalStorage).

Something like that:

class XsignerOwnersRepository implements IXsignerOwnersRepository {
  constructor(private client: ApolloClient<MyQueryResponse, MyQueryVariables>) {}

  async getMultisigByAddress(address: string): Promise<MultisigData | null> {
    const { data } = await this.client.query<MyQueryResponse, MyQueryVariables>({
      query: FETCH_MULTISIG,
      variables: { address },
    });

    return data?.multisigs[0] || null;
  }
}

And then use it in this hook.

skip: !address,
});

const formattedData = data?.multisigs[0]
? {
address: data.multisigs[0].addressSS58,
name: walletName,
threshold: data.multisigs[0].threshold,
owners: data.multisigs[0].owners.map((address, index) => ({
address,
name: `Signer ${index + 1}`,
})) as ArrayOneOrMore<Owner>,
}
: undefined;

return { data, isLoading, error };
return {
data: formattedData,
isLoading: loading,
error: error ? customReportError(error) : null,
};
}
20 changes: 16 additions & 4 deletions src/hooks/signatoriesAccount/useFormSignersAccountState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const INVALID_OWNER_NAME_ERROR =
"Owner name must be less than 50 chars.";
export const INVALID_WALLET_NAME_ERROR =
"Wallet name must exist and be less than 50 chars.";
export const DUPLICATE_ADDRESS_ERROR = "Duplicate owner address detected.";

const VALIDATIONS = {
walletName: (name: string) => name && name.length <= 50,
Expand Down Expand Up @@ -80,16 +81,27 @@ export const useFormSignersAccountState = () => {
let updateRequired = false; // A flag to check if an update is really needed

const newErrors = [...errors];
const seenAddresses = new Set<string>();

newOwners.forEach((owner, index) => {
let errorMessage = "";

if (!VALIDATIONS.ownerAddress(owner.address)) {
errorMessage = INVALID_ADDRESS_ERROR;
// Check for duplicated addresses
if (seenAddresses.has(owner.address)) {
errorMessage = DUPLICATE_ADDRESS_ERROR;
} else {
seenAddresses.add(owner.address);
}

if (!VALIDATIONS.ownerName(owner.name)) {
errorMessage = INVALID_OWNER_NAME_ERROR;
// Continue with other validations if no duplicate error
if (!errorMessage) {
if (!VALIDATIONS.ownerAddress(owner.address)) {
errorMessage = INVALID_ADDRESS_ERROR;
}

if (!VALIDATIONS.ownerName(owner.name)) {
errorMessage = INVALID_OWNER_NAME_ERROR;
}
}

const error = {
Expand Down
56 changes: 32 additions & 24 deletions src/pages/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import "@/styles/globals.css";
import "react-loading-skeleton/dist/skeleton.css";

import { ApolloClient, ApolloProvider, InMemoryCache } from "@apollo/client";
import { CacheProvider, EmotionCache } from "@emotion/react";
import { NextPage } from "next";
import type { AppProps } from "next/app";
Expand All @@ -16,6 +17,11 @@ import { PolkadotContextProvider } from "@/context/usePolkadotContext";
import ThemeCustomization from "@/themes";
import createEmotionCache from "@/utils/createEmotionCache";

const squidClient = new ApolloClient({
uri: "https://squid.subsquid.io/ink-multisig-shibuya/v/v2/graphql",
cache: new InMemoryCache(),
});

// Client-side cache, shared for the whole session of the user in the browser.
const clientSideEmotionCache = createEmotionCache();

Expand All @@ -39,29 +45,31 @@ export default function App(props: ExtendedProps) {
Component.getLayout ?? ((page) => <AppLayout>{page}</AppLayout>);

return (
<CacheProvider value={emotionCache}>
<UseInkProvider
config={{
dappName: "XSigners Wallet",
chains: CHAINS,
}}
>
<PolkadotContextProvider>
<LocalDbProvider>
<SettingsThemeConsumer>
{({ settings }) => {
return (
<ThemeCustomization settings={settings}>
<WalletConnectionGuard walletRequired={walletRequired}>
{getLayout(<Component {...pageProps} />)}
</WalletConnectionGuard>
</ThemeCustomization>
);
}}
</SettingsThemeConsumer>
</LocalDbProvider>
</PolkadotContextProvider>
</UseInkProvider>
</CacheProvider>
<ApolloProvider client={squidClient}>
<CacheProvider value={emotionCache}>
<UseInkProvider
config={{
dappName: "XSigners Wallet",
chains: CHAINS,
}}
>
<PolkadotContextProvider>
<LocalDbProvider>
<SettingsThemeConsumer>
{({ settings }) => {
return (
<ThemeCustomization settings={settings}>
<WalletConnectionGuard walletRequired={walletRequired}>
{getLayout(<Component {...pageProps} />)}
</WalletConnectionGuard>
</ThemeCustomization>
);
}}
</SettingsThemeConsumer>
</LocalDbProvider>
</PolkadotContextProvider>
</UseInkProvider>
</CacheProvider>
</ApolloProvider>
);
}
Loading