Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: auto-add key to wallet, support for test wallet #630

Merged
merged 33 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
b5af5d5
feat: auto-add key to wallet (start with test wallet)
sidvishnoi Sep 26, 2024
1d6f867
add basic keyAutoAdd service, runner
sidvishnoi Sep 26, 2024
285f320
wip
sidvishnoi Sep 26, 2024
2861b38
reuse existingTab after adding key for continuation
sidvishnoi Sep 26, 2024
7af427d
Merge branch 'main' into setup-page/key-share
sidvishnoi Sep 26, 2024
5ced3fb
implement happy path
sidvishnoi Sep 26, 2024
1b01e51
support previous step results; for rafiki.money - reverse-engineer API
sidvishnoi Sep 26, 2024
0306920
Merge branch 'main' into setup-page/key-share
sidvishnoi Sep 27, 2024
0f5c98c
nits
sidvishnoi Sep 27, 2024
be5f879
pass nickname from background; cleanup; prepare for helpers
sidvishnoi Sep 27, 2024
fe5c8bb
wait for login if not logged in
sidvishnoi Sep 27, 2024
bdeb60e
update state keys
sidvishnoi Sep 27, 2024
3b80e26
handle key already added but to different account
sidvishnoi Sep 27, 2024
f723367
style nits
sidvishnoi Sep 27, 2024
01a7b63
post PROGRESS right before success and failure too
sidvishnoi Sep 27, 2024
17a7112
style: code reorganize in testWallet.ts
sidvishnoi Sep 27, 2024
5bb8d09
support skipping step; simplify step params; include details with status
sidvishnoi Sep 27, 2024
1ffb4c5
lint fix
sidvishnoi Sep 27, 2024
3a10510
error message improvements
sidvishnoi Sep 30, 2024
ba356c7
cleanup, reorganize, rename
sidvishnoi Sep 30, 2024
777768a
README: add keyAutoAdd to project structure
sidvishnoi Sep 30, 2024
094b04b
use extension name + browser name as key nickname
sidvishnoi Sep 30, 2024
97c8c39
don't try reconnecting if tab closed
sidvishnoi Sep 30, 2024
64e61ea
handle grant reject (otherwise transient state was stuck in connecting)
sidvishnoi Sep 30, 2024
d09ef81
nit
sidvishnoi Sep 30, 2024
fe14a04
add E2E tests
sidvishnoi Sep 30, 2024
cc94e13
remove test.only
sidvishnoi Sep 30, 2024
52f4c61
remove 'public key not added' test as redundant by this PR
sidvishnoi Sep 30, 2024
f002f87
Merge branch 'main' into setup-page/key-share
sidvishnoi Oct 1, 2024
e05cfe7
merge findBuildId into getAccountDetails
sidvishnoi Oct 2, 2024
4095290
remove unnecessary `x-nextjs-data` header
sidvishnoi Oct 2, 2024
1737ff2
remove `waitForElement` as unused (for now)
sidvishnoi Oct 2, 2024
0ce1414
bring in some error handling/progress goodies from other PR
sidvishnoi Oct 2, 2024
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
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
160 changes: 160 additions & 0 deletions src/background/services/keyAutoAdd.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { ErrorWithKey, ensureEnd, 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,
});
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);
reject(
new ErrorWithKey('connectWalletKeyService_error_failed', [
message.payload.stepName,
message.payload.error.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
Loading