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

Manage assets #412

Merged
merged 19 commits into from
Mar 29, 2022
Merged
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
3 changes: 3 additions & 0 deletions @shared/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export interface AccountHistoryInterface {

export interface ErrorMessage {
errorMessage: string;
response?: Horizon.ErrorResponseData.TransactionFailed;
}

declare global {
Expand All @@ -89,3 +90,5 @@ declare global {
freighterApi: { [key: string]: any };
}
}

export type CURRENCY = { code: string; issuer: string; image: string };
Original file line number Diff line number Diff line change
Expand Up @@ -636,7 +636,7 @@ export const popupMessageListener = (request: Request) => {
);

return {
iconUrl: assetIconCache[assetCode],
iconUrl: assetIconCache[assetCode] || "",
};
};

Expand Down
10 changes: 10 additions & 0 deletions extension/src/helpers/stellar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ export const getAssetFromCanonical = (canonical: string) => {
throw new Error(`invalid asset canonical id: ${canonical}`);
};

export const getCanonicalFromAsset = (
assetCode: string,
assetIssuer: string,
) => {
if (assetCode === "XLM" && !assetIssuer) {
return "native";
}
return `${assetCode}:${assetIssuer}`;
};

export const stroopToXlm = (
stroops: BigNumber | string | number,
): BigNumber => {
Expand Down
4 changes: 4 additions & 0 deletions extension/src/popup/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { Preferences } from "popup/views/Preferences";
import { Security } from "popup/views/Security";
import { About } from "popup/views/About";
import { SendPayment } from "popup/views/SendPayment";
import { ManageAssets } from "popup/views/ManageAssets";

import "popup/metrics/views";
import { DEV_SERVER } from "@shared/constants/services";
Expand Down Expand Up @@ -273,6 +274,9 @@ export const Router = () => {
<PublicKeyRoute path={ROUTES.sendPayment}>
<SendPayment />
</PublicKeyRoute>
<PublicKeyRoute path={ROUTES.manageAssets}>
<ManageAssets />
</PublicKeyRoute>
<HomeRoute />
{DEV_SERVER && (
<Route path={ROUTES.debug}>
Expand Down
4 changes: 2 additions & 2 deletions extension/src/popup/basics/BackButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const BackButton = ({
const history = useHistory();

return (
<button
<div
className={`BackButton ${hasBackCopy ? "BackButton--has-copy" : ""}`}
onClick={() => {
if (customBackAction) {
Expand All @@ -28,6 +28,6 @@ export const BackButton = ({
>
<Icon.ArrowLeft />
{hasBackCopy ? <div className="BackButton__copy">Back</div> : null}
</button>
</div>
);
};
2 changes: 0 additions & 2 deletions extension/src/popup/basics/BackButton/styles.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
.BackButton {
align-items: center;
background: none;
border: none;
cursor: pointer;
display: flex;
height: var(--back--button-dimension);
Expand Down
2 changes: 1 addition & 1 deletion extension/src/popup/components/SubviewHeader/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
align-items: center;
color: var(--pal-brand-primary-on);
display: flex;
margin-bottom: 2rem;
padding-bottom: 2rem;
position: relative;

&--title {
Expand Down
10 changes: 6 additions & 4 deletions extension/src/popup/components/account/AccountAssets/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,21 @@ export const AssetIcon = ({
assetIcons: AssetIcons;
code: string;
issuerKey: string;
retryAssetIconFetch: (arg: { key: string; code: string }) => void;
retryAssetIconFetch?: (arg: { key: string; code: string }) => void;
}) =>
assetIcons[code] || code === "XLM" ? (
<img
className="AccountAssets--asset-logo"
className="AccountAssets__asset--logo"
alt={`${code} logo`}
src={code === "XLM" ? StellarLogo : assetIcons[code] || ""}
onError={() => {
retryAssetIconFetch({ key: issuerKey, code });
if (retryAssetIconFetch) {
retryAssetIconFetch({ key: issuerKey, code });
}
}}
/>
) : (
<div className="AccountAssets__asset-bullet" />
<div className="AccountAssets__asset--bullet" />
);

export const AccountAssets = ({
Expand Down
22 changes: 10 additions & 12 deletions extension/src/popup/components/account/AccountAssets/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,18 @@
color: var(--pal-text-primary);
font-size: 1rem;
line-height: 1.5rem;
}

&--asset-logo {
margin-right: 1rem;
width: 2rem;
height: 2rem;
}
&--logo {
margin-right: 1rem;
width: 2rem;
height: 2rem;
}

&__asset-bullet {
background: var(--pal-brand-primary);
border-radius: 10rem;
height: 0.5rem;
margin: 0 1.375rem 0 0.5rem;
width: 0.5rem;
&--bullet {
@extend .AccountAssets__asset--logo;
background: var(--pal-brand-primary);
border-radius: 10rem;
}
}

&__copy-left {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export const TransactionDetail = ({

// TODO: Combine these 2 into 1 call. getIconUrlFromIssuer load's the issuer account from Horizon.
// Find a way to get the icon and the home domain in one call even when icon is cached
// https://github.com/stellar/freighter/issues/410
try {
({ home_domain: assetDomain } = await server.loadAccount(assetIssuer));
} catch (e) {
Expand Down
124 changes: 124 additions & 0 deletions extension/src/popup/components/manageAssets/AddAsset/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import React, { useState } from "react";
import { Button, Input, InfoBlock } from "@stellar/design-system";
import { Form, Formik, Field, FieldProps } from "formik";
import StellarSdk from "stellar-sdk";

import { SubviewHeader } from "popup/components/SubviewHeader";

import { FormRows } from "popup/basics/Forms";

import { CURRENCY } from "@shared/api/types";

import { ManageAssetRows, ManageAssetCurrency } from "../ManageAssetRows";

import "./styles.scss";

interface FormValues {
assetDomain: string;
}
const initialValues: FormValues = {
assetDomain: "",
};

interface AssetDomainToml {
CURRENCIES?: CURRENCY[];
DOCUMENTATION?: { ORG_URL: string };
}

interface AddAssetProps {
setErrorAsset: (errorAsset: string) => void;
}

export const AddAsset = ({ setErrorAsset }: AddAssetProps) => {
const [assetRows, setAssetRows] = useState([] as ManageAssetCurrency[]);
const [isCurrencyNotFound, setIsCurrencyNotFound] = useState(false);

const handleSubmit = async (values: FormValues) => {
setIsCurrencyNotFound(false);
setAssetRows([]);

const { assetDomain } = values;
const assetDomainStr = assetDomain.startsWith("http")
? assetDomain
: `https://${assetDomain}`;
const assetDomainUrl = new URL(assetDomainStr.replace(/\/$/, ""));

let assetDomainToml = {} as AssetDomainToml;

try {
assetDomainToml = await StellarSdk.StellarTomlResolver.resolve(
assetDomainUrl.host,
);
} catch (e) {
console.error(e);
}

if (!assetDomainToml.CURRENCIES) {
setIsCurrencyNotFound(true);
} else {
setAssetRows(
assetDomainToml.CURRENCIES.map((currency) => ({
...currency,
domain: assetDomainToml.DOCUMENTATION?.ORG_URL || "",
})),
);
}
};

return (
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
{({ dirty, errors, isSubmitting, isValid, touched }) => (
<Form>
<div className="AddAsset">
<SubviewHeader title="Add Another Asset" />
<FormRows>
<div>
<Field name="assetDomain">
{({ field }: FieldProps) => (
<Input
autoComplete="off"
id="assetDomain"
placeholder="Asset Domain"
error={
errors.assetDomain && touched.assetDomain
? errors.assetDomain
: ""
}
{...field}
/>
)}
</Field>
</div>
<div className="AddAsset__results">
{isCurrencyNotFound ? (
<InfoBlock>Currency not found</InfoBlock>
) : null}
{assetRows.length ? (
<>
<div className="AddAsset__title">
Assets found in this domain
</div>
<ManageAssetRows
assetRows={assetRows}
setErrorAsset={setErrorAsset}
/>
</>
) : null}
</div>
<div>
<Button
fullWidth
type="submit"
isLoading={isSubmitting}
disabled={!(dirty && isValid)}
>
Search
</Button>
</div>
</FormRows>
</div>
</Form>
)}
</Formik>
);
};
16 changes: 16 additions & 0 deletions extension/src/popup/components/manageAssets/AddAsset/styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.AddAsset {
padding: var(--popup-vertical-padding) var(--popup--side-padding);

&__title {
font-size: var(--font-size-secondary);
font-weight: var(--font-weight-medium);
line-height: 1.5rem;
margin: 0.5rem 0 1rem 0;
text-transform: uppercase;
}

&__results {
flex-grow: 1;
height: 23.75rem;
}
}
88 changes: 88 additions & 0 deletions extension/src/popup/components/manageAssets/ChooseAsset/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import React, { useEffect, useState } from "react";
import { useSelector } from "react-redux";
import StellarSdk from "stellar-sdk";
import { Button } from "@stellar/design-system";
import { Link } from "react-router-dom";

import { ROUTES } from "popup/constants/routes";
import { sortBalances } from "popup/helpers/account";
import { transactionSubmissionSelector } from "popup/ducks/transactionSubmission";
import { settingsNetworkDetailsSelector } from "popup/ducks/settings";

import { SubviewHeader } from "popup/components/SubviewHeader";

import { Balances } from "@shared/api/types";

import { ManageAssetCurrency, ManageAssetRows } from "../ManageAssetRows";

import "./styles.scss";

interface ChooseAssetProps {
balances: Balances;
setErrorAsset: (errorAsset: string) => void;
}

export const ChooseAsset = ({ balances, setErrorAsset }: ChooseAssetProps) => {
const { assetIcons } = useSelector(transactionSubmissionSelector);
const { networkUrl } = useSelector(settingsNetworkDetailsSelector);
const [assetRows, setAssetRows] = useState([] as ManageAssetCurrency[]);

useEffect(() => {
const fetchDomains = async () => {
const collection = [] as ManageAssetCurrency[];
const sortedBalances = sortBalances(balances);

// TODO: cache home domain when getting asset icon
// https://github.com/stellar/freighter/issues/410
for (let i = 0; i < sortedBalances.length; i += 1) {
const {
token: { code, issuer },
} = sortedBalances[i];
const server = new StellarSdk.Server(networkUrl);

let domain = "";

if (issuer?.key) {
try {
// eslint-disable-next-line no-await-in-loop
({ home_domain: domain } = await server.loadAccount(issuer.key));
} catch (e) {
console.error(e);
}
}

collection.push({
code,
issuer: issuer?.key || "",
image: assetIcons[code],
domain,
});
}

setAssetRows(collection);
};

fetchDomains();
}, [assetIcons, balances, networkUrl]);

return (
<div className="ChooseAsset">
<SubviewHeader title="Choose Asset" />
<div className="ChooseAsset__wrapper">
<div className="ChooseAsset__assets">
<ManageAssetRows
assetRows={assetRows}
setErrorAsset={setErrorAsset}
/>
</div>
<div className="ChooseAsset__button">
<Link to={ROUTES.addAsset}>
<Button fullWidth variant={Button.variant.tertiary}>
Add another asset
</Button>
</Link>
</div>
</div>
</div>
);
};
Loading