Skip to content

Commit

Permalink
feat(keyAutoAdd): support key auto-add for Chimoney (#749)
Browse files Browse the repository at this point in the history
  • Loading branch information
sidvishnoi authored Dec 5, 2024
1 parent 1553e3c commit 0c4a818
Show file tree
Hide file tree
Showing 12 changed files with 504 additions and 12 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/nightly-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ jobs:
FYNBOS_WALLET_ADDRESS_URL: ${{ vars.E2E_FYNBOS_WALLET_ADDRESS_URL }}
FYNBOS_USERNAME: ${{ vars.E2E_FYNBOS_USERNAME }}
FYNBOS_PASSWORD: ${{ secrets.E2E_FYNBOS_PASSWORD }}
CHIMONEY_WALLET_ORIGIN: ${{ vars.E2E_CHIMONEY_WALLET_URL_ORIGIN }}
CHIMONEY_WALLET_ADDRESS_URL: ${{ vars.E2E_CHIMONEY_WALLET_ADDRESS_URL }}
CHIMONEY_USERNAME: ${{ vars.E2E_CHIMONEY_USERNAME }}
CHIMONEY_PASSWORD: ${{ secrets.E2E_CHIMONEY_PASSWORD }}

- name: Encrypt report
shell: bash
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/tests-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ jobs:
FYNBOS_WALLET_ADDRESS_URL: ${{ vars.E2E_FYNBOS_WALLET_ADDRESS_URL }}
FYNBOS_USERNAME: ${{ vars.E2E_FYNBOS_USERNAME }}
FYNBOS_PASSWORD: ${{ secrets.E2E_FYNBOS_PASSWORD }}
CHIMONEY_WALLET_ORIGIN: ${{ vars.E2E_CHIMONEY_WALLET_URL_ORIGIN }}
CHIMONEY_WALLET_ADDRESS_URL: ${{ vars.E2E_CHIMONEY_WALLET_ADDRESS_URL }}
CHIMONEY_USERNAME: ${{ vars.E2E_CHIMONEY_USERNAME }}
CHIMONEY_PASSWORD: ${{ secrets.E2E_CHIMONEY_PASSWORD }}

- name: Encrypt report
shell: bash
Expand Down
1 change: 1 addition & 0 deletions cspell-dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Interledger
Fynbos
Rafiki
Chimoney

SPSP
webextension
Expand Down
28 changes: 16 additions & 12 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,22 @@ Make sure you run `pnpm build chrome` before running tests.
1. Copy `tests/e2e/.env.example` to `tests/e2e/.env`
2. Update `tests/e2e/.env` with your secrets.

| Environment Variable | Description | Secret? | Optional? |
| --------------------------- | ----------------------------------------------------------- | ------- | --------- |
| `TEST_WALLET_ORIGIN` | URL origin of the test wallet | - | - |
| `TEST_WALLET_USERNAME` | -- Login email for the test wallet | - | - |
| `TEST_WALLET_PASSWORD` | -- Login password for the test wallet | Yes | - |
| `TEST_WALLET_ADDRESS_URL` | Your wallet address URL that will be connected to extension | - | - |
| `TEST_WALLET_KEY_ID` | ID of the key that will be connected to extension (UUID v4) | - | - |
| `TEST_WALLET_PRIVATE_KEY` | Private key (hex-encoded Ed25519 private key) | Yes | - |
| `TEST_WALLET_PUBLIC_KEY` | Public key (base64-encoded Ed25519 public key) | - | - |
| `FYNBOS_WALLET_ADDRESS_URL` | Fynbos wallet address (used for Fynbos specific tests only) | - | Yes |
| `FYNBOS_USERNAME` | -- Login email for Fynbos wallet | - | Yes |
| `FYNBOS_PASSWORD` | -- Login password for Fynbos wallet | Yes | Yes |
| Environment Variable | Description | Secret? | Optional? |
| ----------------------------- | ----------------------------------------------------------- | ------- | --------- |
| `TEST_WALLET_ORIGIN` | URL origin of the test wallet | - | - |
| `TEST_WALLET_USERNAME` | -- Login email for the test wallet | - | - |
| `TEST_WALLET_PASSWORD` | -- Login password for the test wallet | Yes | - |
| `TEST_WALLET_ADDRESS_URL` | Your wallet address URL that will be connected to extension | - | - |
| `TEST_WALLET_KEY_ID` | ID of the key that will be connected to extension (UUID v4) | - | - |
| `TEST_WALLET_PRIVATE_KEY` | Private key (hex-encoded Ed25519 private key) | Yes | - |
| `TEST_WALLET_PUBLIC_KEY` | Public key (base64-encoded Ed25519 public key) | - | - |
| `FYNBOS_WALLET_ADDRESS_URL` | Fynbos wallet address (used for Fynbos specific tests only) | - | Yes |
| `FYNBOS_USERNAME` | -- Login email for Fynbos wallet | - | Yes |
| `FYNBOS_PASSWORD` | -- Login password for Fynbos wallet | Yes | Yes |
| `CHIMONEY_WALLET_ORIGIN` | Chimoney wallet dashboard URL (for Chimoney specific tests) | - | Yes |
| `CHIMONEY_WALLET_ADDRESS_URL` | -- Chimoney wallet address | - | Yes |
| `CHIMONEY_USERNAME` | -- Login email for Chimoney wallet | - | Yes |
| `CHIMONEY_PASSWORD` | -- Login password for Chimoney wallet | Yes | Yes |

To get the `TEST_WALLET_KEY_ID`, `TEST_WALLET_PRIVATE_KEY` and `TEST_WALLET_PUBLIC_KEY`:

Expand Down
9 changes: 9 additions & 0 deletions src/background/services/keyAutoAdd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,11 @@ function getContentScripts(): Scripting.RegisteredContentScript[] {
matches: ['https://eu1.fynbos.dev/*', 'https://wallet.fynbos.app/*'],
js: ['content/keyAutoAdd/fynbos.js'],
},
{
id: 'keyAutoAdd/chimoney',
matches: ['https://sandbox.chimoney.io/*', 'https://dash.chimoney.io/*'],
js: ['content/keyAutoAdd/chimoney.js'],
},
];
}

Expand All @@ -221,6 +226,10 @@ function walletAddressToProvider(walletAddress: WalletAddress): string {
return 'https://eu1.fynbos.dev/settings/keys';
case 'fynbos.me':
return 'https://wallet.fynbos.app/settings/keys';
case 'ilp-sandbox.chimoney.com':
return 'https://sandbox.chimoney.io/interledger';
case 'ilp.chimoney.com':
return 'https://dash.chimoney.io/interledger';
default:
throw new ErrorWithKey('connectWalletKeyService_error_notImplemented');
}
Expand Down
113 changes: 113 additions & 0 deletions src/content/keyAutoAdd/chimoney.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// 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';
import { getAuthToken, getWalletAddressId } from './lib/helpers/chimoney';

// #region: Steps

const waitForLogin: Run<void> = async (
{ keyAddUrl },
{ skip, setNotificationSize },
) => {
await sleep(2000);
let alreadyLoggedIn = window.location.href.startsWith(keyAddUrl);
if (!alreadyLoggedIn) setNotificationSize('notification');
try {
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<{ walletAddressId: string }> = async (
{ walletAddressUrl },
{ setNotificationSize },
) => {
const walletAddress = new URL(walletAddressUrl);
setNotificationSize('fullscreen');

await waitForElement('h5', {
match: (el) => el.textContent?.trim() === 'Interledger Wallet Address Info',
}).catch(() => {
throw new Error('Are you on the correct page?');
});

const walletAddressElem = await waitForElement('h6', {
match(el) {
const prefix = walletAddress.hostname;
const text = el.textContent?.trim() ?? '';
return text.startsWith(`Wallet Address:`) && text.includes(prefix);
},
}).catch(() => {
throw new Error(
'Failed to find wallet address. Are you on the correct page?',
);
});

// To match both payment pointer and wallet address URL format
const walletAddressText = walletAddress.hostname + walletAddress.pathname;
if (walletAddressElem.textContent?.includes(walletAddressText)) {
throw new ErrorWithKey('connectWalletKeyService_error_accountNotFound');
}

const walletAddressId = await getWalletAddressId();
return { walletAddressId };
};

const addKey: Run<void> = async ({ publicKey }, { output }) => {
const { walletAddressId } = output(findWallet);

const res = await fetch('/api/interledger/create-user-wallet-address-key', {
method: 'POST',
headers: {
authorization: `Bearer ${getAuthToken()}`,
'content-type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
key: publicKey,
walletAddressId,
// Chimoney doesn't have a nickName field supported yet
}),
}).catch((error) => {
return Response.json(null, { status: 599, statusText: error.message });
});

if (!res.ok) {
throw new Error(`Failed to upload public key (${res.statusText})`);
}
const data = await res.json().catch(() => null);
if (data?.status !== 'success') {
throw new Error(`Failed to upload public key (${await res.text()})`);
}
};
// #endregion

// #region: Main
new KeyAutoAdd([
{
name: 'Waiting for you to login',
run: waitForLogin,
maxDuration: LOGIN_WAIT_TIMEOUT,
},
{ name: 'Finding wallet', run: findWallet },
{ name: 'Adding key', run: addKey },
]).init();
// #endregion
46 changes: 46 additions & 0 deletions src/content/keyAutoAdd/lib/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,52 @@ 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 Down
49 changes: 49 additions & 0 deletions src/content/keyAutoAdd/lib/helpers/chimoney.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { sleep } from '@/shared/helpers';

export const getAuthToken = (): string => {
const getFirebaseAuthKey = () => {
for (let i = 0; i < sessionStorage.length; i++) {
const key = sessionStorage.key(i);
if (key?.startsWith('firebase:authUser:')) {
return key;
}
}
};

const key = getFirebaseAuthKey();
if (!key) {
throw new Error('No Firebase auth key found');
}
const firebaseDataStr = sessionStorage.getItem(key);
if (!firebaseDataStr) {
throw new Error('No Firebase auth data found');
}
const firebaseData: {
stsTokenManager: {
accessToken: string;
refreshToken: string;
expirationTime: number;
};
} = JSON.parse(firebaseDataStr);
const token = firebaseData?.stsTokenManager?.accessToken;
if (!token) {
throw new Error('No Firebase auth token found');
}
const JWT_REGEX =
/^([A-Za-z0-9-_=]{2,})\.([A-Za-z0-9-_=]{2,})\.([A-Za-z0-9-_=]{2,})$/;
if (!JWT_REGEX.test(token)) {
throw new Error('Invalid Firebase auth token');
}
return token;
};

export const getWalletAddressId = async (): Promise<string> => {
// A Firebase request will set this field eventually. We wait max 6s for that.
let attemptToFindWalletAddressId = 0;
while (++attemptToFindWalletAddressId < 12) {
const walletAddressId = sessionStorage.getItem('walletAddressId');
if (walletAddressId) return walletAddressId;
await sleep(500);
}
throw new Error('No walletAddressId found in sessionStorage');
};
6 changes: 6 additions & 0 deletions tests/e2e/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,9 @@ TEST_WALLET_PRIVATE_KEY="hex-encoded-private-key"
FYNBOS_WALLET_ADDRESS_URL=
FYNBOS_USERNAME=
FYNBOS_PASSWORD=

# Chimoney specific tests, using Chimoney sandbox
CHIMONEY_WALLET_ORIGIN=
CHIMONEY_WALLET_ADDRESS_URL=
CHIMONEY_USERNAME=
CHIMONEY_PASSWORD=
Loading

0 comments on commit 0c4a818

Please sign in to comment.