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
10 changes: 9 additions & 1 deletion docs/browser-bridge.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@ Unsupported methods return an EIP-1193-style error object with code `4200`.
The injected provider also exposes `isConnected()`, `on`, and `removeListener` with a minimal event
surface for `accountsChanged`, `chainChanged`, `connect`, and `disconnect`.

## Provider discovery

The injected provider supports EIP-6963 multi-wallet discovery in addition to the legacy
`window.ethereum` path:

- announces `eip6963:announceProvider` on page load
- re-announces when the dapp dispatches `eip6963:requestProvider`
- uses `name: "Deckard"`, `rdns: "com.deckard.wallet"`, and a self-contained SVG data URI icon

`personal_sign`, `eth_sendTransaction`, broad signing, hardware wallets, native messaging, and store
distribution are intentionally not implemented in this bridge slice. Kohaku remains useful for
wallet-internal provider/backend integration, not as the browser-facing dapp provider.
Expand Down Expand Up @@ -195,7 +204,6 @@ Open <http://127.0.0.1:8777/> in the browser where the unpacked extension is loa

- Decide native messaging vs hardened localhost using the PRD-04 spike evidence.
- Add a real approval UI for `eth_requestAccounts` and per-origin revocation.
- Add EIP-6963 provider announcement.
- Persist permissions safely.
- Consider a small integration test for the loopback `/rpc` HTTP boundary.
- Add clear-signing/message-signing only after Deckard has the reviewed intent model for it.
27 changes: 27 additions & 0 deletions extension/injected.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@
let nextId = 1;
const pending = new Map();
const listeners = new Map();
const deckardIconSvg = [
'<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 96 96">',
'<rect width="96" height="96" rx="18" fill="#090b0b"/>',
'<path d="M28 22h20c16 0 28 11 28 26S64 74 48 74H28V22Zm14 13v26h6c8 0 14-5 14-13s-6-13-14-13h-6Z" fill="#f4d58d"/>',
'<circle cx="70" cy="26" r="7" fill="#4dd8d8"/>',
'</svg>',
].join('');
const deckardProviderInfo = Object.freeze({
uuid: '3f2e4f7c-5e49-4d7d-8e2c-0d9a7c4f1193',
name: 'Deckard',
icon: `data:image/svg+xml,${encodeURIComponent(deckardIconSvg)}`,
rdns: 'com.deckard.wallet',
});

class DeckardProvider {
constructor() {
Expand Down Expand Up @@ -81,6 +94,18 @@
}

const provider = new DeckardProvider();
const providerDetail = Object.freeze({
info: deckardProviderInfo,
provider,
});

function announceProvider() {
window.dispatchEvent(
new CustomEvent('eip6963:announceProvider', {
detail: providerDetail,
}),
);
}

window.addEventListener('message', (event) => {
if (event.source !== window) return;
Expand Down Expand Up @@ -136,5 +161,7 @@
configurable: true,
});

window.addEventListener('eip6963:requestProvider', announceProvider);
announceProvider();
window.dispatchEvent(new Event('ethereum#initialized'));
})();
55 changes: 55 additions & 0 deletions tests/extension/browser-bridge-extension.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,61 @@ test('local dapp can connect through the injected provider', async ({ page }, te
});
});

test('local dapp can discover Deckard through EIP-6963', async ({ page }) => {
await page.goto('/');
await expect(page.locator('#output')).toContainText('window.ethereum detected');

const discoveryState = await page.evaluate(async () => {
type Eip6963ProviderDetail = {
info: {
uuid: string;
name: string;
icon: string;
rdns: string;
};
provider: NonNullable<Window['ethereum']>;
};

const announcedProviders: Eip6963ProviderDetail[] = [];
window.addEventListener('eip6963:announceProvider', (event) => {
announcedProviders.push((event as CustomEvent<Eip6963ProviderDetail>).detail);
});
window.dispatchEvent(new Event('eip6963:requestProvider'));
await new Promise((resolve) => setTimeout(resolve, 0));

const detail = announcedProviders.find((providerDetail) => (
providerDetail.info.rdns === 'com.deckard.wallet'
));
if (!detail) {
throw new Error('Deckard EIP-6963 provider announcement missing');
}

const chainId = await detail.provider.request({ method: 'eth_chainId' });
return {
announcementCount: announcedProviders.length,
detailFrozen: Object.isFrozen(detail),
infoFrozen: Object.isFrozen(detail.info),
sameProvider: detail.provider === window.ethereum,
chainId,
info: detail.info,
};
});

expect(discoveryState).toEqual({
announcementCount: 1,
detailFrozen: true,
infoFrozen: true,
sameProvider: true,
chainId: '0xaa36a7',
info: {
uuid: '3f2e4f7c-5e49-4d7d-8e2c-0d9a7c4f1193',
name: 'Deckard',
icon: expect.stringMatching(/^data:image\/svg\+xml,/),
rdns: 'com.deckard.wallet',
},
});
});

declare global {
interface Window {
ethereum?: {
Expand Down
Loading