Skip to content

Commit

Permalink
feat: auto-add key to wallet, support for test wallet (#630)
Browse files Browse the repository at this point in the history
  • Loading branch information
sidvishnoi authored Oct 2, 2024
1 parent 19f59c9 commit 1edee22
Show file tree
Hide file tree
Showing 23 changed files with 987 additions and 109 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ All commands are run from the root of the project, from a terminal:

Inside this project, you'll see the following folders and files:

```
```sh
.
├── .github/ # GitHub Workflows
├── docs/ # Repository documentation
Expand All @@ -85,7 +85,8 @@ Inside this project, you'll see the following folders and files:
│ ├── _locales/ # Files for multi-lang support
│ ├── assets/ # Images for the extension (icon, etc.)
│ ├── background/ # Source code for the background script/service worker
│ ├── content/ # Source code for the content script
│ ├── content/ # Source code for the content scripts
│ │ └── keyAutoAdd/ # content scripts for automatic key addition to wallets
│ ├── popup/ # Source code for the popup UI
│ ├── shared/ # Shared utilities
│ └── manifest.json # Extension's manifest - processed by Webpack depending on the target build
Expand Down
2 changes: 2 additions & 0 deletions cspell-dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ iframes
unmangles
data-testid
nums
jwks
requestfinished

# scripts and 3rd party terms
nvmrc
Expand Down
4 changes: 4 additions & 0 deletions esbuild/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ export const options: BuildOptions = {
in: path.join(SRC_DIR, 'content', 'index.ts'),
out: path.join('content', 'content'),
},
{
in: path.join(SRC_DIR, 'content', 'keyAutoAdd', 'testWallet.ts'),
out: path.join('content', 'keyAutoAdd', 'testWallet'),
},
{
in: path.join(SRC_DIR, 'content', 'polyfill.ts'),
out: path.join('polyfill', 'polyfill'),
Expand Down
2 changes: 1 addition & 1 deletion esbuild/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ function liveReloadPlugin({ target }: { target: Target }): ESBuildPlugin {
new EventSource("http://localhost:${port}/esbuild").addEventListener(
"change",
(ev) => {
const patterns = ["background.js", "content.js", "polyfill.js"];
const patterns = ["background.js", "content.js", "polyfill.js", "keyAutoAdd/"];
const data = JSON.parse(ev.data);
if (data.updated.some((s) => patterns.some(e => s.includes(e)))) {
globalThis.location.reload();
Expand Down
19 changes: 19 additions & 0 deletions src/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,26 @@
"connectWallet_error_tabClosed": {
"message": "Connect wallet cancelled. You closed the tab before completion."
},
"connectWallet_error_grantRejected": {
"message": "Connect wallet cancelled. You rejected the request."
},
"connectWalletKeyService_error_notImplemented": {
"message": "Automatic key addition is not implemented for given wallet provider yet."
},
"connectWalletKeyService_error_failed": {
"message": "Automatic key addition failed at step “$STEP_ID$” with message “$MESSAGE$”.",
"placeholders": {
"STEP_ID": { "content": "$1", "example": "Doing something" },
"MESSAGE": { "content": "$2", "example": "Could not do something" }
}
},
"connectWalletKeyService_error_timeoutLogin": {
"message": "Timed out waiting for login"
},
"connectWalletKeyService_error_skipAlreadyLoggedIn": {
"message": "Already logged in"
},
"connectWalletKeyService_error_accountNotFound": {
"message": "Failed to find account for given wallet address. Are you logged in to some other account?"
}
}
2 changes: 1 addition & 1 deletion src/background/services/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ export class Background {
if (message.payload.recurring) {
this.scheduleResetOutOfFundsState();
}
return;
return success(undefined);

case 'RECONNECT_WALLET': {
await this.openPaymentsService.reconnectWallet();
Expand Down
167 changes: 167 additions & 0 deletions src/background/services/keyAutoAdd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import {
ErrorWithKey,
ensureEnd,
isErrorWithKey,
withResolvers,
} from '@/shared/helpers';
import type { Browser, Runtime, Tabs } from 'webextension-polyfill';
import type { WalletAddress } from '@interledger/open-payments';
import type { TabId } from '@/shared/types';
import type { Cradle } from '@/background/container';
import type {
BeginPayload,
KeyAutoAddToBackgroundMessage,
} from '@/content/keyAutoAdd/lib/types';

export const CONNECTION_NAME = 'key-auto-add';

type OnTabRemovedCallback = Parameters<
Browser['tabs']['onRemoved']['addListener']
>[0];
type OnConnectCallback = Parameters<
Browser['runtime']['onConnect']['addListener']
>[0];
type OnPortMessageListener = Parameters<
Runtime.Port['onMessage']['addListener']
>[0];

export class KeyAutoAddService {
private browser: Cradle['browser'];
private storage: Cradle['storage'];
private browserName: Cradle['browserName'];
private t: Cradle['t'];

private tab: Tabs.Tab | null = null;

constructor({
browser,
storage,
browserName,
t,
}: Pick<Cradle, 'browser' | 'storage' | 'browserName' | 't'>) {
Object.assign(this, { browser, storage, browserName, t });
}

async addPublicKeyToWallet(walletAddress: WalletAddress) {
const info = walletAddressToProvider(walletAddress);
try {
const { publicKey, keyId } = await this.storage.get([
'publicKey',
'keyId',
]);
this.setConnectState('connecting:key');
await this.process(info.url, {
publicKey,
keyId,
walletAddressUrl: walletAddress.id,
nickName: this.t('appName') + ' - ' + this.browserName,
keyAddUrl: info.url,
});
await this.validate(walletAddress.id, keyId);
} catch (error) {
this.setConnectState('error:key');
throw error;
}
}

/**
* Allows re-using same tab for further processing. Available only after
* {@linkcode addPublicKeyToWallet} has been called.
*/
get tabId(): TabId | undefined {
return this.tab?.id;
}

private async process(url: string, payload: BeginPayload) {
const { resolve, reject, promise } = withResolvers();

const tab = await this.browser.tabs.create({ url });
this.tab = tab;
if (!tab.id) {
reject(new Error('Could not create tab'));
return promise;
}

const onTabCloseListener: OnTabRemovedCallback = (tabId) => {
if (tabId !== tab.id) return;
this.browser.tabs.onRemoved.removeListener(onTabCloseListener);
reject(new ErrorWithKey('connectWallet_error_tabClosed'));
};
this.browser.tabs.onRemoved.addListener(onTabCloseListener);

const onConnectListener: OnConnectCallback = (port) => {
if (port.name !== CONNECTION_NAME) return;
if (port.error) {
reject(new Error(port.error.message));
return;
}

port.postMessage({ action: 'BEGIN', payload });

port.onMessage.addListener(onMessageListener);

port.onDisconnect.addListener(() => {
// wait for connect again so we can send message again if not connected,
// and not errored already (e.g. page refreshed)
});
};

const onMessageListener: OnPortMessageListener = (
message: KeyAutoAddToBackgroundMessage,
) => {
if (message.action === 'SUCCESS') {
this.browser.runtime.onConnect.removeListener(onConnectListener);
this.browser.tabs.onRemoved.removeListener(onTabCloseListener);
resolve(message.payload);
} else if (message.action === 'ERROR') {
this.browser.runtime.onConnect.removeListener(onConnectListener);
this.browser.tabs.onRemoved.removeListener(onTabCloseListener);
const { stepName, details: err } = message.payload;
reject(
new ErrorWithKey('connectWalletKeyService_error_failed', [
stepName,
isErrorWithKey(err.error) ? this.t(err.error) : err.message,
]),
);
} else if (message.action === 'PROGRESS') {
// can save progress to show in popup
// console.table(message.payload.steps);
} else {
reject(new Error(`Unexpected message: ${JSON.stringify(message)}`));
}
};

this.browser.runtime.onConnect.addListener(onConnectListener);

return promise;
}

private async validate(walletAddressUrl: string, keyId: string) {
type JWKS = { keys: { kid: string }[] };
const jwksUrl = new URL('jwks.json', ensureEnd(walletAddressUrl, '/'));
const res = await fetch(jwksUrl.toString());
const jwks: JWKS = await res.json();
if (!jwks.keys.find((key) => key.kid === keyId)) {
throw new Error('Key not found in jwks');
}
}

private setConnectState(status: 'connecting:key' | 'error:key' | null) {
const state = status ? { status } : null;
this.storage.setPopupTransientState('connect', () => state);
}
}

export function walletAddressToProvider(walletAddress: WalletAddress): {
url: string;
} {
const { host } = new URL(walletAddress.id);
switch (host) {
case 'ilp.rafiki.money':
return { url: 'https://rafiki.money/settings/developer-keys' };
// case 'eu1.fynbos.me': // fynbos dev
// case 'fynbos.me': // fynbos production
default:
throw new ErrorWithKey('connectWalletKeyService_error_notImplemented');
}
}
Loading

0 comments on commit 1edee22

Please sign in to comment.