Skip to content

Commit

Permalink
Directly return errors from background script to in-page script
Browse files Browse the repository at this point in the history
The listener in `browser.runtime.onMessage.addListener` can handle
promises. As such, we can directly _reject_ this promise if there
is any error in invoking the various wallet functions.

Such a rejected promise is then _caught_ inside the content script
and forwarded as an event to the in-page script. The communication
of the events between in-page script and content-script has been
reworked.

1. The new implementation contains less duplicated code due to the
use of a generic `invokeContentScript` function.

2. Every request is tagged with an ID which allows us to uniquely
identify, which particular request failed. This should help with
race conditions where the same event is fired multiple times.

Related: comit-network#196.
  • Loading branch information
thomaseizinger committed Aug 6, 2021
1 parent 4bca80d commit 831e4cb
Show file tree
Hide file tree
Showing 11 changed files with 264 additions and 388 deletions.
5 changes: 4 additions & 1 deletion extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,12 @@
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@types/debug": "^4.1.5",
"@types/debug": "^4.1.7",
"@types/jest": "^25.0.0",
"@types/node": "^12.0.0",
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
"@types/uuid": "^8.3.1",
"debug": "^4.3.1",
"framer-motion": "^4.0.0",
"moment": "^2.29.1",
Expand All @@ -54,7 +55,9 @@
"react-dom": "^17.0.2",
"react-qr-code": "^1.1.1",
"react-scripts": "4.0.1",
"type-fest": "^2.0.0",
"typescript": "^4.1.2",
"uuid": "^8.3.2",
"wasm-pack": "0.10.0",
"web-ext": "^6.1.0",
"web-vitals": "^1.0.1",
Expand Down
8 changes: 4 additions & 4 deletions extension/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,17 +77,17 @@ const App = () => {
{!signLoan && !swapToSign && <OpenLoans openLoans={openLoans} onRepayed={refreshAll} />}

{swapToSign && <ConfirmSwap
onCancel={async (tabId: number) => {
await rejectSwap(tabId);
onCancel={async () => {
await rejectSwap();
refreshAll();
}}
onSuccess={refreshAll}
swapToSign={swapToSign!}
/>}
{signLoan
&& <ConfirmLoan
onCancel={async (tabId: number) => {
await rejectLoan(tabId);
onCancel={async () => {
await rejectLoan();
refreshAll();
}}
onSuccess={refreshAll}
Expand Down
16 changes: 8 additions & 8 deletions extension/src/background-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ export async function getAddress(): Promise<Address> {
return proxy.getAddress();
}

export async function signAndSendSwap(txHex: string, tabId: number): Promise<void> {
export async function signAndSendSwap(txHex: string): Promise<void> {
// @ts-ignore
return proxy.signAndSendSwap(txHex, tabId);
return proxy.signAndSendSwap(txHex);
}

export async function signLoan(tabId: number): Promise<void> {
export async function signLoan(): Promise<void> {
// @ts-ignore
return proxy.signLoan(tabId);
return proxy.signLoan();
}

export async function getLoanToSign(): Promise<LoanToSign | undefined> {
Expand All @@ -28,14 +28,14 @@ export async function getSwapToSign(): Promise<SwapToSign | undefined> {
return proxy.getSwapToSign();
}

export async function rejectLoan(tabId: number): Promise<void> {
export async function rejectLoan(): Promise<void> {
// @ts-ignore
return proxy.rejectLoan(tabId);
return proxy.rejectLoan();
}

export async function rejectSwap(tabId: number): Promise<void> {
export async function rejectSwap(): Promise<void> {
// @ts-ignore
return proxy.rejectSwap(tabId);
return proxy.rejectSwap();
}

export async function withdrawAll(address: string): Promise<Txid> {
Expand Down
220 changes: 108 additions & 112 deletions extension/src/background/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Debug from "debug";
import { browser } from "webextension-polyfill-ts";
import { Direction, Message, MessageKind } from "../messages";
import { LoanDetails, LoanToSign, SwapToSign } from "../models";
import WavesProvider from "../in-page";
import { LoanDetails, LoanToSign, SwapToSign, Txid } from "../models";
import {
bip39SeedWords,
createNewBip39Wallet,
Expand All @@ -26,92 +26,85 @@ import {
// TODO: Is this global or do we need one per file?
Debug.enable("*");
const debug = Debug("background");
const error = Debug("background:error");

// First thing we load settings
loadSettings();

debug("Hello world from background script");

const walletName = "demo";
var swapToSign: SwapToSign | undefined;
var loanToSign: LoanToSign | undefined;

browser.runtime.onMessage.addListener(async (msg: Message<any>, sender) => {
debug(`Received: %o from tab %d`, msg, sender.tab?.id);

if (msg.direction === Direction.ToBackground) {
let message;
switch (msg.kind) {
case MessageKind.WalletStatusRequest:
message = await call_wallet(() => walletStatus(walletName), MessageKind.WalletStatusResponse);
break;
case MessageKind.SellRequest:
message = await call_wallet(
async () => await makeSellCreateSwapPayload(walletName, msg.payload),
MessageKind.SellResponse,
);
break;
case MessageKind.BuyRequest:
message = await call_wallet(
async () => await makeBuyCreateSwapPayload(walletName, msg.payload),
MessageKind.BuyResponse,
);
break;
case MessageKind.AddressRequest:
message = await call_wallet(
async () => await getAddress(walletName),
MessageKind.AddressResponse,
);
break;
case MessageKind.LoanRequest:
message = await call_wallet(
async () =>
await makeLoanRequestPayload(
walletName,
msg.payload.collateral,
msg.payload.fee_rate,
),
MessageKind.LoanResponse,
);
break;
case MessageKind.SignAndSendSwap:
try {
const txHex = msg.payload;
const decoded = await extractTrade(walletName, txHex);
swapToSign = { txHex, decoded, tabId: sender.tab!.id! };
updateBadge();
} catch (e) {
error(e);
message = { kind: MessageKind.SwapTxid, direction: Direction.ToPage, error: e };
}
break;
case MessageKind.SignLoan:
try {
const details = await extractLoan(walletName, msg.payload);
loanToSign = { details, tabId: sender.tab!.id! };
updateBadge();
} catch (e) {
error(e);
message = { kind: MessageKind.SignedLoan, direction: Direction.ToPage, error: e };
}
break;
}
return message;
}

var swapToSign: SwapToSign | null;
var resolveSwapSignRequest: ((txid: Txid) => void) | null;
var rejectSwapSignRequest: ((e: any) => void) | null;

var loanToSign: LoanToSign | null;
var resolveLoanSignRequest: ((txid: Txid) => void) | null;
var rejectLoanSignRequest: ((e: any) => void) | null;

export interface RpcMessage<T extends keyof WavesProvider> {
type: "rpc-message";
method: T;
args: Parameters<WavesProvider[T]>;
}

/*
* Defines the public interface of the background script.
*
* To ensure maximum benefit from the type checker, other components like the content script should only use this function to send messages.
*/
export function invokeBackgroundScriptRpc(message: Omit<RpcMessage<keyof WavesProvider>, "type">): Promise<any> {
return browser.runtime.sendMessage({
type: "rpc-message",
...message,
});
}

addRpcMessageListener("walletStatus", () => walletStatus(walletName));
addRpcMessageListener("getBuyCreateSwapPayload", ([usdt]) => makeBuyCreateSwapPayload(walletName, usdt));
addRpcMessageListener("getSellCreateSwapPayload", ([btc]) => makeSellCreateSwapPayload(walletName, btc));
addRpcMessageListener("getNewAddress", () => getAddress(walletName));
addRpcMessageListener(
"makeLoanRequestPayload",
([collateral, feerate]) => makeLoanRequestPayload(walletName, collateral, feerate),
);

addRpcMessageListener("signAndSendSwap", ([txHex]) => {
extractTrade(walletName, txHex).then(decoded => {
swapToSign = { txHex, decoded };
updateBadge();
});

return new Promise((resolve, reject) => {
resolveSwapSignRequest = resolve;
rejectSwapSignRequest = reject;
});
});
addRpcMessageListener("signLoan", ([loanRequest]) => {
extractLoan(walletName, loanRequest).then(details => {
loanToSign = { details };
updateBadge();
});

return new Promise((resolve, reject) => {
resolveLoanSignRequest = resolve;
rejectLoanSignRequest = reject;
});
});

async function call_wallet<T>(wallet_fn: () => Promise<T>, kind: MessageKind): Promise<Message<T | undefined>> {
let payload;
let err;
try {
payload = await wallet_fn();
} catch (e) {
error(e);
err = e;
}
function addRpcMessageListener<T extends keyof WavesProvider>(
method: T,
callback: (args: Parameters<WavesProvider[T]>) => ReturnType<WavesProvider[T]>,
) {
browser.runtime.onMessage.addListener((msg: RpcMessage<T>) => {
if (msg.type !== "rpc-message" || msg.method !== method) {
return;
}

debug(`Received: %o`, msg);

return { kind, direction: Direction.ToPage, payload, error: err };
return callback(msg.args);
});
}

// @ts-ignore
Expand All @@ -135,57 +128,60 @@ window.getSwapToSign = async () => {
return swapToSign;
};
// @ts-ignore
window.signAndSendSwap = async (txHex: string, tabId: number) => {
let payload;
let err;

try {
payload = await signAndSendSwap(walletName, txHex);
} catch (e) {
error(e);
err = e;
window.signAndSendSwap = (txHex: string) => {
if (!resolveSwapSignRequest || !rejectSwapSignRequest) {
throw new Error("No pending promise functions for swap sign request");
}

browser.tabs.sendMessage(tabId, { direction: Direction.ToPage, kind: MessageKind.SwapTxid, payload, error: err });
swapToSign = undefined;
updateBadge();
signAndSendSwap(walletName, txHex).then(resolveSwapSignRequest).catch(rejectSwapSignRequest).then(() => {
resolveSwapSignRequest = null;
rejectSwapSignRequest = null;
swapToSign = null;
updateBadge();
});
};
// @ts-ignore
window.rejectSwap = (tabId: number) => {
browser.tabs.sendMessage(tabId, { direction: Direction.ToPage, kind: MessageKind.SwapRejected });
swapToSign = undefined;
window.rejectSwap = () => {
if (!resolveSwapSignRequest || !rejectSwapSignRequest) {
throw new Error("No pending promise functions for swap sign request");
}

rejectSwapSignRequest("User declined signing request");
rejectSwapSignRequest = null;
swapToSign = null;

updateBadge();
};
// @ts-ignore
window.getLoanToSign = () => {
return loanToSign;
};
// @ts-ignore
window.signLoan = async (tabId: number) => {
window.signLoan = async () => {
if (!resolveLoanSignRequest || !rejectLoanSignRequest) {
throw new Error("No pending promise functions for loan sign request");
}

// TODO: Currently, we assume that whatever the user has verified
// on the pop-up matches what is stored in the extension's
// storage. It would be better to send around the swap ID to check
// that the wallet is signing the same transaction the user has authorised

let payload;
let err;

try {
payload = await signLoan(walletName);
} catch (e) {
error(e);
err = e;
}

browser.tabs.sendMessage(tabId, { direction: Direction.ToPage, kind: MessageKind.SignedLoan, payload, error: err });
loanToSign = undefined;
updateBadge();
signLoan(walletName).then(resolveLoanSignRequest).catch(rejectLoanSignRequest).then(() => {
resolveLoanSignRequest = null;
rejectLoanSignRequest = null;
loanToSign = null;
updateBadge();
});
};
// @ts-ignore
window.rejectLoan = (tabId: number) => {
browser.tabs.sendMessage(tabId, { direction: Direction.ToPage, kind: MessageKind.LoanRejected });
loanToSign = undefined;
updateBadge();
window.rejectLoan = () => {
if (!resolveLoanSignRequest || !rejectLoanSignRequest) {
throw new Error("No pending promise functions for loan sign request");
}

rejectLoanSignRequest("User declined signing request");
rejectLoanSignRequest = null;
loanToSign = null;
};
// @ts-ignore
window.withdrawAll = async (address: string) => {
Expand Down
6 changes: 3 additions & 3 deletions extension/src/components/ConfirmLoan.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Usdt from "./tether.svg";
const debug = Debug("confirmloan:error");

interface ConfirmLoanProps {
onCancel: (tabId: number) => void;
onCancel: () => void;
onSuccess: () => void;
loanToSign: LoanToSign;
}
Expand All @@ -21,7 +21,7 @@ export default function ConfirmLoan(
) {
let { isPending, run } = useAsync({
deferFn: async () => {
await signLoan(loanToSign.tabId);
await signLoan();
onSuccess();
},
});
Expand Down Expand Up @@ -92,7 +92,7 @@ export default function ConfirmLoan(
<Button
variant="secondary"
mr={3}
onClick={() => onCancel(loanToSign.tabId)}
onClick={() => onCancel()}
>
Cancel
</Button>
Expand Down
Loading

0 comments on commit 831e4cb

Please sign in to comment.