Skip to content

Commit

Permalink
Safe account detection (#702)
Browse files Browse the repository at this point in the history
The account object returned by `useAccount()` now has a `safeStatus`
property, which allows to avoid querying the Safe API for non-Safe
accounts.
  • Loading branch information
bpierre authored Jan 17, 2025
1 parent dd03541 commit 13d4f71
Show file tree
Hide file tree
Showing 18 changed files with 169 additions and 108 deletions.
3 changes: 3 additions & 0 deletions frontend/app/src/demo-mode/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ type DemoModeContext = DemoModeState & {
account: DemoModeState["account"] & {
connect: () => void;
disconnect: () => void;
safeStatus: null;
};
clearDemoMode: () => void;
enabled: boolean;
Expand Down Expand Up @@ -95,6 +96,7 @@ const DemoContext = createContext<DemoModeContext>({
...demoModeStateDefault.account,
connect: noop,
disconnect: noop,
safeStatus: null,
},
clearDemoMode: noop,
enabled: DEMO_MODE,
Expand Down Expand Up @@ -203,6 +205,7 @@ export function DemoMode({
...state.account,
connect,
disconnect,
safeStatus: null,
},
clearDemoMode,
enabled: DEMO_MODE,
Expand Down
77 changes: 77 additions & 0 deletions frontend/app/src/safe-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { Address } from "@/src/types";

import { SAFE_API_URL } from "@/src/env";
import { sleep } from "@/src/utils";
import { vAddress } from "@/src/valibot-utils";
import * as v from "valibot";

async function safeApiCall(path: string) {
if (!SAFE_API_URL) {
throw new Error("SAFE_API_URL is not set");
}
return fetch(`${SAFE_API_URL}/v1${path}`, {
method: "GET",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
});
}

export const SafeTransactionSchema = v.object({
confirmations: v.array(v.object({ owner: vAddress() })),
confirmationsRequired: v.number(),
isExecuted: v.union([v.null(), v.boolean()]),
isSuccessful: v.union([v.null(), v.boolean()]),
transactionHash: v.union([v.null(), v.string()]),
});

export async function getSafeTransaction(safeTxHash: string): Promise<
v.InferOutput<typeof SafeTransactionSchema>
> {
const response = await safeApiCall(`/multisig-transactions/${safeTxHash}`);
if (!response.ok) {
throw new Error(response.statusText);
}
return v.parse(SafeTransactionSchema, await response.json());
}

export const SafeStatusSchema = v.object({
address: vAddress(),
nonce: v.number(),
threshold: v.number(),
owners: v.array(vAddress()),
masterCopy: vAddress(),
modules: v.array(vAddress()),
fallbackHandler: vAddress(),
guard: vAddress(),
version: v.string(),
});

export async function getSafeStatus(safeAddress: Address): Promise<
v.InferOutput<typeof SafeStatusSchema> | null
> {
const response = await safeApiCall(`/safes/${safeAddress}`);

if (response.status === 404) {
return null;
}

if (!response.ok) {
throw new Error(response.statusText);
}

return v.parse(SafeStatusSchema, await response.json());
}

export async function waitForSafeTransaction(safeTxHash: string): Promise<`0x${string}`> {
while (true) {
try {
const safeTransaction = await getSafeTransaction(safeTxHash);
if (safeTransaction.transactionHash !== null) {
return safeTransaction.transactionHash as `0x${string}`;
}
} catch (_) {}
await sleep(2000);
}
}
25 changes: 23 additions & 2 deletions frontend/app/src/services/Ethereum.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
CONTRACT_LUSD_TOKEN,
WALLET_CONNECT_PROJECT_ID,
} from "@/src/env";
import { getSafeStatus } from "@/src/safe-utils";
import { noop } from "@/src/utils";
import { isCollateralSymbol, useTheme } from "@liquity2/uikit";
import {
Expand All @@ -42,7 +43,7 @@ import {
safeWallet,
walletConnectWallet,
} from "@rainbow-me/rainbowkit/wallets";

import { useQuery } from "@tanstack/react-query";
import { useMemo } from "react";
import { match } from "ts-pattern";
import { erc20Abi } from "viem";
Expand Down Expand Up @@ -73,18 +74,38 @@ export function useAccount():
connect: () => void;
disconnect: () => void;
ensName: string | undefined;
safeStatus: Awaited<ReturnType<typeof getSafeStatus>> | null;
}
{
const demoMode = useDemoMode();
const account = useAccountWagmi();
const { openConnectModal } = useConnectModal();
const { openAccountModal } = useAccountModal();
const ensName = useEnsName({ address: account?.address });
return demoMode.enabled ? demoMode.account : {

const safeStatus = useQuery({
queryKey: ["safeStatus", account.address],
enabled: Boolean(account.address),
queryFn: () => {
if (!account.address) {
throw new Error("No account address");
}
return getSafeStatus(account.address);
},
staleTime: Infinity,
refetchInterval: false,
});

if (demoMode.enabled) {
return demoMode.account;
}

return {
...account,
connect: openConnectModal || noop,
disconnect: account.isConnected && openAccountModal || noop,
ensName: ensName.data ?? undefined,
safeStatus: safeStatus.data ?? null,
};
}

Expand Down
19 changes: 11 additions & 8 deletions frontend/app/src/services/TransactionFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import type { Config as WagmiConfig } from "wagmi";
import { LOCAL_STORAGE_PREFIX } from "@/src/constants";
import { getContracts } from "@/src/contracts";
import { jsonParseWithDnum, jsonStringifyWithDnum } from "@/src/dnum-utils";
import { useAccount } from "@/src/services/Ethereum";
import { useStoredState } from "@/src/services/StoredState";
import { noop } from "@/src/utils";
import { vAddress } from "@/src/valibot-utils";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { usePathname, useRouter } from "next/navigation";
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import * as v from "valibot";
import { useAccount, useConfig as useWagmiConfig } from "wagmi";
import { useConfig as useWagmiConfig } from "wagmi";

/* flows registration */

Expand Down Expand Up @@ -158,6 +159,7 @@ export type Flowstate<FlowRequest extends BaseFlowRequest = BaseFlowRequest> = {
export type FlowParams<FlowRequest extends BaseFlowRequest = BaseFlowRequest> = {
account: Address | null;
contracts: Contracts;
isSafe: boolean;
request: FlowRequest;
steps: FlowStep[] | null;
storedState: ReturnType<typeof useStoredState>;
Expand Down Expand Up @@ -242,7 +244,7 @@ export function TransactionFlow({
flowDeclaration,
startFlow,
commit,
} = useFlowManager(account.address ?? null);
} = useFlowManager(account.address ?? null, account.safeStatus !== null);

const start: TransactionFlowContext["start"] = useCallback((request) => {
if (account.address) {
Expand All @@ -266,8 +268,9 @@ export function TransactionFlow({
flowParams: flow && account.address
? {
...flow,
contracts: getContracts(),
account: account.address,
contracts: getContracts(),
isSafe: account.safeStatus !== null,
storedState,
wagmiConfig,
}
Expand Down Expand Up @@ -304,21 +307,20 @@ function useSteps(
throw new Error("Flow declaration not found: " + flow.request.flowId);
}

const context = {
return flowDeclaration.getSteps({
account: account.address,
contracts: getContracts(),
isSafe: account.safeStatus !== null,
request: flow.request,
steps: flow.steps,
storedState,
wagmiConfig,
};

return flowDeclaration.getSteps(context);
});
},
});
}

function useFlowManager(account: Address | null) {
function useFlowManager(account: Address | null, isSafe: boolean = false) {
const [flow, setFlow] = useState<Flowstate<FlowRequestMap[keyof FlowRequestMap]> | null>(null);
const wagmiConfig = useWagmiConfig();
const storedState = useStoredState();
Expand Down Expand Up @@ -357,6 +359,7 @@ function useFlowManager(account: Address | null) {
const params: FlowParams<FlowRequestMap[keyof FlowRequestMap]> = {
account,
contracts: getContracts(),
isSafe,
request: flow.request,
steps: flow.steps,
storedState,
Expand Down
4 changes: 2 additions & 2 deletions frontend/app/src/tx-flows/allocateVotingPower.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,8 @@ export const allocateVotingPower: FlowDeclaration<AllocateVotingPowerRequest> =
});
},

async verify({ wagmiConfig }, hash) {
await verifyTransaction(wagmiConfig, hash);
async verify({ wagmiConfig, isSafe }, hash) {
await verifyTransaction(wagmiConfig, hash, isSafe);
},
},
},
Expand Down
4 changes: 2 additions & 2 deletions frontend/app/src/tx-flows/claimCollateralSurplus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,8 @@ export const claimCollateralSurplus: FlowDeclaration<ClaimCollateralSurplusReque
});
},

async verify({ wagmiConfig }, hash) {
await verifyTransaction(wagmiConfig, hash);
async verify({ wagmiConfig, isSafe }, hash) {
await verifyTransaction(wagmiConfig, hash, isSafe);
},
},
},
Expand Down
8 changes: 4 additions & 4 deletions frontend/app/src/tx-flows/closeLoanPosition.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,8 @@ export const closeLoanPosition: FlowDeclaration<CloseLoanPositionRequest> = {
});
},

async verify({ wagmiConfig }, hash) {
await verifyTransaction(wagmiConfig, hash);
async verify({ wagmiConfig, isSafe }, hash) {
await verifyTransaction(wagmiConfig, hash, isSafe);
},
},

Expand Down Expand Up @@ -207,8 +207,8 @@ export const closeLoanPosition: FlowDeclaration<CloseLoanPositionRequest> = {
});
},

async verify({ request, wagmiConfig }, hash) {
await verifyTransaction(wagmiConfig, hash);
async verify({ request, wagmiConfig, isSafe }, hash) {
await verifyTransaction(wagmiConfig, hash, isSafe);

const prefixedTroveId = getPrefixedTroveId(
request.loan.collIndex,
Expand Down
4 changes: 2 additions & 2 deletions frontend/app/src/tx-flows/earnClaimRewards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,8 @@ export const earnClaimRewards: FlowDeclaration<EarnClaimRewardsRequest> = {
});
},

async verify({ wagmiConfig }, hash) {
await verifyTransaction(wagmiConfig, hash);
async verify({ wagmiConfig, isSafe }, hash) {
await verifyTransaction(wagmiConfig, hash, isSafe);
},
},
},
Expand Down
4 changes: 2 additions & 2 deletions frontend/app/src/tx-flows/earnDeposit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ export const earnDeposit: FlowDeclaration<EarnDepositRequest> = {
});
},

async verify({ wagmiConfig }, hash) {
await verifyTransaction(wagmiConfig, hash);
async verify({ wagmiConfig, isSafe }, hash) {
await verifyTransaction(wagmiConfig, hash, isSafe);
},
},
},
Expand Down
4 changes: 2 additions & 2 deletions frontend/app/src/tx-flows/earnWithdraw.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ export const earnWithdraw: FlowDeclaration<EarnWithdrawRequest> = {
});
},

async verify({ wagmiConfig }, hash) {
await verifyTransaction(wagmiConfig, hash);
async verify({ wagmiConfig, isSafe }, hash) {
await verifyTransaction(wagmiConfig, hash, isSafe);
},
},
},
Expand Down
8 changes: 4 additions & 4 deletions frontend/app/src/tx-flows/openBorrowPosition.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,8 @@ export const openBorrowPosition: FlowDeclaration<OpenBorrowPositionRequest> = {
});
},

async verify({ wagmiConfig }, hash) {
await verifyTransaction(wagmiConfig, hash);
async verify({ wagmiConfig, isSafe }, hash) {
await verifyTransaction(wagmiConfig, hash, isSafe);
},
},

Expand Down Expand Up @@ -239,8 +239,8 @@ export const openBorrowPosition: FlowDeclaration<OpenBorrowPositionRequest> = {
});
},

async verify({ contracts, request, wagmiConfig }, hash) {
const receipt = await verifyTransaction(wagmiConfig, hash);
async verify({ contracts, request, wagmiConfig, isSafe }, hash) {
const receipt = await verifyTransaction(wagmiConfig, hash, isSafe);

// extract trove ID from logs
const collateral = contracts.collaterals[request.collIndex];
Expand Down
8 changes: 4 additions & 4 deletions frontend/app/src/tx-flows/openLeveragePosition.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ export const openLeveragePosition: FlowDeclaration<OpenLeveragePositionRequest>
});
},

async verify({ wagmiConfig }, hash) {
await verifyTransaction(wagmiConfig, hash);
async verify({ wagmiConfig, isSafe }, hash) {
await verifyTransaction(wagmiConfig, hash, isSafe);
},
},

Expand Down Expand Up @@ -211,8 +211,8 @@ export const openLeveragePosition: FlowDeclaration<OpenLeveragePositionRequest>
});
},

async verify({ contracts, request, wagmiConfig }, hash) {
const receipt = await verifyTransaction(wagmiConfig, hash);
async verify({ contracts, request, wagmiConfig, isSafe }, hash) {
const receipt = await verifyTransaction(wagmiConfig, hash, isSafe);

// Extract trove ID from logs
const collToken = getCollToken(request.loan.collIndex);
Expand Down
Loading

0 comments on commit 13d4f71

Please sign in to comment.