Skip to content

Commit

Permalink
feat: add pending signature request cancel logic
Browse files Browse the repository at this point in the history
This patch adds a way for users to cancel a pending signature request.
This is so that they don't have to commit a change to the database if it
contains a mistake or was made in error.
  • Loading branch information
pheuberger committed Dec 18, 2024
1 parent c6b11e1 commit 5b17b34
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 43 deletions.
128 changes: 85 additions & 43 deletions components/settings/settings-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,20 @@ import {
FormLabel,
FormMessage,
} from "@/components/ui/form";
import revalidatePathServerAction from "@/app/actions";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { useAddOrUpdateUser, useGetUser } from "@/users/hooks";
import { useAccountDetails } from "@/hooks/useAccountDetails";
import { useAccountStore } from "@/lib/account-store";
import { useCancelSignatureRequest } from "@/safe/signature-requests/useCancelSignatureRequest";
import { errorHasMessage } from "@/lib/errorHasMessage";
import { errorHasReason } from "@/lib/errorHasReason";

import {
parsePendingUserUpdate,
type PendingUserUpdate,
} from "@/settings/pending-user-update-parser";

const formSchema = z.object({
displayName: z.string().max(30, "Max. 30 characters").optional(),
avatar: z.union([z.string().url("Invalid URL"), z.literal("")]).optional(),
Expand Down Expand Up @@ -107,7 +108,12 @@ export const SettingsForm = () => {
}
};

const isPending = isPendingGetUser || isLoadingDetails || isPendingUpdateUser;
const cancelSignatureRequest = useCancelSignatureRequest();
const isPending =
isPendingGetUser ||
isLoadingDetails ||
isPendingUpdateUser ||
cancelSignatureRequest.isPending;

const form = useForm<SettingsFormValues>({
resolver: zodResolver(formSchema),
Expand Down Expand Up @@ -206,6 +212,35 @@ export const SettingsForm = () => {
checkPendingUpdates();
}, [selectedAccount, userData]);

const handleCancelUpdate = async () => {
if (
!selectedAccount?.address ||
!userData?.pendingSignatures[0]?.message_hash
)
return;

// Prevent any other operations while cancellation is in progress
if (cancelSignatureRequest.isPending) return;

try {
await cancelSignatureRequest.mutateAsync({
safeAddress: selectedAccount.address,
messageHash: userData.pendingSignatures[0].message_hash,
});
setPendingUpdate(undefined);

// Reset form with current values to make it editable again
form.reset({
displayName: form.getValues("displayName"),
avatar: form.getValues("avatar"),
});

revalidatePathServerAction("/settings");
} catch (error) {
console.error("Failed to cancel signature request:", error);
}
};

const submitDisabled =
form.formState.isSubmitting ||
!form.formState.isValid ||
Expand Down Expand Up @@ -272,49 +307,56 @@ export const SettingsForm = () => {
)}
Save changes
</Button>

{pendingUpdate && (
<div className="mt-4 flex flex-col gap-3 rounded-lg border border-yellow-200 bg-yellow-50 p-4 text-sm dark:border-yellow-900 dark:bg-yellow-950">
<div className="flex items-center justify-between">
<h3 className="font-semibold text-yellow-800 dark:text-yellow-200">
Pending Update
</h3>
<span className="rounded-full bg-purple-900 px-2 py-1 text-xs font-medium text-yellow-200">
{formatDistanceToNow(
new Date(pendingUpdate.metadata.timestamp),
{ addSuffix: true },
)}
</span>
</div>

<div className="space-y-3 py-2">
<div className="flex flex-col">
<span className="text-xs text-yellow-700 dark:text-yellow-300">
Display Name
</span>
<span className="font-medium text-yellow-900 dark:text-yellow-100">
{pendingUpdate.user.displayName}
</span>
</div>

<div className="flex flex-col">
<span className="text-xs text-yellow-700 dark:text-yellow-300">
Image URL
</span>
<span className="font-medium text-yellow-900 dark:text-yellow-100 break-all">
{pendingUpdate.user.avatar}
</span>
</div>
</div>

<p className="text-yellow-800 dark:text-yellow-200">
The changes will be applied once all required signatures are
collected.
</p>
</div>
)}
</form>
</Form>

{pendingUpdate && (
<div className="mt-4 flex flex-col gap-3 rounded-lg border border-yellow-200 bg-yellow-50 p-4 text-sm dark:border-yellow-900 dark:bg-yellow-950">
<div className="flex items-center justify-between">
<h3 className="font-semibold text-yellow-800 dark:text-yellow-200">
Pending Update
</h3>
<span className="rounded-full bg-purple-900 px-2 py-1 text-xs font-medium text-yellow-200">
{formatDistanceToNow(new Date(pendingUpdate.metadata.timestamp), {
addSuffix: true,
})}
</span>
</div>

<div className="space-y-3 py-2">
<div className="flex flex-col">
<span className="text-xs text-yellow-700 dark:text-yellow-300">
Display Name
</span>
<span className="font-medium text-yellow-900 dark:text-yellow-100">
{pendingUpdate.user.displayName}
</span>
</div>

<div className="flex flex-col">
<span className="text-xs text-yellow-700 dark:text-yellow-300">
Image URL
</span>
<span className="font-medium text-yellow-900 dark:text-yellow-100 break-all">
{pendingUpdate.user.avatar}
</span>
</div>
</div>

<p className="text-yellow-800 dark:text-yellow-200">
The changes will be applied once all required signatures are
collected.
</p>
<Button
variant="outline"
size="sm"
onClick={handleCancelUpdate}
className="mt-2 w-fit border-yellow-300 text-yellow-800 hover:bg-yellow-100 dark:border-yellow-800 dark:text-yellow-200 dark:hover:bg-yellow-900"
>
Cancel pending update
</Button>
</div>
)}
</div>
);
};
96 changes: 96 additions & 0 deletions safe/signature-requests/useCancelSignatureRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { useMutation } from "@tanstack/react-query";
import { useAccount } from "wagmi";

import { useStepProcessDialogContext } from "@/components/global/step-process-dialog";
import { HYPERCERTS_API_URL_REST } from "@/configs/hypercerts";
import { signMessage } from "@/lib/sign-api-message";
import revalidatePathServerAction from "@/app/actions";

const STEP_1 = "step1";
const STEP_2 = "step2";

export const useCancelSignatureRequest = () => {
const { address, chainId } = useAccount();
const dialogContext = useStepProcessDialogContext();

return useMutation({
mutationKey: ["cancelSignatureRequest"],
mutationFn: async ({
safeAddress,
messageHash,
}: {
safeAddress: string;
messageHash: string;
}) => {
if (!address || !chainId) {
throw new Error("No address or chainId found");
}

const { setDialogStep: setStep, setSteps, setOpen } = dialogContext;

setSteps([
{ id: STEP_1, description: "Sign cancellation request" },
{ id: STEP_2, description: "Canceling request" },
]);
setOpen(true);

await setStep(STEP_1);

let signature: string;
try {
signature = await signMessage(address, chainId, {
types: {
SignatureRequest: [
{ name: "cancelSignatureRequestId", type: "string" },
],
},
primaryType: "SignatureRequest",
message: {
cancelSignatureRequestId: `${safeAddress}-${messageHash}`,
},
});
} catch (error) {
await setStep(
STEP_1,
"error",
error instanceof Error ? error.message : "Error signing message",
);
throw error;
}

await setStep(STEP_2);

try {
const response = await fetch(
`${HYPERCERTS_API_URL_REST}/signature-requests/${safeAddress}-${messageHash}/cancel`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
signature,
owner_address: address,
chain_id: chainId,
}),
},
);

if (!response.ok) {
throw new Error("Failed to cancel signature request");
}

await setStep(STEP_2, "completed");
await revalidatePathServerAction("/settings");
setTimeout(() => setOpen(false), 2000);
} catch (error) {
await setStep(
STEP_2,
"error",
error instanceof Error ? error.message : "Error canceling request",
);
throw error;
}
},
});
};
2 changes: 2 additions & 0 deletions users/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export const useGetUser = ({ address }: { address?: string }) => {
where: { safe_address: { eq: $address }, status: { eq: PENDING } }
) {
data {
message_hash
safe_address
message
}
}
Expand Down

0 comments on commit 5b17b34

Please sign in to comment.