Skip to content

Commit d1696df

Browse files
committed
Preserve wallet address on vault credential rotation
Refactored vault credential rotation logic to avoid creating a new server wallet and to preserve the existing project wallet address during rotation. This prevents loss of wallet association and ensures credentials are saved immediately after rotation, reducing risk of unrecoverable state. Also updated related helper logic to support this flow.
1 parent 0382d42 commit d1696df

File tree

4 files changed

+301
-77
lines changed

4 files changed

+301
-77
lines changed

apps/dashboard/src/@/hooks/useApi.ts

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -330,20 +330,16 @@ export async function rotateSecretKeyClient(params: { project: Project }) {
330330
throw new Error(res.error);
331331
}
332332

333-
try {
334-
// if the project has an encrypted vault admin key, rotate it as well
335-
const service = params.project.services.find(
336-
(service) => service.name === "engineCloud",
337-
);
338-
if (service?.encryptedAdminKey) {
339-
await rotateVaultAccountAndAccessToken({
340-
project: params.project,
341-
projectSecretKey: res.data.data.secret,
342-
projectSecretHash: res.data.data.secretHash,
343-
});
344-
}
345-
} catch (error) {
346-
console.error("Failed to rotate vault admin key", error);
333+
// if the project has an encrypted vault admin key, rotate it as well
334+
const service = params.project.services.find(
335+
(service) => service.name === "engineCloud",
336+
);
337+
if (service?.encryptedAdminKey) {
338+
await rotateVaultAccountAndAccessToken({
339+
project: params.project,
340+
projectSecretKey: res.data.data.secret,
341+
projectSecretHash: res.data.data.secretHash,
342+
});
347343
}
348344

349345
return res.data;

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/transactions/lib/vault.client.ts

Lines changed: 61 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ export async function rotateVaultAccountAndAccessToken(props: {
6969
vaultClient,
7070
adminKey,
7171
rotationCode,
72+
// Skip wallet creation on rotation - preserve the existing project wallet
73+
skipWalletCreation: true,
74+
existingProjectWalletAddress: service?.projectWalletAddress ?? undefined,
7275
});
7376

7477
return {
@@ -222,9 +225,18 @@ async function createAndEncryptVaultAccessTokens(props: {
222225
projectSecretHash?: string;
223226
adminKey: string;
224227
rotationCode: string;
228+
skipWalletCreation?: boolean;
229+
existingProjectWalletAddress?: string;
225230
}) {
226-
const { project, projectSecretKey, vaultClient, adminKey, rotationCode } =
227-
props;
231+
const {
232+
project,
233+
projectSecretKey,
234+
vaultClient,
235+
adminKey,
236+
rotationCode,
237+
skipWalletCreation,
238+
existingProjectWalletAddress,
239+
} = props;
228240

229241
const [managementTokenResult, walletTokenResult] = await Promise.all([
230242
createManagementAccessToken({ project, adminKey, vaultClient }),
@@ -246,12 +258,12 @@ async function createAndEncryptVaultAccessTokens(props: {
246258
const managementToken = managementTokenResult.data;
247259
const walletToken = walletTokenResult.data;
248260

249-
// create a default project server wallet
250-
const defaultProjectServerWallet = await createProjectServerWallet({
251-
project,
252-
managementAccessToken: managementToken.accessToken,
253-
label: getProjectWalletLabel(project.name),
254-
});
261+
// CRITICAL: Save credentials IMMEDIATELY after creating tokens.
262+
// This prevents a broken state if wallet creation or other operations fail.
263+
// The rotationCode is consumed when rotating, so if we don't save the new one,
264+
// the project becomes unrecoverable.
265+
let encryptedAdminKey: string | null = null;
266+
let encryptedWalletAccessToken: string | null = null;
255267

256268
if (projectSecretKey) {
257269
// verify that the project secret key is valid
@@ -266,61 +278,53 @@ async function createAndEncryptVaultAccessTokens(props: {
266278
}
267279

268280
// encrypt admin key and wallet token with project secret key
269-
const [encryptedAdminKey, encryptedWalletAccessToken] = await Promise.all([
281+
[encryptedAdminKey, encryptedWalletAccessToken] = await Promise.all([
270282
encrypt(adminKey, projectSecretKey),
271283
encrypt(walletToken.accessToken, projectSecretKey),
272284
]);
285+
}
273286

274-
await updateProjectClient(
275-
{
276-
projectId: props.project.id,
277-
teamId: props.project.teamId,
278-
},
279-
{
280-
services: [
281-
...props.project.services.filter(
282-
(service) => service.name !== "engineCloud",
283-
),
284-
{
285-
name: "engineCloud",
286-
actions: [],
287-
managementAccessToken: managementToken.accessToken,
288-
maskedAdminKey: maskSecret(adminKey),
289-
encryptedAdminKey,
290-
encryptedWalletAccessToken,
291-
rotationCode: rotationCode,
292-
projectWalletAddress: defaultProjectServerWallet.address,
293-
},
294-
],
295-
},
296-
);
297-
} else {
298-
// no secret key, only store the management token, remove any encrypted keys
299-
await updateProjectClient(
300-
{
301-
projectId: props.project.id,
302-
teamId: props.project.teamId,
303-
},
304-
{
305-
services: [
306-
...props.project.services.filter(
307-
(service) => service.name !== "engineCloud",
308-
),
309-
{
310-
name: "engineCloud",
311-
actions: [],
312-
managementAccessToken: managementToken.accessToken,
313-
maskedAdminKey: maskSecret(adminKey),
314-
encryptedAdminKey: null,
315-
encryptedWalletAccessToken: null,
316-
rotationCode: rotationCode,
317-
projectWalletAddress: defaultProjectServerWallet.address,
318-
},
319-
],
320-
},
321-
);
287+
// For rotation, preserve existing wallet address. For new creation, create a default wallet.
288+
let projectWalletAddress: string | null | undefined =
289+
existingProjectWalletAddress ??
290+
project.services.find((s) => s.name === "engineCloud")
291+
?.projectWalletAddress;
292+
293+
// Only create a new wallet if we don't have one (initial setup, not rotation)
294+
if (!skipWalletCreation && !projectWalletAddress) {
295+
const defaultProjectServerWallet = await createProjectServerWallet({
296+
project,
297+
managementAccessToken: managementToken.accessToken,
298+
label: getProjectWalletLabel(project.name),
299+
});
300+
projectWalletAddress = defaultProjectServerWallet.address;
322301
}
323302

303+
// Save credentials
304+
await updateProjectClient(
305+
{
306+
projectId: props.project.id,
307+
teamId: props.project.teamId,
308+
},
309+
{
310+
services: [
311+
...props.project.services.filter(
312+
(service) => service.name !== "engineCloud",
313+
),
314+
{
315+
name: "engineCloud",
316+
actions: [],
317+
managementAccessToken: managementToken.accessToken,
318+
maskedAdminKey: maskSecret(adminKey),
319+
encryptedAdminKey,
320+
encryptedWalletAccessToken,
321+
rotationCode: rotationCode,
322+
projectWalletAddress: projectWalletAddress ?? null,
323+
},
324+
],
325+
},
326+
);
327+
324328
return {
325329
managementToken,
326330
walletToken,

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/wallets/server-wallets/wallets/page.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ServerWalletsTable } from "../../../transactions/components/server-wall
99
import type { Wallet } from "../../../transactions/server-wallets/wallet-table/types";
1010
import { listSolanaAccounts } from "../../../transactions/solana-wallets/lib/vault.client";
1111
import type { SolanaWallet } from "../../../transactions/solana-wallets/wallet-table/types";
12+
import { VaultRecoveryCard } from "./vault-recovery-card.client";
1213

1314
export const dynamic = "force-dynamic";
1415

@@ -107,12 +108,10 @@ export default async function Page(props: {
107108
return (
108109
<div className="flex flex-col gap-10">
109110
{eoas.error ? (
110-
<div className="rounded-xl border border-destructive/50 bg-destructive/10 p-4">
111-
<p className="text-destructive font-semibold mb-2">
112-
EVM Wallet Error
113-
</p>
114-
<p className="text-sm text-muted-foreground">{eoas.error.message}</p>
115-
</div>
111+
<VaultRecoveryCard
112+
errorMessage={eoas.error.message}
113+
project={project}
114+
/>
116115
) : (
117116
<ServerWalletsTable
118117
client={client}

0 commit comments

Comments
 (0)