Skip to content

Commit

Permalink
feat: automatically add public key for Fynbos wallets
Browse files Browse the repository at this point in the history
  • Loading branch information
sidvishnoi committed Oct 3, 2024
1 parent 1fe9de1 commit 50d6c09
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 9 deletions.
4 changes: 4 additions & 0 deletions esbuild/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export const options: BuildOptions = {
in: path.join(SRC_DIR, 'content', 'keyAutoAdd', 'testWallet.ts'),
out: path.join('content', 'keyAutoAdd', 'testWallet'),
},
{
in: path.join(SRC_DIR, 'content', 'keyAutoAdd', 'fynbos.ts'),
out: path.join('content', 'keyAutoAdd', 'fynbos'),
},
{
in: path.join(SRC_DIR, 'content', 'polyfill.ts'),
out: path.join('polyfill', 'polyfill'),
Expand Down
6 changes: 4 additions & 2 deletions src/background/services/keyAutoAdd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,10 @@ export function walletAddressToProvider(walletAddress: WalletAddress): {
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
case 'eu1.fynbos.me':
return { url: 'https://eu1.fynbos.dev/settings/keys' };
case 'fynbos.me':
return { url: 'https://wallet.fynbos.app/settings/keys' };
default:
throw new ErrorWithKey('connectWalletKeyService_error_notImplemented');
}
Expand Down
129 changes: 129 additions & 0 deletions src/content/keyAutoAdd/fynbos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// cSpell:ignore nextjs
import { errorWithKey, ErrorWithKey, sleep } from '@/shared/helpers';
import {
KeyAutoAdd,
LOGIN_WAIT_TIMEOUT,
type StepRun as Run,
} from './lib/keyAutoAdd';
import { isTimedOut, waitForElement, waitForURL } from './lib/helpers';
// #region: Steps

type IndexRouteResponse = {
isUser: boolean;
walletInfo: {
walletID: string;
url: string;
};
};

const waitForLogin: Run<void> = async (
{ keyAddUrl },
{ skip, setNotificationSize },
) => {
let alreadyLoggedIn = window.location.href.startsWith(keyAddUrl);
if (!alreadyLoggedIn) setNotificationSize('notification');
try {
sleep(2000);
alreadyLoggedIn = await waitForURL(
(url) => (url.origin + url.pathname).startsWith(keyAddUrl),
{ timeout: LOGIN_WAIT_TIMEOUT },
);
setNotificationSize('fullscreen');
} catch (error) {
if (isTimedOut(error)) {
throw new ErrorWithKey('connectWalletKeyService_error_timeoutLogin');
}
throw new Error(error);
}

if (alreadyLoggedIn) {
skip(errorWithKey('connectWalletKeyService_error_skipAlreadyLoggedIn'));
}
};

const findWallet: Run<{ walletId: string }> = async (
{ walletAddressUrl },
{ setNotificationSize },
) => {
setNotificationSize('fullscreen');
const url = `/?_data=${encodeURIComponent('routes/_index')}`;
const res = await fetch(url, {
method: 'GET',
headers: {
accept: 'application/json',
'content-type': 'application/json',
},
mode: 'cors',
credentials: 'include',
});
const data: IndexRouteResponse = await res.json();
if (!data?.walletInfo?.url) {
throw new ErrorWithKey('connectWalletKeyService_error_accountNotFound');
}

if (data.walletInfo.url === walletAddressUrl) {
return { walletId: data.walletInfo.walletID };
}

throw new ErrorWithKey('connectWalletKeyService_error_accountNotFound');
};

const findForm: Run<{
form: HTMLFormElement;
nickNameField: HTMLInputElement;
publicKeyField: HTMLTextAreaElement;
}> = async () => {
const pathname = '/settings/keys/add-public';
const link = await waitForElement<HTMLAnchorElement>(`a[href="${pathname}"]`);
link.click();
await waitForURL((url) => url.pathname === pathname);

const form = await waitForElement<HTMLFormElement>('form#add-public-key');
const nickNameField = await waitForElement<HTMLInputElement>(
'input#applicationName',
{ root: form },
);
const publicKeyField = await waitForElement<HTMLTextAreaElement>(
'textarea#publicKey',
{ root: form },
);
return { form, nickNameField, publicKeyField };
};

const addKey: Run<void> = async ({ publicKey, nickName }, { output }) => {
const { form, nickNameField, publicKeyField } = output(findForm);

nickNameField.focus();
nickNameField.value = nickName;
nickNameField.blur();

publicKeyField.focus();
publicKeyField.value = publicKey;
publicKeyField.blur();

const submitButton = await waitForElement<HTMLButtonElement>(
'button[type="submit"]',
{ root: form },
);
submitButton.click();

await waitForURL((url) => url.pathname === '/settings/keys');
};
// #endregion

// #region: Helpers
// anything?
// #endregion

// #region: Main
new KeyAutoAdd([
{
name: 'Waiting for you to login',
run: waitForLogin,
maxDuration: LOGIN_WAIT_TIMEOUT,
},
{ name: 'Finding wallet', run: findWallet },
{ name: 'Finding form to add public key', run: findForm },
{ name: 'Adding key', run: addKey },
]).init();
// #endregion
62 changes: 55 additions & 7 deletions src/content/keyAutoAdd/lib/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,63 @@

import { withResolvers } from '@/shared/helpers';

class TimeoutError extends Error {
name = 'TimeoutError';
constructor(message: string, { cause }: { cause: Event }) {
super(message, { cause });
}
}

interface WaitForOptions {
timeout: number;
}

interface WaitForElementOptions extends WaitForOptions {
root: HTMLElement | HTMLHtmlElement | Document;
/**
* Once a selector is matched, you can request an additional check to ensure
* this is the element you're looking for.
*/
match: (el: HTMLElement) => boolean;
}

export function waitForElement<T extends HTMLElement = HTMLElement>(
selector: string,
{
root = document,
timeout = 10 * 1000,
match = () => true,
}: Partial<WaitForElementOptions> = {},
): Promise<T> {
const { resolve, reject, promise } = withResolvers<T>();
if (document.querySelector(selector)) {
resolve(document.querySelector<T>(selector)!);
return promise;
}

const abortSignal = AbortSignal.timeout(timeout);
abortSignal.addEventListener('abort', (e) => {
observer.disconnect();
reject(
new TimeoutError(`Timeout waiting for element: {${selector}}`, {
cause: e,
}),
);
});

const observer = new MutationObserver(() => {
const el = document.querySelector<T>(selector);
if (el && match(el)) {
observer.disconnect();
resolve(el);
}
});

observer.observe(root, { childList: true, subtree: true });

return promise;
}

interface WaitForURLOptions extends WaitForOptions {}

export async function waitForURL(
Expand All @@ -23,7 +76,7 @@ export async function waitForURL(
const abortSignal = AbortSignal.timeout(timeout);
abortSignal.addEventListener('abort', (e) => {
observer.disconnect();
reject(e);
reject(new TimeoutError(`Timeout waiting for URL`, { cause: e }));
});

let url = window.location.href;
Expand All @@ -44,10 +97,5 @@ export async function waitForURL(
}

export function isTimedOut(e: any) {
return (
e instanceof Event &&
e.type === 'abort' &&
e.currentTarget instanceof AbortSignal &&
e.currentTarget.reason?.name === 'TimeoutError'
);
return e instanceof TimeoutError;
}
5 changes: 5 additions & 0 deletions src/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
"matches": ["https://rafiki.money/*/*"],
"js": ["content/keyAutoAdd/testWallet.js"],
"run_at": "document_end"
},
{
"matches": ["https://eu1.fynbos.dev/*", "https://wallet.fynbos.app/*"],
"js": ["content/keyAutoAdd/fynbos.js"],
"run_at": "document_end"
}
],
"background": {
Expand Down

0 comments on commit 50d6c09

Please sign in to comment.