Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/multiple function access keys #156

Merged
Show file tree
Hide file tree
Changes from all 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
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;
petersalomonsen marked this conversation as resolved.
Show resolved Hide resolved
}
}
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