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

[Draft] feat: add guardian recovery #52

Draft
wants to merge 51 commits into
base: main
Choose a base branch
from
Draft
Changes from 1 commit
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
880f3d2
feat: add initial guardian recovery views
aon Jan 9, 2025
e6efc33
feat: add confirm guardian view
aon Jan 14, 2025
d2f4d8e
feat: improve guardian view with edge cases
aon Jan 15, 2025
eb03f6f
fix: remove commented code
aon Jan 15, 2025
213fff5
Merge pull request #1 from Moonsong-Labs/feat/initial-recovery-views
aon Jan 16, 2025
fee0c9f
Merge pull request #2 from Moonsong-Labs/feat/confirm-guardian-view
aon Jan 16, 2025
5fc1724
feat: improve component imports
aon Jan 17, 2025
774d978
feat: add logout icon in desktop breakpoint
aon Jan 17, 2025
7259168
fix: naming
aon Jan 17, 2025
14c454b
Merge pull request #3 from Moonsong-Labs/feat/add-recovery-flow
aon Jan 17, 2025
72e653e
Merge remote-tracking branch 'upstream/main' into update-upstream
aon Jan 17, 2025
ed8c70c
fix: pnpm lock
aon Jan 17, 2025
9fd9cc8
Merge pull request #4 from Moonsong-Labs/update-upstream
aon Jan 17, 2025
407db29
fix: wrong nav component import
aon Jan 17, 2025
2c2ee47
fix: add missing package to cspell
aon Jan 17, 2025
6864fc0
feat: add recover account views
aon Jan 20, 2025
b4213e0
feat: add unknown account page
aon Jan 20, 2025
f05f3cd
feat: improve account init recovery start
aon Jan 21, 2025
b6d13d9
feat: reorganize routes with typed routes
aon Jan 21, 2025
27d66e9
feat: add recovery process warning when logged in
aon Jan 21, 2025
96e841e
feat: add account not ready page
aon Jan 21, 2025
9ad3286
fix: confirm-guardian page
aon Jan 21, 2025
5bb9e60
feat: add base guardian recovery module
MiniRoman Jan 22, 2025
565cf05
chore: update contracts submodule
MiniRoman Jan 22, 2025
47c4c4f
chore: update contracts submodule
MiniRoman Jan 23, 2025
71df9a9
chore: update contracts submodule
MiniRoman Jan 23, 2025
2885ef9
Merge pull request #5 from Moonsong-Labs/feat/add-recover-account-views
aon Jan 23, 2025
ba6a841
Merge pull request #6 from Moonsong-Labs/feat/add-contracts-integration
aon Jan 23, 2025
c9c065b
feat: add sso account validation
aon Jan 23, 2025
b289130
Update packages/auth-server/pages/recovery/guardian/index.vue
aon Jan 24, 2025
3d9ddd8
Merge pull request #7 from Moonsong-Labs/feat/add-recovery-guardian-r…
aon Jan 24, 2025
acc3fd3
feat: add integration with /recovery/guardian/find-account
aon Jan 27, 2025
cb1ce10
Merge pull request #8 from Moonsong-Labs/feat/integrate-route-recover…
aon Jan 27, 2025
da9dc3b
feat: integrate contracts in guardians settings page
MiniRoman Jan 28, 2025
602987e
feat: set proper path to confirm-guardian page
MiniRoman Jan 28, 2025
56f4eb9
feat: add guardian confirmation integration
MiniRoman Jan 28, 2025
9f1b187
feat: add ui improvements
aon Jan 28, 2025
05ea4b7
feat: address pr comments
MiniRoman Jan 29, 2025
859e9d8
Merge pull request #9 from Moonsong-Labs/feat/integrate-contract-in-g…
aon Jan 29, 2025
bf8656c
feat: merge from dev
aon Jan 30, 2025
af08b5a
feat: update contracts submodule
aon Jan 30, 2025
af569f7
Merge pull request #10 from Moonsong-Labs/feat/ui-improvements
aon Jan 30, 2025
8224b10
chore: update contract submodule
aon Jan 30, 2025
8fe79b7
feat: add integration to confirm recovery view
MiniRoman Feb 4, 2025
210bfda
feat: add integration with cancel recovery (#53)
aon Feb 4, 2025
424faf4
feat: add verify recovery view on the main page
aon Feb 4, 2025
36232c0
chore: update contracts
aon Feb 4, 2025
3d171fc
feat: add missing nuxt config
aon Feb 5, 2025
157fc49
feat: improve confirm guardian flow
aon Feb 12, 2025
151896c
chore: update contracts package
aon Feb 12, 2025
7890bea
feat: execute pending recovery on login (#57)
MiniRoman Feb 12, 2025
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
Prev Previous commit
Next Next commit
feat: integrate contracts in guardians settings page
MiniRoman committed Jan 28, 2025

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
commit da9dc3b145fd8edbf62ae93dd079831f02180288
Original file line number Diff line number Diff line change
@@ -30,17 +30,28 @@
class="flex flex-col gap-4 flex-1 text-left justify-center px-6"
>
<p>Insert address</p>
<Input />

<ZkInput
id="address"
v-model="address"
placeholder="0x..."
:error="!!addressError"
:messages="addressError ? [addressError] : undefined"
@input="validateAddress"
/>
<div class="flex gap-3">
<Button @click="currentStep = 'confirm'">
<ZkButton
:loading="proposeGuardianInProgress"
@click="proposeGuardian()"
>
Continue
</Button>
<Button
</ZkButton>
<ZkButton
type="secondary"
@click="currentStep = 'info'"
>
Back
</Button>
</ZkButton>
</div>
</div>

@@ -63,14 +74,19 @@
</template>

<script setup lang="ts">
import type { Address } from "viem";
import { ref } from "vue";

import Button from "~/components/zk/button.vue";
import Input from "~/components/zk/input.vue";
import Link from "~/components/zk/link.vue";

type GuardianStep = "info" | "add-guardian" | "confirm";
const currentStep = ref<GuardianStep>("info");
const { proposeGuardian: proposeGuardianAction, proposeGuardianInProgress } = useRecoveryGuardian();

const address = ref("" as Address);
const addressError = ref("");
const isValidAddress = ref(false);

const props = defineProps<{
closeModal: () => void;
@@ -83,4 +99,20 @@ defineEmits<{
function completeSetup() {
props.closeModal();
}

const proposeGuardian = async () => {
await proposeGuardianAction(address.value);
currentStep.value = "confirm";
};

const validateAddress = () => {
const result = AddressSchema.safeParse(address.value);
if (result.success) {
addressError.value = "";
isValidAddress.value = true;
} else {
addressError.value = "Not a valid address";
isValidAddress.value = false;
}
};
</script>
63 changes: 62 additions & 1 deletion packages/auth-server/composables/useRecoveryGuardian.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import type { Address } from "viem";
import { GuardianRecoveryModuleAbi } from "zksync-sso/abi";

const getGuardiansInProgress = ref(false);
const getGuardiansError = ref<Error | null>(null);
const getGuardiansData = ref<readonly { addr: Address; isReady: boolean }[] | null>(null);

export const useRecoveryGuardian = () => {
const { getPublicClient, defaultChain } = useClientStore();
const { getClient, getPublicClient, defaultChain } = useClientStore();
const paymasterAddress = contractsByChain[defaultChain!.id].accountPaymaster;

const getGuardedAccountsInProgress = ref(false);
const getGuardedAccountsError = ref<Error | null>(null);
@@ -27,9 +32,65 @@ export const useRecoveryGuardian = () => {
}
}

async function getGuardians(guardedAccount: Address) {
getGuardiansInProgress.value = true;
getGuardiansError.value = null;

try {
const client = getPublicClient({ chainId: defaultChain.id });
const data = await client.readContract({
address: contractsByChain[defaultChain.id].recovery,
abi: GuardianRecoveryModuleAbi,
functionName: "guardiansFor",
args: [guardedAccount],
});
getGuardiansData.value = data;
return data;
} catch (err) {
getGuardiansError.value = err as Error;
return [];
} finally {
getGuardiansInProgress.value = false;
}
}

const { inProgress: proposeGuardianInProgress, error: proposeGuardianError, execute: proposeGuardian } = useAsync(async (address: Address) => {
const client = getClient({ chainId: defaultChain.id });
const tx = await client.proposeGuardian({
newGuardian: address,
paymaster: {
address: paymasterAddress,
},
});
await getGuardians(client.account.address);
return tx;
});

const { inProgress: removeGuardianInProgress, error: removeGuardianError, execute: removeGuardian } = useAsync(async (address: Address) => {
const client = getClient({ chainId: defaultChain.id });
const tx = await client.removeGuardian({
guardian: address,
paymaster: {
address: paymasterAddress,
},
});
getGuardians(client.account.address);
return tx;
});

return {
proposeGuardianInProgress: proposeGuardianInProgress,
proposeGuardianError: proposeGuardianError,
proposeGuardian: proposeGuardian,
removeGuardianInProgress: removeGuardianInProgress,
removeGuardianError: removeGuardianError,
removeGuardian: removeGuardian,
getGuardedAccountsInProgress,
getGuardedAccountsError,
getGuardedAccounts,
getGuardiansInProgress,
getGuardiansError,
getGuardiansData,
getGuardians,
};
};
30 changes: 21 additions & 9 deletions packages/auth-server/pages/dashboard/settings/index.vue
Original file line number Diff line number Diff line change
@@ -25,6 +25,7 @@
<Card
v-for="method in recoveryMethods"
:key="method.address"
:loading="getGuardiansInProgress && removeGuardianInProgress"
class="p-6"
:class="{ 'border-yellow-400 bg-yellow-50 dark:bg-yellow-950 dark:border-yellow-600': method.pendingUrl }"
>
@@ -77,7 +78,7 @@
<Button
type="danger"
class="text-sm lg:w-auto w-full"
@click="removeRecoveryMethod(method.address)"
@click="removeRecoveryAction(method.address)"
>
Remove
</Button>
@@ -92,6 +93,7 @@
<script setup lang="ts">
import { WalletIcon } from "@heroicons/vue/24/solid";
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
import type { Address } from "viem";

import AddRecoveryMethodModal from "~/components/account-recovery/AddRecoveryMethodModal.vue";
import CopyToClipboard from "~/components/common/CopyToClipboard.vue";
@@ -101,15 +103,25 @@ import { shortenAddress } from "~/utils/formatters";

const breakpoints = useBreakpoints(breakpointsTailwind);
const isMobile = breakpoints.smaller("lg");
const recoveryMethods = ref([] as { method: string; address: string; addedOn: Date; pendingUrl?: string }[]);
const recoveryMethods = ref([] as { method: string; address: Address; addedOn: Date; pendingUrl?: string }[]);
const { getClient, defaultChain } = useClientStore();
const { getGuardiansInProgress, getGuardians, getGuardiansData, removeGuardian, removeGuardianInProgress } = useRecoveryGuardian();
watchEffect(() => {
recoveryMethods.value = (getGuardiansData.value ?? []).map((x) => ({
method: "External Account",
address: x.addr,
addedOn: new Date(),
...(!x.isReady && { pendingUrl: "https://auth-test.zksync.dev/dashboard/0x1234567890" }),
}));
});

recoveryMethods.value = [
{ method: "External Account", address: "0x72D8dd6EE7ce73D545B229127E72c8AA013F4a9e", addedOn: new Date() },
{ method: "External Account", address: "0x72D8dd6EE7ce73D545B229127E72c8AA013F4a9e", addedOn: new Date(), pendingUrl: "https://auth-test.zksync.dev/dashboard/0x1234567890" },
];
const getGuardiansAction = async function () {
await getGuardians(getClient({ chainId: defaultChain.id }).account.address);
};

await getGuardiansAction();

const removeRecoveryMethod = (address: string) => {
// TODO: Implement removal logic
recoveryMethods.value = recoveryMethods.value.filter((m) => m.address !== address);
const removeRecoveryAction = async (address: Address) => {
await removeGuardian(address);
};
</script>
16 changes: 12 additions & 4 deletions packages/sdk/src/client/passkey/decorators/passkey.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { type Chain, type Transport } from "viem";

import {
addGuardian, type AddGuardianArgs, type AddGuardianReturnType,
proposeGuardian, type ProposeGuardianArgs, type ProposeGuardianReturnType,
removeGuardian, type RemoveGuardianArgs, type RemoveGuardianReturnType,
} from "../../recovery/actions/recovery.js";
import {
createSession, type CreateSessionArgs, type CreateSessionReturnType,
@@ -12,7 +13,8 @@ import type { ClientWithZksyncSsoPasskeyData } from "../client.js";
export type ZksyncSsoPasskeyActions = {
createSession: (args: Omit<CreateSessionArgs, "contracts">) => Promise<CreateSessionReturnType>;
revokeSession: (args: Omit<RevokeSessionArgs, "contracts">) => Promise<RevokeSessionReturnType>;
addGuardian: (args: Omit<AddGuardianArgs, "contracts">) => Promise<AddGuardianReturnType>;
proposeGuardian: (args: Omit<ProposeGuardianArgs, "contracts">) => Promise<ProposeGuardianReturnType>;
removeGuardian: (args: Omit<RemoveGuardianArgs, "contracts">) => Promise<RemoveGuardianReturnType>;
};

export function zksyncSsoPasskeyActions<
@@ -32,8 +34,14 @@ export function zksyncSsoPasskeyActions<
contracts: client.contracts,
});
},
addGuardian: async (args: Omit<AddGuardianArgs, "contracts">) => {
return await addGuardian(client, {
proposeGuardian: async (args: Omit<ProposeGuardianArgs, "contracts">) => {
return await proposeGuardian(client, {
...args,
contracts: client.contracts,
});
},
removeGuardian: async (args: Omit<RemoveGuardianArgs, "contracts">) => {
return await removeGuardian(client, {
...args,
contracts: client.contracts,
});
61 changes: 54 additions & 7 deletions packages/sdk/src/client/recovery/actions/recovery.ts
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ import { noThrow } from "../../../utils/helpers.js";
import { encodePasskeyModuleParameters } from "../../../utils/index.js";
import { getPublicKeyBytesFromPasskeySignature } from "../../../utils/passkey.js";

export type AddGuardianArgs = {
export type ProposeGuardianArgs = {
newGuardian: Address;
contracts: {
recovery: Address; // recovery module
@@ -18,17 +18,17 @@ export type AddGuardianArgs = {
};
onTransactionSent?: (hash: Hash) => void;
};
export type AddGuardianReturnType = {
export type ProposeGuardianReturnType = {
transactionReceipt: TransactionReceipt;
};
export const addGuardian = async <
export const proposeGuardian = async <
transport extends Transport,
chain extends Chain,
account extends Account,
>(client: Client<transport, chain, account>, args: Prettify<AddGuardianArgs>): Promise<Prettify<AddGuardianReturnType>> => {
>(client: Client<transport, chain, account>, args: Prettify<ProposeGuardianArgs>): Promise<Prettify<ProposeGuardianReturnType>> => {
const callData = encodeFunctionData({
abi: GuardianRecoveryModuleAbi,
functionName: "addValidationKey",
functionName: "proposeValidationKey",
args: [args.newGuardian],
});

@@ -48,7 +48,54 @@ export const addGuardian = async <
}

const transactionReceipt = await waitForTransactionReceipt(client, { hash: transactionHash });
if (transactionReceipt.status !== "success") throw new Error("createSession transaction reverted");
if (transactionReceipt.status !== "success") throw new Error("proposeGuardian transaction reverted");

return {
transactionReceipt,
};
};
export type RemoveGuardianArgs = {
guardian: Address;
contracts: {
recovery: Address; // recovery module
};
paymaster?: {
address: Address;
paymasterInput?: Hex;
};
onTransactionSent?: (hash: Hash) => void;
};
export type RemoveGuardianReturnType = {
transactionReceipt: TransactionReceipt;
};
export const removeGuardian = async <
transport extends Transport,
chain extends Chain,
account extends Account,
>(client: Client<transport, chain, account>, args: Prettify<RemoveGuardianArgs>): Promise<Prettify<RemoveGuardianReturnType>> => {
const callData = encodeFunctionData({
abi: GuardianRecoveryModuleAbi,
functionName: "removeValidationKey",
args: [args.guardian],
});

const sendTransactionArgs = {
account: client.account,
to: args.contracts.recovery,
paymaster: args.paymaster?.address,
paymasterInput: args.paymaster?.address ? (args.paymaster?.paymasterInput || getGeneralPaymasterInput({ innerInput: "0x" })) : undefined,
data: callData,
gas: 10_000_000n, // TODO: Remove when gas estimation is fixed
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any;

const transactionHash = await sendTransaction(client, sendTransactionArgs);
if (args.onTransactionSent) {
noThrow(() => args.onTransactionSent?.(transactionHash));
}

const transactionReceipt = await waitForTransactionReceipt(client, { hash: transactionHash });
if (transactionReceipt.status !== "success") throw new Error("removeGuardian transaction reverted");

return {
transactionReceipt,
@@ -114,7 +161,7 @@ export const initRecovery = async <
}

const transactionReceipt = await waitForTransactionReceipt(client, { hash: transactionHash });
if (transactionReceipt.status !== "success") throw new Error("createSession transaction reverted");
if (transactionReceipt.status !== "success") throw new Error("initRecovery transaction reverted");

return {
transactionReceipt,