Skip to content

Commit

Permalink
Merge pull request #156 from petersalomonsen/feat/multiple-funciton-a…
Browse files Browse the repository at this point in the history
…ccess-keys

Support multiple access keys in the VM. Allows widgets to request a function access key to a custom contract by issuing a regular transaction, the following transactions from the widgets will not require a wallet confirmation if the user approves that.

Preview URL: 

https://psalomobos.near.page/devhub.near/widget/app

Or see a demonstration video here:

https://youtu.be/CuwWv1zAKf4?si=lBUi9kXxRrhVjv-s

resolves #148 and NEAR-DevHub/neardevhub-bos#137
  • Loading branch information
evgenykuzyakov committed Jan 8, 2024
2 parents e9e6173 + 895a294 commit 6bb10d1
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 6bb10d1

Please sign in to comment.