Skip to content

Commit 38d4636

Browse files
MiniRomanaon
andcommitted
feat: execute pending recovery on login (#57)
* feat: execute pending recovery on login * feat: move recovery client to sdk * feat: add account-not-ready view * chore: fix pnpm lock --------- Co-authored-by: aon <21188659+aon@users.noreply.github.com>
1 parent 628e5d0 commit 38d4636

File tree

19 files changed

+934
-68
lines changed

19 files changed

+934
-68
lines changed

packages/auth-server/composables/useAccountLogin.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,25 @@ export const useAccountLogin = (_chainId: MaybeRef<SupportedChainId>) => {
2828
});
2929
return { success: true } as const;
3030
} catch {
31-
const { checkRecoveryRequest } = useRecoveryGuardian();
31+
const { checkRecoveryRequest, executeRecovery } = useRecoveryGuardian();
3232
const recoveryRequest = await checkRecoveryRequest(credential.id);
3333
if (recoveryRequest) {
34+
const isReady = recoveryRequest[1];
35+
if (isReady) {
36+
await executeRecovery(recoveryRequest[0]);
37+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
38+
const { username, address, passkeyPublicKey } = await fetchAccount(client as any, {
39+
contracts: contractsByChain[chainId.value],
40+
uniqueAccountId: credential.id,
41+
});
42+
login({
43+
username,
44+
address,
45+
passkey: toHex(passkeyPublicKey),
46+
});
47+
return { success: true } as const;
48+
}
49+
3450
return {
3551
success: false,
3652
recoveryRequest: {

packages/auth-server/composables/useRecoveryGuardian.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Account, Address, Chain, Client, Transport } from "viem";
2+
import { hexToBytes } from "viem";
23
import { GuardianRecoveryModuleAbi } from "zksync-sso/abi";
34
import { confirmGuardian as sdkConfirmGuardian } from "zksync-sso/client";
45

@@ -7,7 +8,7 @@ const getGuardiansError = ref<Error | null>(null);
78
const getGuardiansData = ref<readonly { addr: Address; isReady: boolean }[] | null>(null);
89

910
export const useRecoveryGuardian = () => {
10-
const { getClient, getPublicClient, getWalletClient, defaultChain } = useClientStore();
11+
const { getClient, getPublicClient, getWalletClient, getRecoveryClient, defaultChain } = useClientStore();
1112
const paymasterAddress = contractsByChain[defaultChain!.id].accountPaymaster;
1213

1314
const getGuardedAccountsInProgress = ref(false);
@@ -156,6 +157,19 @@ export const useRecoveryGuardian = () => {
156157
return tx;
157158
});
158159

160+
const { inProgress: executeRecoveryInProgress, error: executeRecoveryError, execute: executeRecovery } = useAsync(async (address: Address) => {
161+
const recoveryClient = await getRecoveryClient({ chainId: defaultChain.id, address });
162+
const pendingRecovery = await getPendingRecoveryData(address);
163+
164+
const tx = await recoveryClient.addAccountOwnerPasskey({
165+
passkeyPublicKey: hexToBytes(pendingRecovery![0]!),
166+
paymaster: {
167+
address: paymasterAddress,
168+
},
169+
});
170+
return tx;
171+
});
172+
159173
return {
160174
confirmGuardianInProgress,
161175
confirmGuardianError,
@@ -189,5 +203,8 @@ export const useRecoveryGuardian = () => {
189203
checkRecoveryRequestInProgress,
190204
checkRecoveryRequestError,
191205
checkRecoveryRequest,
206+
executeRecoveryInProgress,
207+
executeRecoveryError,
208+
executeRecovery,
192209
};
193210
};

packages/auth-server/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"@vueuse/nuxt": "^11.1.0",
2121
"@wagmi/core": "^2.13.3",
2222
"@wagmi/vue": "^0.0.49",
23+
"date-fns": "^4.1.0",
2324
"nuxt": "^3.12.3",
2425
"nuxt-gtag": "3.0.2",
2526
"nuxt-typed-router": "3.7.3",

packages/auth-server/pages/index.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ const logIn = async () => {
7474
return;
7575
}
7676
if (result?.recoveryRequest?.isReady === false) {
77-
navigateTo("/recovery/account-not-ready");
77+
navigateTo(`/recovery/account-not-ready?address=${result!.recoveryRequest.account}`);
7878
return;
7979
}
8080
// TODO: handle rest of the cases

packages/auth-server/pages/recovery/account-not-ready.vue

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,28 @@
22
<main class="h-full flex flex-col justify-center px-4">
33
<AppAccountLogo class="dark:text-neutral-100 h-16 md:h-20 mb-12" />
44

5-
<div class="space-y-2">
5+
<div
6+
class="space-y-2"
7+
:loading="!recoveryState"
8+
>
69
<h2 class="text-3xl font-bold text-center mb-2 text-gray-900 dark:text-white">
710
Account in Recovery
811
</h2>
912
<p class="text-center text-gray-600 dark:text-gray-400 text-lg">
10-
Your account is not ready yet
13+
{{ recoveryState!.type === 'notYet' ? "Your account is not ready yet" : "Your recovery request already expired"
14+
}}
1115
</p>
1216
</div>
1317

14-
<div class="flex flex-col items-center mt-8">
18+
<div
19+
v-if="recoveryState!.type === 'notYet'"
20+
class="flex flex-col items-center mt-8"
21+
>
1522
<div class="p-6 rounded-lg bg-gray-50 dark:bg-gray-800 max-w-md w-full text-center">
1623
<p class="text-gray-600 dark:text-gray-300 mb-4">
17-
Your account is currently in the recovery process. It will be ready in <span class="font-semibold text-gray-900 dark:text-white">24hs</span>.
24+
Your account is currently in the recovery process. It will be ready in <span
25+
class="font-semibold text-gray-900 dark:text-white"
26+
>{{ recoveryState!.time }}</span>.
1827
</p>
1928
<p class="text-sm text-gray-500 dark:text-gray-400">
2029
Please check back later
@@ -28,11 +37,56 @@
2837
href="/"
2938
class="inline-flex items-center gap-2 justify-center"
3039
>
31-
<ZkIcon
32-
icon="arrow_back"
33-
/>
40+
<ZkIcon icon="arrow_back" />
3441
Back to Home
3542
</ZkLink>
3643
</div>
3744
</main>
3845
</template>
46+
47+
<script setup lang="ts">
48+
import { formatDuration, intervalToDuration } from "date-fns";
49+
import type { Address } from "viem";
50+
import { z } from "zod";
51+
52+
import { AddressSchema } from "@/utils/schemas";
53+
54+
const route = useRoute();
55+
const { checkRecoveryRequest, getPendingRecoveryData } = useRecoveryGuardian();
56+
57+
const accountAddress = ref<Address | null>(null);
58+
59+
const params = z.object({
60+
address: AddressSchema,
61+
}).safeParse(route.query);
62+
63+
const pendingRecovery = ref<Awaited<ReturnType<typeof checkRecoveryRequest>> | null>(null);
64+
const recoveryState = ref<{ type: "notYet"; time: string } | { type: "expired" } | null>(null);
65+
66+
if (!params.success) {
67+
throw createError({
68+
statusCode: 404,
69+
statusMessage: "Page not found",
70+
fatal: true,
71+
});
72+
} else {
73+
accountAddress.value = params.data.address;
74+
const recoveryRequestData = await getPendingRecoveryData(accountAddress.value);
75+
if (recoveryRequestData) {
76+
const recoveryData = await checkRecoveryRequest(recoveryRequestData[2]);
77+
pendingRecovery.value = recoveryData;
78+
if (recoveryData && recoveryData[2] > 0n) {
79+
recoveryState.value = {
80+
type: "notYet", time: formatDuration(intervalToDuration({
81+
start: 0,
82+
end: parseFloat((recoveryData[2]).toString()) * 1000,
83+
})),
84+
};
85+
} else if (recoveryData && recoveryData[2] === 0n) {
86+
recoveryState.value = {
87+
type: "expired",
88+
};
89+
}
90+
}
91+
}
92+
</script>

packages/auth-server/stores/client.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
33
import { zksyncInMemoryNode, zksyncSepoliaTestnet } from "viem/chains";
44
import { eip712WalletActions } from "viem/zksync";
55
import { createZksyncPasskeyClient, type PasskeyRequiredContracts } from "zksync-sso/client/passkey";
6+
import { createZksyncRecoveryGuardianClient } from "zksync-sso/client/recovery";
67

78
import localChainData from "./local-node.json";
89

@@ -72,6 +73,21 @@ export const useClientStore = defineStore("client", () => {
7273
return client;
7374
};
7475

76+
const getRecoveryClient = ({ chainId, address }: { chainId: SupportedChainId; address: Address }) => {
77+
const chain = supportedChains.find((chain) => chain.id === chainId);
78+
if (!chain) throw new Error(`Chain with id ${chainId} is not supported`);
79+
const contracts = contractsByChain[chainId];
80+
81+
const client = createZksyncRecoveryGuardianClient({
82+
address,
83+
contracts,
84+
chain: chain,
85+
transport: http(),
86+
});
87+
88+
return client;
89+
};
90+
7591
const getConfigurableClient = ({
7692
chainId,
7793
address,
@@ -86,7 +102,6 @@ export const useClientStore = defineStore("client", () => {
86102
const chain = supportedChains.find((chain) => chain.id === chainId);
87103
if (!chain) throw new Error(`Chain with id ${chainId} is not supported`);
88104
const contracts = contractsByChain[chainId];
89-
90105
return createZksyncPasskeyClient({
91106
address,
92107
credentialPublicKey,
@@ -140,6 +155,7 @@ export const useClientStore = defineStore("client", () => {
140155
getClient,
141156
getThrowAwayClient,
142157
getWalletClient,
158+
getRecoveryClient,
143159
getConfigurableClient,
144160
};
145161
});

packages/sdk/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@
7979
"import": "./dist/_esm/client/passkey/index.js",
8080
"require": "./dist/_cjs/client/passkey/index.js"
8181
},
82+
"./client/recovery": {
83+
"types": "./dist/_types/client/recovery/index.d.ts",
84+
"import": "./dist/_esm/client/recovery/index.js",
85+
"require": "./dist/_cjs/client/recovery/index.js"
86+
},
8287
"./client/session": {
8388
"types": "./dist/_types/client/session/index.d.ts",
8489
"import": "./dist/_esm/client/session/index.js",

0 commit comments

Comments
 (0)