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

enable sending a token or a SAC payment to a C address #1424

Merged
merged 4 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const ChooseAsset = ({ balances }: ChooseAssetProps) => {
const isSwap = useIsSwap();
const isSoroswapEnabled = useIsSoroswapEnabled();

const managingAssets = assetSelect.type === AssetSelectType.MANAGE;
const isManagingAssets = assetSelect.type === AssetSelectType.MANAGE;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is just a var name issue that was bugging me , so fixing it here


useEffect(() => {
const fetchDomains = async () => {
Expand Down Expand Up @@ -101,7 +101,7 @@ export const ChooseAsset = ({ balances }: ChooseAssetProps) => {
contract: contractId,
});
// include native asset for asset dropdown selection
} else if (!managingAssets) {
} else if (!isManagingAssets) {
collection.push({
code,
issuer: "",
Expand Down Expand Up @@ -144,7 +144,7 @@ export const ChooseAsset = ({ balances }: ChooseAssetProps) => {
}, [
assetIcons,
balances,
managingAssets,
isManagingAssets,
isSorobanSuported,
isSwap,
isSoroswapEnabled,
Expand All @@ -157,7 +157,7 @@ export const ChooseAsset = ({ balances }: ChooseAssetProps) => {
<React.Fragment>
<SubviewHeader
title={t("Your assets")}
customBackIcon={!managingAssets ? <Icon.Close /> : undefined}
customBackIcon={!isManagingAssets ? <Icon.Close /> : undefined}
/>
<View.Content>
{isLoading && (
Expand All @@ -168,19 +168,19 @@ export const ChooseAsset = ({ balances }: ChooseAssetProps) => {
<div className="ChooseAsset__wrapper">
<div
className={`ChooseAsset__assets${
managingAssets && isSorobanSuported ? "--short" : ""
isManagingAssets && isSorobanSuported ? "--short" : ""
}`}
ref={ManageAssetRowsWrapperRef}
>
{managingAssets ? (
{isManagingAssets ? (
<ManageAssetRows assetRows={assetRows} />
) : (
<SelectAssetRows assetRows={assetRows} />
)}
</div>
</div>
</View.Content>
{managingAssets && (
{isManagingAssets && (
<View.Footer isInline allowWrap>
<div className="ChooseAsset__button">
<Link to={ROUTES.searchAsset}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { useSelector, useDispatch } from "react-redux";
import { Formik, Form, Field, FieldProps } from "formik";
import { Icon, Textarea, Link, Button } from "@stellar/design-system";
import { useTranslation } from "react-i18next";
import { Asset } from "stellar-sdk";

import { navigateTo } from "popup/helpers/navigate";
import { useNetworkFees } from "popup/helpers/useNetworkFees";
import { useIsSwap } from "popup/helpers/useIsSwap";
import { isMuxedAccount } from "helpers/stellar";
import { getNativeContractDetails } from "popup/helpers/searchAsset";
import { isMuxedAccount, getAssetFromCanonical } from "helpers/stellar";
import { ROUTES } from "popup/constants/routes";
import { SubviewHeader } from "popup/components/SubviewHeader";
import { FormRows } from "popup/basics/Forms";
Expand All @@ -19,13 +21,14 @@ import {
saveTransactionFee,
saveSimulation,
transactionSubmissionSelector,
saveIsToken,
} from "popup/ducks/transactionSubmission";
import { simulateTokenPayment, simulateSwap } from "popup/ducks/token-payment";

import { InfoTooltip } from "popup/basics/InfoTooltip";
import { publicKeySelector } from "popup/ducks/accountServices";
import { settingsNetworkDetailsSelector } from "popup/ducks/settings";
import { parseTokenAmount } from "popup/helpers/soroban";
import { parseTokenAmount, isContractId } from "popup/helpers/soroban";
import { Balances, TokenBalance } from "@shared/api/types";
import { AppDispatch } from "popup/App";

Expand Down Expand Up @@ -85,6 +88,24 @@ export const Settings = ({
// dont show memo for regular sends to Muxed, or for swaps
const showMemo = !isSwap && !isMuxedAccount(destination);
const showSlippage = (isPathPayment || isSwap) && !isSoroswap;
const isSendSacToContract =
isContractId(destination) &&
!isContractId(getAssetFromCanonical(asset).issuer);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The logic here is basically:
If you're sending to a C address, but you're trying to send a classic asset, grab the C address of the SAC

const getSacContractAddress = () => {
if (asset === "native") {
return getNativeContractDetails(networkDetails).contract;
}

const assetFromCanonical = new Asset(
getAssetFromCanonical(asset).code,
getAssetFromCanonical(asset).issuer,
);
const contractAddress = assetFromCanonical.contractId(
networkDetails.networkPassphrase,
);

return contractAddress;
};

async function goToReview() {
if (isSoroswap) {
Expand All @@ -109,8 +130,10 @@ export const Settings = ({
return;
}

if (isToken) {
const assetAddress = asset.split(":")[1];
if (isToken || isSendSacToContract) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sending a token to a C address generally worked "out of the box" once I allowed a user to enter a C address as a destination 🎉

const assetAddress = isSendSacToContract
? getSacContractAddress()
: asset.split(":")[1];
const balances =
accountBalances.balances || ({} as NonNullable<Balances>);
const assetBalance = balances[asset] as TokenBalance;
Expand All @@ -119,10 +142,9 @@ export const Settings = ({
throw new Error("Asset Balance not available");
}

const parsedAmount = parseTokenAmount(
amount,
Number(assetBalance.decimals),
);
const parsedAmount = isSendSacToContract
? parseTokenAmount(amount, 7)
: parseTokenAmount(amount, Number(assetBalance.decimals));

const params = {
publicKey,
Expand All @@ -143,6 +165,7 @@ export const Settings = ({

if (simulateTokenPayment.fulfilled.match(simulation)) {
dispatch(saveSimulation(simulation.payload));
dispatch(saveIsToken(true));
navigateTo(next);
}
return;
Expand Down
49 changes: 29 additions & 20 deletions extension/src/popup/components/sendPayment/SendTo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { IdenticonImg } from "popup/components/identicons/IdenticonImg";
import { FormRows } from "popup/basics/Forms";
import { emitMetric } from "helpers/metrics";
import { navigateTo } from "popup/helpers/navigate";
import { isContractId } from "popup/helpers/soroban";
import { METRIC_NAMES } from "popup/constants/metricsNames";
import { ROUTES } from "popup/constants/routes";
import { View } from "popup/basics/layout/View";
Expand Down Expand Up @@ -88,7 +89,7 @@ const InvalidAddressWarning = () => {
icon={<Icon.Warning />}
title={t("INVALID STELLAR ADDRESS")}
>
{t("Addresses are uppercase and begin with letters “G“ or “M“.")}
{t("Addresses are uppercase and begin with letters “G“, “M“, or “C“.")}
</Notification>
</div>
);
Expand All @@ -106,7 +107,7 @@ export const SendTo = ({ previous }: { previous: ROUTES }) => {
);

const [recentAddresses, setRecentAddresses] = useState<string[]>([]);
const [validatedPubKey, setValidatedPubKey] = useState("");
const [validatedAddress, setValidatedAddress] = useState("");
Copy link
Contributor Author

Choose a reason for hiding this comment

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

var rename to reflect we can send to either a public key or a C address

const [fedAddress, setFedAddress] = useState("");
const [isLoading, setIsLoading] = useState(false);

Expand All @@ -122,14 +123,17 @@ export const SendTo = ({ previous }: { previous: ROUTES }) => {
const formik = useFormik({
initialValues: { destination: federationAddress || destination },
onSubmit: () => {
handleContinue(validatedPubKey, fedAddress);
handleContinue(validatedAddress, fedAddress);
},
validateOnChange: false,
validate: (values) => {
if (isValidPublicKey(values.destination)) {
if (
isValidPublicKey(values.destination) ||
isContractId(values.destination)
) {
return {};
}
return { destination: t("invalid public key") };
return { destination: t("invalid destination address") };
},
});

Expand Down Expand Up @@ -157,19 +161,19 @@ export const SendTo = ({ previous }: { previous: ROUTES }) => {
}
// muxed account
if (isMuxedAccount(inputDest)) {
setValidatedPubKey(inputDest);
setValidatedAddress(inputDest);
} else if (isFederationAddress(inputDest)) {
// federation address
try {
const fedResp = await Federation.Server.resolve(inputDest);
setValidatedPubKey(fedResp.account_id);
setValidatedAddress(fedResp.account_id);
setFedAddress(inputDest);
} catch (e) {
formik.setErrors({ destination: t("invalid federation address") });
}
} else {
// else, a regular account
setValidatedPubKey(inputDest);
setValidatedAddress(inputDest);
}
setIsLoading(false);
}, 2000),
Expand All @@ -192,30 +196,35 @@ export const SendTo = ({ previous }: { previous: ROUTES }) => {
setIsLoading(true);
}
// reset
setValidatedPubKey("");
setValidatedAddress("");
setFedAddress("");
db(formik.values.destination);
}, [db, formik.values.destination]);

// on valid input get destination balances
useEffect(() => {
if (!validatedPubKey) {
if (!validatedAddress) {
return;
}

// TODO - remove once wallet-sdk can handle muxed
let publicKey = validatedPubKey;
if (isMuxedAccount(validatedPubKey)) {
const mAccount = MuxedAccount.fromAddress(validatedPubKey, "0");
publicKey = mAccount.baseAccount().accountId();
let address = validatedAddress;

if (isContractId(validatedAddress)) {
return;
}

if (isMuxedAccount(validatedAddress)) {
const mAccount = MuxedAccount.fromAddress(validatedAddress, "0");
address = mAccount.baseAccount().accountId();
}
dispatch(
getDestinationBalances({
publicKey,
publicKey: address,
networkDetails,
}),
);
}, [dispatch, validatedPubKey, networkDetails]);
}, [dispatch, validatedAddress, networkDetails]);

return (
<React.Fragment>
Expand Down Expand Up @@ -262,10 +271,10 @@ export const SendTo = ({ previous }: { previous: ROUTES }) => {
address,
);
const publicKey = fedResp.account_id;
setValidatedPubKey(publicKey);
setValidatedAddress(publicKey);
handleContinue(publicKey, address);
} else {
setValidatedPubKey(address);
setValidatedAddress(address);
handleContinue(address);
}
}}
Expand Down Expand Up @@ -305,8 +314,8 @@ export const SendTo = ({ previous }: { previous: ROUTES }) => {
)}
<div className="SendTo__subheading">Address</div>
<div className="SendTo__subheading-identicon">
<IdenticonImg publicKey={validatedPubKey} />
<span>{truncatedPublicKey(validatedPubKey)}</span>
<IdenticonImg publicKey={validatedAddress} />
<span>{truncatedPublicKey(validatedAddress)}</span>
</div>
</>
) : null}
Expand Down