Skip to content

Commit

Permalink
feat: don't ask again for similar transactions
Browse files Browse the repository at this point in the history
when sending transactions, offer to not ask for confirmation later
for transactions calling the same contract and method
  • Loading branch information
petersalomonsen committed Jan 6, 2024
1 parent e9e6173 commit 895a294
Show file tree
Hide file tree
Showing 5 changed files with 370 additions and 95 deletions.
2 changes: 1 addition & 1 deletion dist/index.js

Large diffs are not rendered by default.

253 changes: 191 additions & 62 deletions src/lib/components/ConfirmTransactions.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import Modal from "react-bootstrap/Modal";
import Alert from "react-bootstrap/Alert";
import Toast from "react-bootstrap/Toast";
import ToastContainer from "react-bootstrap/ToastContainer";
import { Markdown } from "./Markdown";
import { displayGas, displayNear, Loading } from "../data/utils";
import { displayGas, displayNear, Loading, computeWritePermission } from "../data/utils";
import { useNear } from "../data/near";
import { useCache } from "../data/cache";
import { useAccountId } from "../data/account";
import uuid from "react-uuid";

const jsonMarkdown = (data) => {
Expand All @@ -12,78 +17,202 @@ ${json}
\`\`\``;
};

const StorageDomain = {
page: "confirm_transactions",
};

const StorageType = {
SendTransactionWithoutConfirmation: "send_transaction_without_confirmation",
};

export default function ConfirmTransactions(props) {
const gkey = useState(uuid());
const near = useNear(props.networkId);
const accountId = useAccountId(props.networkId);
const cache = useCache();

const [loading, setLoading] = useState(false);

const [transactions] = useState(props.transactions);
const [dontAskForConfirmation, setDontAskForConfirmation] = useState(null);
const [dontAskAgainChecked, setDontAskAgainChecked] = useState(false);
const [dontAskAgainErrorMessage, setDontAskAgainErrorMessage] = useState(null);

const widgetSrc = props.widgetSrc;

const getWidgetContractPermission = async (widgetSrc, contractId) =>
await cache.asyncLocalStorageGet(StorageDomain, {
widgetSrc,
contractId,
type: StorageType.SendTransactionWithoutConfirmation,
});

const eligibleForDontAskAgain = transactions[0].contractName !== near.contract.contractId && transactions.length === 1 && !(transactions[0].deposit && transactions[0].deposit.gt(0));

useEffect(() => {
(async () => {
if (eligibleForDontAskAgain) {
const contractId = transactions[0].contractName;
const isSignedIntoContract = await near.isSignedIntoContract(contractId);

const widgetContractPermission = await getWidgetContractPermission(widgetSrc, contractId);

const dontAskForConfirmation = !!(isSignedIntoContract && widgetContractPermission && widgetContractPermission[transactions[0].methodName]);

setDontAskForConfirmation(dontAskForConfirmation);

if (dontAskForConfirmation) {
setLoading(true);
const result = await near.sendTransactions(transactions);
setLoading(false);
onHide(result);
}
} else {
setDontAskForConfirmation(false);
}
})();
}, []);

const onHide = props.onHide;
const transactions = props.transactions;

const show = !!transactions;

return (
<Modal size="xl" centered scrollable show={show} onHide={onHide}>
<Modal.Header closeButton>
<Modal.Title>Confirm Transaction</Modal.Title>
</Modal.Header>
<Modal.Body>
{transactions &&
transactions.map((transaction, i) => (
<div key={`${gkey}-${i}`}>
<div>
<h4>Transaction #{i + 1}</h4>
</div>
<div>
<span className="text-secondary">Contract ID: </span>
<span className="font-monospace">
{transaction.contractName}
</span>
</div>
<div>
<span className="text-secondary">Method name: </span>
<span className="font-monospace">{transaction.methodName}</span>
</div>
{transaction.deposit && transaction.deposit.gt(0) && (
const dontAskAgainCheckboxChange = async () => {
setDontAskAgainChecked(!dontAskAgainChecked);
setDontAskAgainErrorMessage(null);
};

if (dontAskForConfirmation === null) {
return <></>;
} else if (dontAskForConfirmation) {
const transaction = transactions[0];
return (
<ToastContainer position="bottom-end" className="position-fixed">
<Toast show={show} bg="info">
<Toast.Header>
Sending transaction {Loading}
</Toast.Header>
<Toast.Body>
Calling contract <span className="font-monospace">{transaction.contractName}</span> with method <span className="font-monospace">{transaction.methodName}</span>
</Toast.Body>
</Toast>
</ToastContainer>
);
} else {
return (
<Modal size="xl" centered scrollable show={show} onHide={onHide}>
<Modal.Header closeButton>
<Modal.Title>Confirm Transaction</Modal.Title>
</Modal.Header>
<Modal.Body>
{transactions &&
transactions.map((transaction, i) => (
<div key={`${gkey}-${i}`}>
<div>
<span className="text-secondary">Deposit: </span>
<h4>Transaction #{i + 1}</h4>
</div>
<div>
<span className="text-secondary">Contract ID: </span>
<span className="font-monospace">
{displayNear(transaction.deposit)}
{transaction.contractName}
</span>
</div>
)}
<div>
<span className="text-secondary">Gas: </span>
<span className="font-monospace">
{displayGas(transaction.gas)}
</span>
<div>
<span className="text-secondary">Method name: </span>
<span className="font-monospace">{transaction.methodName}</span>
</div>
{transaction.deposit && transaction.deposit.gt(0) && (
<div>
<span className="text-secondary">Deposit: </span>
<span className="font-monospace">
{displayNear(transaction.deposit)}
</span>
</div>
)}
<div>
<span className="text-secondary">Gas: </span>
<span className="font-monospace">
{displayGas(transaction.gas)}
</span>
</div>
<Markdown text={jsonMarkdown(transaction.args)} />
</div>
<Markdown text={jsonMarkdown(transaction.args)} />
</div>
))}
</Modal.Body>
<Modal.Footer>
<button
className="btn btn-success"
disabled={loading}
onClick={(e) => {
e.preventDefault();
setLoading(true);
near.sendTransactions(transactions).then(() => {
))}
{eligibleForDontAskAgain ?
<>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="dontaskagaincheckbox"
checked={dontAskAgainChecked}
onChange={() => dontAskAgainCheckboxChange()}
/>
<label class="form-check-label" for="dontaskagaincheckbox">
Don't ask again for sending similar transactions by{" "}
<span className="font-monospace">{widgetSrc}</span>
</label>
</div>
{dontAskAgainErrorMessage ?
<Alert variant="danger">
There was an error when choosing "Don't ask again": {dontAskAgainErrorMessage}
</Alert>
: <></>}
</>
:
<></>
}
</Modal.Body>
<Modal.Footer>
<button
className="btn btn-success"
disabled={loading}
onClick={async (e) => {
e.preventDefault();
setLoading(true);
if (dontAskAgainChecked) {
const pendingTransaction = transactions[0];
const contractId = pendingTransaction.contractName;
const methodName = pendingTransaction.methodName;
const permissionObject = (await getWidgetContractPermission(widgetSrc, contractId)) || {};
permissionObject[methodName] = true;

cache.localStorageSet(
StorageDomain,
{
widgetSrc,
contractId,
type: StorageType.SendTransactionWithoutConfirmation,
},
permissionObject
);

try {
if (!(await near.isSignedIntoContract(contractId))) {
const results = await near.signInAndSetPendingTransaction(pendingTransaction);
setLoading(false);
onHide(results ? results.find(result => result.transaction.receiver_id === contractId) : results);
return;
}
} catch (e) {
setDontAskAgainErrorMessage(e.message);
setLoading(false);
return;
}
}
const result = await near.sendTransactions(transactions);
setLoading(false);
onHide();
});
}}
>
{loading && Loading} Confirm
</button>
<button
className="btn btn-secondary"
onClick={onHide}
disabled={loading}
>
Close
</button>
</Modal.Footer>
</Modal>
);
onHide(result);
}}
>
{loading && Loading} Confirm
</button>
<button
className="btn btn-secondary"
onClick={onHide}
disabled={loading}
>
Close
</button>
</Modal.Footer>
</Modal >
);
}
}
21 changes: 15 additions & 6 deletions src/lib/components/Widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import React, {
useContext,
useEffect,
useLayoutEffect,
useState,
useState
} from "react";
import { useNear } from "../data/near";
import ConfirmTransactions from "./ConfirmTransactions";
Expand Down Expand Up @@ -53,8 +53,9 @@ export const Widget = React.forwardRef((props, forwardedRef) => {
const [prevVmInput, setPrevVmInput] = useState(null);
const [configs, setConfigs] = useState(null);
const [srcOrCode, setSrcOrCode] = useState(null);

const ethersProviderContext = useContext(EthersProviderContext);

const networkId =
configs &&
configs.findLast((config) => config && config.networkId)?.networkId;
Expand Down Expand Up @@ -182,7 +183,7 @@ export const Widget = React.forwardRef((props, forwardedRef) => {
requestCommit,
confirmTransactions,
configs,
ethersProviderContext,
ethersProviderContext
]);

useEffect(() => {
Expand Down Expand Up @@ -239,7 +240,7 @@ export const Widget = React.forwardRef((props, forwardedRef) => {
forwardedProps,
]);

return element !== null && element !== undefined ? (
const widget = element !== null && element !== undefined ? (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
Expand All @@ -250,9 +251,15 @@ export const Widget = React.forwardRef((props, forwardedRef) => {
<>
{element}
{transactions && (
<ConfirmTransactions
<ConfirmTransactions
transactions={transactions}
onHide={() => setTransactions(null)}
widgetSrc={src}
onHide={(result) => {
setTransactions(null);
if (result && result.transaction) {
cache.invalidateCache(near, result.transaction.receiver_id);
}
}}
networkId={networkId}
/>
)}
Expand All @@ -273,4 +280,6 @@ export const Widget = React.forwardRef((props, forwardedRef) => {
) : (
loading ?? Loading
);

return widget;
});
3 changes: 3 additions & 0 deletions src/lib/data/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ class Cache {
} catch {
// ignore
}
} else if (key.action === Action.ViewCall && key.contractId === data) {
// Invalidate cache for entire contract
affectedKeys.push([stringKey, key.blockId === "final"]);
}
// Trying to parse index
if (key.action === Action.Fetch && key.url === indexUrl) {
Expand Down
Loading

0 comments on commit 895a294

Please sign in to comment.