Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
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
4 changes: 4 additions & 0 deletions .github/workflows/nightly-build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ jobs:
CHIMONEY_WALLET_ADDRESS_URL: ${{ vars.E2E_CHIMONEY_WALLET_ADDRESS_URL }}
CHIMONEY_USERNAME: ${{ vars.E2E_CHIMONEY_USERNAME }}
CHIMONEY_PASSWORD: ${{ secrets.E2E_CHIMONEY_PASSWORD }}
MMAON_WALLET_ORIGIN: ${{ vars.E2E_MMAON_WALLET_URL_ORIGIN }}
MMAON_WALLET_ADDRESS_URL: ${{ vars.E2E_MMAON_WALLET_ADDRESS_URL }}
MMAON_USERNAME: ${{ vars.E2E_MMAON_USERNAME }}
MMAON_PASSWORD: ${{ secrets.E2E_MMAON_PASSWORD }}
INTERLEDGER_CARDS_WALLET_ADDRESS_URL: ${{ vars.E2E_INTERLEDGER_CARDS_WALLET_ADDRESS_URL }}
INTERLEDGER_CARDS_ILP_DEV_WALLET_ADDRESS_URL: ${{ vars.E2E_INTERLEDGER_CARDS_ILP_DEV_WALLET_ADDRESS_URL }}
INTERLEDGER_CARDS_USERNAME: ${{ secrets.E2E_INTERLEDGER_CARDS_USERNAME }}
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 @@ -70,6 +70,10 @@ jobs:
CHIMONEY_WALLET_ADDRESS_URL: ${{ vars.E2E_CHIMONEY_WALLET_ADDRESS_URL }}
CHIMONEY_USERNAME: ${{ vars.E2E_CHIMONEY_USERNAME }}
CHIMONEY_PASSWORD: ${{ secrets.E2E_CHIMONEY_PASSWORD }}
MMAON_WALLET_ORIGIN: ${{ vars.E2E_MMAON_WALLET_URL_ORIGIN }}
MMAON_WALLET_ADDRESS_URL: ${{ vars.E2E_MMAON_WALLET_ADDRESS_URL }}
MMAON_USERNAME: ${{ vars.E2E_MMAON_USERNAME }}
MMAON_PASSWORD: ${{ secrets.E2E_MMAON_PASSWORD }}
INTERLEDGER_CARDS_WALLET_ADDRESS_URL: ${{ vars.E2E_INTERLEDGER_CARDS_WALLET_ADDRESS_URL }}
INTERLEDGER_CARDS_ILP_DEV_WALLET_ADDRESS_URL: ${{ vars.E2E_INTERLEDGER_CARDS_ILP_DEV_WALLET_ADDRESS_URL }}
INTERLEDGER_CARDS_USERNAME: ${{ secrets.E2E_INTERLEDGER_CARDS_USERNAME }}
Expand Down
1 change: 1 addition & 0 deletions cspell-dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Fynbos
Rafiki
Chimoney
GateHub
MMAON

SPSP
webextension
Expand Down
44 changes: 38 additions & 6 deletions src/background/services/keyAutoAdd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ export class KeyAutoAddService {
async addPublicKeyToWallet(
walletAddress: WalletAddress,
existingTabId: TabId,
walletAddressUrl?: string,
) {
const keyAddUrl = walletAddressToProvider(walletAddress);
const keyAddUrl = walletAddressToProvider(walletAddress, walletAddressUrl);
try {
const { publicKey, keyId } = await this.storage.get([
'publicKey',
Expand Down Expand Up @@ -198,9 +199,12 @@ export class KeyAutoAddService {
}));
}

static supports(walletAddress: WalletAddress): boolean {
static supports(
walletAddress: WalletAddress,
walletAddressUrl?: string,
): boolean {
try {
walletAddressToProvider(walletAddress);
walletAddressToProvider(walletAddress, walletAddressUrl);
return true;
} catch {
return false;
Expand Down Expand Up @@ -268,9 +272,37 @@ const CONTENT_SCRIPTS: Scripting.RegisteredContentScript[] = [
js: ['content/keyAutoAdd/gatehub.js'],
persistAcrossSessions: false,
},
{
id: 'keyAutoAdd/mmaon/sandbox',
matches: ['https://staging.mmaon.com/*'],
js: ['content/keyAutoAdd/mmaon.js'],
persistAcrossSessions: false,
},
{
id: 'keyAutoAdd/mmaon/prod',
matches: ['https://www.mmaon.com/*'],
js: ['content/keyAutoAdd/mmaon.js'],
persistAcrossSessions: false,
},
];

function walletAddressToProvider(walletAddress: WalletAddress): string {
function walletAddressToProvider(
walletAddress: WalletAddress,
walletAddressUrl?: string,
): string {
if (walletAddressUrl) {
// Some wallet URLs have redirects to other "managing wallets" and need some
// special handling.
const { host } = new URL(walletAddressUrl);
switch (host) {
case 'ilp-staging.mmaon.com':
return 'https://staging.mmaon.com/wallet/dashboard';
case 'ilp.mmaon.on':
return 'https://www.mmaon.com/wallet/dashboard';
// case 'ilp.dev' is handled normally as ilp.interledger.cards below
}
}

const { host } = new URL(walletAddress.id);
switch (host) {
case 'ilp.interledger-test.dev':
Expand All @@ -290,7 +322,7 @@ function walletAddressToProvider(walletAddress: WalletAddress): string {
return 'https://wallet.sandbox.gatehub.net/#/wallets/';
case 'ilp.gatehub.net':
return 'https://wallet.gatehub.net/#/wallets/';
default:
throw new ErrorWithKey('connectWalletKeyService_error_notImplemented');
}

throw new ErrorWithKey('connectWalletKeyService_error_notImplemented');
}
27 changes: 21 additions & 6 deletions src/background/services/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import { isInvalidClientError } from '@/background/services/openPayments';
import { APP_URL } from '@/background/constants';
import { bytesToHex } from '@noble/hashes/utils';
import type { Cradle } from '@/background/container';
import type { AmountValue, TabId } from '@/shared/types';
import type { AmountValue, TabId, WalletInfo } from '@/shared/types';
import type { WalletAddress } from '@interledger/open-payments';
import type { Browser } from 'webextension-polyfill';

Expand Down Expand Up @@ -147,7 +147,7 @@ export class WalletService {
error.key === 'connectWallet_error_invalidClient' &&
autoKeyAdd
) {
if (!KeyAutoAddService.supports(walletAddress)) {
if (!KeyAutoAddService.supports(walletAddress, walletAddressUrl)) {
this.setConnectStateError(error);
throw new ErrorWithKey(
'connectWalletKeyService_error_notImplemented',
Expand All @@ -169,7 +169,11 @@ export class WalletService {
this.setConnectState(
this.t('connectWalletKeyService_text_stepAddKey'),
);
await this.addPublicKeyToWallet(walletAddress, tabId);
await this.addPublicKeyToWallet(
walletAddress,
tabId,
walletAddressUrl,
);
const grant =
await this.outgoingPaymentGrantService.createOutgoingPaymentGrant(
walletAddress,
Expand Down Expand Up @@ -406,10 +410,13 @@ export class WalletService {
* Adds public key to wallet by "browser automation" - the content script
* takes control of tab when the correct message is sent, and adds the key
* through the wallet's dashboard.
*
* @param walletAddressUrl User provided & normalized wallet address URL
*/
private async addPublicKeyToWallet(
walletAddress: WalletAddress,
tabId: TabId,
walletAddressUrl: string,
) {
const keyAutoAdd = new KeyAutoAddService({
browser: this.browser,
Expand All @@ -420,7 +427,11 @@ export class WalletService {
});
this.events.emit('request_popup_close');
try {
await keyAutoAdd.addPublicKeyToWallet(walletAddress, tabId);
await keyAutoAdd.addPublicKeyToWallet(
walletAddress,
tabId,
walletAddressUrl,
);
} catch (error) {
const isTabClosed = error.key === 'connectWallet_error_tabClosed';
if (tabId && !isTabClosed) {
Expand All @@ -443,10 +454,14 @@ export class WalletService {
}
}

private async retryAddPublicKeyToWallet(walletAddress: WalletAddress) {
private async retryAddPublicKeyToWallet(walletAddress: WalletInfo) {
const tabId = await reuseOrCreateTab(this.browser);
try {
await this.addPublicKeyToWallet(walletAddress, tabId);
await this.addPublicKeyToWallet(
walletAddress,
tabId,
walletAddress.url || '',
);
await this.outgoingPaymentGrantService.rotateToken();
await redirectToWelcomeScreen(
this.browser,
Expand Down
6 changes: 5 additions & 1 deletion src/background/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,15 @@ export const getExchangeRates = async (): Promise<ExchangeRates> => {
`Could not fetch exchange rates. [Status code: ${response.status}]`,
);
}
const rates = await response.json();
const rates: ExchangeRates = await response.json();
if (!rates.base || !rates.rates) {
throw new Error('Invalid rates format');
}

// MMAON rate is not listed at EXCHANGE_RATES_URL. Hardcode it here until it's
// either added to the list or we switch to the preferred solution:
// https://github.com/interledger/web-monetization-extension/issues/977
rates.rates.MMAON ??= 20; // 20 USD = 1 MMAON
return rates;
};

Expand Down
90 changes: 90 additions & 0 deletions src/content/keyAutoAdd/mmaon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// cSpell:ignore nextjs
import { errorWithKey, ErrorWithKey, sleep } from '@/shared/helpers';
import {
KeyAutoAdd,
LOGIN_WAIT_TIMEOUT,
type StepRun as Run,
} from './lib/keyAutoAdd';
import { isTimedOut, waitForURL } from './lib/helpers';
import { walletAddressUrlToId } from './lib/helpers/gatehub';

// #region: Steps

const waitForLogin: Run<void> = async (
{ keyAddUrl },
{ skip, setNotificationSize },
) => {
await sleep(500);
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<void> = async (
{ walletAddressUrl },
{ setNotificationSize },
) => {
setNotificationSize('fullscreen');
const accountAddress = walletAddressUrlToId(walletAddressUrl);

const res = await fetch('/api/gatehub/wallet', { credentials: 'include' });
if (!res.ok) {
throw new Error('Failed to get wallet details');
}
const accountInfo: { address: string } = await res.json();

if (accountInfo.address !== accountAddress) {
throw new ErrorWithKey('connectWalletKeyService_error_accountNotFound');
}
};

const addKey: Run<void> = async ({ publicKey, nickName }) => {
const res = await fetch('/api/open-payments/upload-keys', {
method: 'POST',
body: JSON.stringify({
base64Key: publicKey,
nickname: nickName,
}),
headers: { 'content-type': 'application/json' },
credentials: 'include',
}).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?.keyId) {
// Note: `keyId` is used for revoking keys; it's not same as JWK's `kid`
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
6 changes: 6 additions & 0 deletions tests/e2e/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ CHIMONEY_WALLET_ADDRESS_URL=
CHIMONEY_USERNAME=
CHIMONEY_PASSWORD=

# MMAON specific tests, using MMAON sandbox
MMAON_WALLET_ORIGIN=
MMAON_WALLET_ADDRESS_URL=
MMAON_USERNAME=
MMAON_PASSWORD=

# Interledger.cards specific tests (optional)
INTERLEDGER_CARDS_USERNAME=
INTERLEDGER_CARDS_PASSWORD=
Expand Down
Loading
Loading