Skip to content

Commit

Permalink
feat: 🎸 add consent to all places rather than up-front
Browse files Browse the repository at this point in the history
  • Loading branch information
apttx committed Mar 27, 2024
1 parent c011c1c commit f9a6ac9
Show file tree
Hide file tree
Showing 14 changed files with 752 additions and 373 deletions.
32 changes: 21 additions & 11 deletions packages/dev-frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@ import { DisposableWalletProvider } from "./testUtils/DisposableWalletProvider";
import { LiquityFrontend } from "./LiquityFrontend";
import { AppLoader } from "./components/AppLoader";
import { useAsyncValue } from "./hooks/AsyncValue";
import { mainnet as hederaMainnet, testnet as hederaTestnet, previewnet as hederaPreviewnet } from "./hedera/wagmi-chains";
import {
mainnet as hederaMainnet,
testnet as hederaTestnet,
previewnet as hederaPreviewnet
} from "./hedera/wagmi-chains";
import { AuthenticationProvider, LoginForm } from "./authentication";
import { HederaTokensProvider } from "./hedera/hedera_context";

const isDemoMode = import.meta.env.VITE_APP_DEMO_MODE === "true";

Expand Down Expand Up @@ -82,26 +87,29 @@ const UnsupportedNetworkFallback: React.FC = () => (
const App = () => {
const config = useAsyncValue(getConfig);

if(!config.loaded) {
return <ThemeProvider theme={theme} />
if (!config.loaded) {
return <ThemeProvider theme={theme} />;
}

// TODO: no deployments on previewnet and mainnet yet
const chains =
// eslint-disable-next-line no-constant-condition
const chains = (isDemoMode || import.meta.env.MODE === "test" || config.value.testnetOnly /* TODO: no deployments on previewnet and mainnet yet */ || true)
? [hederaTestnet]
: [hederaTestnet, hederaPreviewnet, hederaMainnet]
isDemoMode ||
import.meta.env.MODE === "test" ||
config.value.testnetOnly /* TODO: no deployments on previewnet and mainnet yet */ ||
true
? [hederaTestnet]
: [hederaTestnet, hederaPreviewnet, hederaMainnet];
const loader = <AppLoader />;
const client = createClient(
getDefaultClient({
appName: "Liquity",
chains,
walletConnectProjectId: config.value.walletConnectProjectId,
infuraId: config.value.infuraApiKey,
alchemyId: config.value.alchemyApiKey,
alchemyId: config.value.alchemyApiKey
})
)

);

return (
<ThemeProvider theme={theme}>
Expand All @@ -115,13 +123,15 @@ const App = () => {
unsupportedMainnetFallback={<UnsupportedMainnetFallback />}
>
<TransactionProvider>
<LiquityFrontend loader={loader} />
<HederaTokensProvider>
<LiquityFrontend loader={loader} />
</HederaTokensProvider>
</TransactionProvider>
</LiquityProvider>
</WalletConnector>
</ConnectKitProvider>
</WagmiConfig>
</AuthenticationProvider>
</AuthenticationProvider>
</ThemeProvider>
);
};
Expand Down
262 changes: 3 additions & 259 deletions packages/dev-frontend/src/LiquityFrontend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,196 +21,15 @@ import { StabilityViewProvider } from "./components/Stability/context/StabilityV
import { StakingViewProvider } from "./components/Staking/context/StakingViewProvider";
import "tippy.js/dist/tippy.css"; // Tooltip default style
import { BondsProvider } from "./components/Bonds/context/BondsProvider";
import { useAccount, useSigner } from "wagmi";

import { Signer as EthersSigner, Contract } from "ethers";
import { TransactionResponse } from "@ethersproject/abstract-provider";
import { Icon } from "./components/Icon";
import { TokenId } from "@hashgraph/sdk";
import { useHederaChain } from "./hedera/wagmi-chains";
import { Imprint } from "./components/Imprint";

type LiquityFrontendProps = {
loader?: React.ReactNode;
};

const associateToken = async (options: { signer: EthersSigner; tokenAddress: string }) => {
const abi = [`function associate()`, `function dissociate()`];
const gasLimit = 1000000;

try {
const associationContract = new Contract(options.tokenAddress, abi, options.signer);
const associationTransaction: TransactionResponse = await associationContract.associate({
gasLimit: gasLimit
});
const associationReceipt = await associationTransaction.wait();
return associationReceipt;
} catch (error: unknown) {
const errorMessage = `couldn't associate token ${JSON.stringify(options.tokenAddress)}`;
console.error(errorMessage, error);
throw new Error(errorMessage, { cause: error });
}
};

interface HederaApiToken {
token_id: `0.0.${string}`;
}
interface HederaApiTokensData {
tokens: HederaApiToken[];
}
interface HederaToken {
id: `0.0.${string}`;
}

const fetchTokens = async (options: { apiBaseUrl: string; accountAddress: `0x${string}` }) => {
const accountAddressUrlSegment = options.accountAddress.replace(/^0x/, "");
// TODO: get api endpoint based on chain id
const response = await fetch(`${options.apiBaseUrl}/accounts/${accountAddressUrlSegment}/tokens`, {
method: "GET",
headers: {}
});

if (!response.ok) {
const responseText = await response.text();
const errorMessage = `tokens api responded with ${response.status}: \`${responseText}\``;
console.error(errorMessage, { responseText, response });
throw errorMessage;
}

const data: HederaApiTokensData = await response.json();

const tokens = data.tokens.map(tokenData => {
const id = tokenData.token_id;
const token = {
id
};

return token;
});

return tokens;
};

export const LiquityFrontend: React.FC<LiquityFrontendProps> = ({ loader }) => {
const { account: accountAddress, provider, liquity, config } = useLiquity();
const signerResult = useSigner();
const account = useAccount();
const hederaChain = useHederaChain();
const [tokens, setTokens] = useState<HederaToken[]>([]);
const [isConsentOverridden, setIsConsentOverridden] = useState(false);
const [tokensApiError, setTokensApiError] = useState<Error | null>(null);
useMemo(async () => {
if (!account.address) {
return;
}

if (!hederaChain) {
return;
}

try {
const tokens = await fetchTokens({
apiBaseUrl: hederaChain.apiBaseUrl,
accountAddress: account.address
});

setTokens(tokens);
} catch (error: unknown) {
setTokensApiError(error as Error);
}
}, [account.address, hederaChain]);

const hchfTokenId = TokenId.fromSolidityAddress(config.hchfTokenId).toString();
const hasAssociatedWithHchf = tokens.some(token => {
const isHchf = token.id === hchfTokenId;
return isHchf;
});
const hlqtTokenId = TokenId.fromSolidityAddress(config.hlqtTokenId).toString();
const hasAssociatedWithHlqty = tokens.some(token => {
const isHlqty = token.id === hlqtTokenId;
return isHlqty;
});
const hasConsentedToAll =
isConsentOverridden || [hasAssociatedWithHchf, hasAssociatedWithHlqty].every(consent => consent);

// TODO: move consent to separate component
const [isLoadingHchfAssociation, setIsLoadingHchfAssociation] = useState(false);
const associateHchf = async () => {
setIsLoadingHchfAssociation(true);
try {
if (!signerResult.data) {
throw new Error(
`need \`liquity.connection.signer\` to be defined to sign token association transactions`
);
}
const signer = signerResult.data;

await associateToken({ tokenAddress: config.hchfTokenId, signer });

if (!account.address) {
console.warn(
"need an account address to update the account info. refresh the page to get up-to-date (token) info."
);
return;
}

if (!hederaChain) {
console.warn(
"need a hedera chain to update the account info. refresh the page to get up-to-date (token) info."
);
return;
}

const tokens = await fetchTokens({
apiBaseUrl: hederaChain.apiBaseUrl,
accountAddress: account.address
});

setTokens(tokens);
} catch {
// eslint
}
setIsLoadingHchfAssociation(false);
};

const [isLoadingHlqtyAssociation, setIsLoadingHlqtyAssociation] = useState(false);
const associateHlqty = async () => {
setIsLoadingHlqtyAssociation(true);
try {
if (!signerResult.data) {
throw new Error(
`need \`liquity.connection.signer\` to be defined to sign token association transactions`
);
}
const signer = signerResult.data;

await associateToken({ tokenAddress: config.hlqtTokenId, signer });

if (!account.address) {
console.warn(
"need an account address to update the account info. refresh the page to get up-to-date (token) info."
);
return;
}

if (!hederaChain) {
console.warn(
"need a hedera chain to update the account info. refresh the page to get up-to-date (token) info."
);
return;
}

const tokens = await fetchTokens({
apiBaseUrl: hederaChain.apiBaseUrl,
accountAddress: account.address
});

setTokens(tokens);
} catch {
// eslint
}
setIsLoadingHlqtyAssociation(false);
};
const { account: accountAddress, provider, liquity } = useLiquity();

// For console tinkering ;-)
Object.assign(window, {
Expand All @@ -225,82 +44,7 @@ export const LiquityFrontend: React.FC<LiquityFrontendProps> = ({ loader }) => {

return (
<LiquityStoreProvider {...{ loader }} store={liquity.store}>
{!hasConsentedToAll ? (
<Flex
sx={{
flexDirection: "column",
minHeight: "100%",
justifyContent: "center",
marginInline: "clamp(2rem, 100%, 50% - 16rem)"
}}
>
<Heading>Consent to HLiquity</Heading>

<Paragraph sx={{ marginTop: "1rem" }}>
You have to associate with HLiquity tokens and approve HLiquity contracts before you can
use HLiquity.
</Paragraph>

<Flex
sx={{
marginTop: "2rem",
flexDirection: "column",
minHeight: "100%",
gap: "1rem",
justifyContent: "center"
}}
>
<Button
disabled={isLoadingHchfAssociation || hasAssociatedWithHchf}
onClick={associateHchf}
variant={hasAssociatedWithHchf ? "success" : "primary"}
sx={{
gap: "1rem"
}}
>
<span>Consent to receiving HCHF</span>
{hasAssociatedWithHchf && <Icon name="check" />}
{isLoadingHchfAssociation && <Spinner size="1rem" color="inherit" />}
</Button>

<Button
disabled={isLoadingHlqtyAssociation || hasAssociatedWithHlqty}
onClick={associateHlqty}
variant={hasAssociatedWithHlqty ? "success" : "primary"}
sx={{
gap: "1rem"
}}
>
<span>Consent to receiving HLQT</span>
{hasAssociatedWithHlqty && <Icon name="check" />}
{isLoadingHlqtyAssociation && <Spinner size="1rem" color="inherit" />}
</Button>
</Flex>

{tokensApiError && (
<>
<Heading sx={{ marginTop: "4rem", color: "danger" }}>
Couldn't check your associations
</Heading>

<Paragraph sx={{ marginTop: "1rem", color: "danger" }}>
Something went wrong while fetching which tokens you're associated with. Continuing
without consent will cause transactions to fail.
</Paragraph>

<Button
onClick={() => {
setIsConsentOverridden(true);
}}
variant="danger"
sx={{ marginTop: "2rem" }}
>
Continue anyway
</Button>
</>
)}
</Flex>
) : (
{
<Router>
<TroveViewProvider>
<StabilityViewProvider>
Expand Down Expand Up @@ -336,7 +80,7 @@ export const LiquityFrontend: React.FC<LiquityFrontendProps> = ({ loader }) => {
</StabilityViewProvider>
</TroveViewProvider>
</Router>
)}
}

<footer sx={{ marginInline: "clamp(2rem, 100%, 50% - 38rem)", paddingBottom: "2rem" }}>
<Imprint />
Expand Down
Loading

0 comments on commit f9a6ac9

Please sign in to comment.