Skip to content

Commit

Permalink
feat: auto-add key to wallet (start with test wallet)
Browse files Browse the repository at this point in the history
  • Loading branch information
sidvishnoi committed Sep 26, 2024
1 parent dd58393 commit b5af5d5
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 3 deletions.
141 changes: 141 additions & 0 deletions src/background/services/keyShare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import type { Browser, Runtime, Tabs } from 'webextension-polyfill';
import type { WalletAddress } from '@interledger/open-payments';
import type { Cradle } from '@/background/container';
import { ErrorWithKey, withResolvers } from '@/shared/helpers';

export const CONNECTION_NAME = 'key-share';

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

type BeginPayload = { walletAddressUrl: string; publicKey: string };

export class KeyShareService {
private browser: Cradle['browser'];
private storage: Cradle['storage'];

private status: null | 'SUCCESS' | 'ERROR' = null;
private tab: Tabs.Tab | null = null;

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

async addPublicKeyToWallet(walletAddress: WalletAddress) {
const { publicKey } = await this.storage.get(['publicKey']);
if (!publicKey) {
// won't happen, just added for lint fix
throw new Error('No public key found');
}
const info = walletAddressToProvider(walletAddress);
try {
this.setConnectState('adding-key');
await this.process(info.url, {
publicKey,
walletAddressUrl: walletAddress.id,
});
} catch (error) {
if (this.tab?.id) {
// can redirect to OPEN_PAYMENTS_REDIRECT_URL
await this.browser.tabs.remove(this.tab.id);
}
this.setConnectState('error-key');
throw error;
}
}

private async process(
url: string,
{ walletAddressUrl, publicKey }: 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) {
// ignore. not our tab
return;
}

if (this.status === 'SUCCESS') {
// ok
} else {
reject(new Error('Tab closed before completion'));
}
};
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: { walletAddressUrl, publicKey },
});

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: {
action: string;
payload: any;
}) => {
if (message.action === 'SUCCESS') {
resolve(message.payload);
} else if (message.action === 'ERROR') {
reject(message.payload);
} else if (message.action === 'PROGRESS') {
// can save progress to show in popup
} else {
reject(new Error(`Unexpected message: ${JSON.stringify(message)}`));
}
};

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

return promise;
}

private setConnectState(status: 'adding-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');
}
}
19 changes: 17 additions & 2 deletions src/background/services/openPayments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { signMessage } from 'http-message-signatures/lib/httpbis';
import { createContentDigestHeader } from 'httpbis-digest-headers';
import type { Browser, Tabs } from 'webextension-polyfill';
import { getExchangeRates, getRateOfPay, toAmount } from '../utils';
import { KeyShareService } from './keyShare';
import { exportJWK, generateEd25519KeyPair } from '@/shared/crypto';
import { bytesToHex } from '@noble/hashes/utils';
import { ErrorWithKey, getWalletInformation } from '@/shared/helpers';
Expand Down Expand Up @@ -363,6 +364,7 @@ export class OpenPaymentsService {
// add key to wallet and try again
try {
await this.addPublicKeyToWallet(walletAddress);
this.setConnectState('connecting');
await this.completeGrant(
amount,
walletAddress,
Expand Down Expand Up @@ -514,8 +516,21 @@ export class OpenPaymentsService {
return grantDetails;
}

private async addPublicKeyToWallet(_walletAddress: WalletAddress) {
throw new ErrorWithKey('connectWalletKeyService_error_notImplemented');
private async addPublicKeyToWallet(walletAddress: WalletAddress) {
const keyShare = new KeyShareService({
browser: this.browser,
storage: this.storage,
});
try {
await keyShare.addPublicKeyToWallet(walletAddress);
} catch (error) {
if (error instanceof ErrorWithKey) {
throw error;
} else {
// TODO: check if need to handle errors here
throw new Error(error.message, { cause: error });
}
}
}

private setConnectState(status: 'connecting' | 'error' | null) {
Expand Down
14 changes: 14 additions & 0 deletions src/shared/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,20 @@ export const removeQueryParams = (urlString: string) => {
return url.origin + url.pathname;
};

/**
* Polyfill for `Promise.withResolvers()`
*/
export function withResolvers<T>() {
let resolve: (value: T | PromiseLike<T>) => void;
let reject: (reason?: any) => void;
const promise = new Promise<T>((res, rej) => {
resolve = res;
reject = rej;
});
// @ts-expect-error we know TypeScript!
return { resolve, reject, promise };
}

export const isOkState = (state: Storage['state']) => {
return Object.values(state).every((value) => value === false);
};
Expand Down
2 changes: 1 addition & 1 deletion src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export type PopupTabInfo = {

export type PopupTransientState = Partial<{
connect: Partial<{
status: 'connecting' | 'error' | null;
status: 'connecting' | 'error' | 'adding-key' | 'error-key' | null;
}> | null;
}>;

Expand Down

0 comments on commit b5af5d5

Please sign in to comment.