Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 23 additions & 0 deletions shared/src/wallet/core/internal-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { type Fr } from "@aztec/aztec.js/fields";
import type { AccountType } from "../database/wallet-db";
import {
WalletInteraction,
WalletUpdateEvent,
type WalletInteractionType,
} from "../types/wallet-interaction";

Expand Down Expand Up @@ -356,34 +357,56 @@ export class InternalWallet extends BaseNativeWallet {
requestedCapabilities?: GrantedCapability[],
): Promise<void> {
await this.db.storeCapabilityGrants(appId, granted, requestedCapabilities);
this.emitCapabilityChange();
}

async revokeCapability(
appId: string,
capability: GrantedCapability,
): Promise<void> {
await this.db.revokeCapability(appId, capability);
this.emitCapabilityChange();
}

async updateAccountAuthorization(
appId: string,
accounts: Aliased<AztecAddress>[],
): Promise<void> {
await this.db.updateAccountAuthorization(appId, accounts);
this.emitCapabilityChange();
}

async updateAddressBookAuthorization(
appId: string,
contacts: Aliased<AztecAddress>[],
): Promise<void> {
await this.db.updateAddressBookAuthorization(appId, contacts);
this.emitCapabilityChange();
}

async revokeAuthorization(key: string): Promise<void> {
await this.db.revokeAuthorization(key);
this.emitCapabilityChange();
}

async revokeAppAuthorizations(appId: string): Promise<void> {
await this.db.revokeAppAuthorizations(appId);
this.emitCapabilityChange();
}

/**
* Emit a wallet-update event so cookie sync picks up capability changes
* made through the Apps tab UI (storeCapabilityGrants, revoke, etc.).
*/
private emitCapabilityChange(): void {
const interaction = WalletInteraction.from({
type: "capabilityChange" as any,
status: "SUCCESS",
complete: true,
title: "Capability change",
});
this.interactionManager.dispatchEvent(
new WalletUpdateEvent(interaction),
);
}
}
43 changes: 28 additions & 15 deletions shared/src/wallet/database/wallet-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1160,33 +1160,46 @@ export class WalletDB {

/**
* Import authorization entries from a portable structure into the DB.
* Additive: merges with existing entries (new entries overwrite conflicting keys).
* Used to bootstrap capability grants from cookies.
* Full overwrite: replaces local entries with cookie data for each app present in the import.
* Keys present in the cookie but missing locally are created.
* Keys present locally but missing in the cookie are deleted (the cookie is authoritative).
* Used to bootstrap capability grants from cookies at PXE init.
*/
async importAllAuthorizations(
apps: Array<{ appId: string; entries: Record<string, unknown> }>,
): Promise<number> {
let imported = 0;
let updated = 0;

for (const { appId, entries } of apps) {
const prefix = `${appId}:`;

// Collect existing keys for this app so we can delete stale ones
const existingKeys = new Set<string>();
for await (const key of this.authorizations.keysAsync({ start: prefix, end: `${prefix}\uffff` })) {
existingKeys.add(key);
}

// Write all entries from the cookie
for (const [storageKey, value] of Object.entries(entries)) {
const fullKey = `${appId}:${storageKey}`;
// Only import keys that don't already exist locally.
// This prevents stale cookie data from overwriting local revocations.
const existing = await this.authorizations.getAsync(fullKey);
if (!existing) {
await this.authorizations.set(
fullKey,
Buffer.from(jsonStringify(value)),
);
imported++;
}
await this.authorizations.set(
fullKey,
Buffer.from(jsonStringify(value)),
);
existingKeys.delete(fullKey); // Mark as seen
updated++;
}

// Delete local keys that are no longer in the cookie (revoked on the other side)
for (const staleKey of existingKeys) {
await this.authorizations.delete(staleKey);
updated++;
}
}

this.logger.info(
`Imported ${imported} new authorization entries for ${apps.length} app(s)`,
`Imported authorization data for ${apps.length} app(s) (${updated} entries processed)`,
);
return imported;
return updated;
}
}
3 changes: 3 additions & 0 deletions web/src/wallet/wallet-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,9 @@ async function bootstrapContactsFromCookie(
// transaction fully commits (same issue as account bootstrap).
await new Promise(resolve => setTimeout(resolve, 0));
await db.storeSender(address, contact.alias);
// Yield again before registerSender — it internally calls getAccounts()
// which iterates PXE's keystore, colliding with the storeSender tx above.
await new Promise(resolve => setTimeout(resolve, 0));
await pxe.registerSender(address);
imported++;
}
Expand Down