Skip to content

Commit

Permalink
Merge pull request #181 from NearSocial/release-2.6.0
Browse files Browse the repository at this point in the history
## 2.6.0

- Support multiple Limited Access Keys on BOS gateway to enable "Don't ask me again" when interacting with third-party contracts on BOS. See #148

- Provide an error callback in the vm init method to allow gateways to capture handled errors

- FIX: Styled components were not possible to be extended due to an issue parsing Radix components
  • Loading branch information
evgenykuzyakov authored Feb 28, 2024
2 parents fe647fe + d81d071 commit 8dbb065
Show file tree
Hide file tree
Showing 9 changed files with 406 additions and 101 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## 2.6.0

- Support multiple Limited Access Keys on BOS gateway to enable "Don't ask me again" when interacting with third-party contracts on BOS. See https://github.com/NearSocial/VM/issues/148

- Provide an error callback in the vm init method to allow gateways to capture handled errors

- FIX: Styled components were not possible to be extended due to an issue parsing Radix components

## 2.5.6

- FIX: Restrict native object prototypes from being accessed. To address BN issue, reported by BrunoModificato from OtterSec.
Expand Down
2 changes: 1 addition & 1 deletion dist/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "near-social-vm",
"version": "2.5.6",
"version": "2.6.0",
"description": "Near Social VM",
"main": "dist/index.js",
"files": [
Expand Down
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 >
);
}
}
31 changes: 24 additions & 7 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 All @@ -12,6 +12,7 @@ import {
deepCopy,
deepEqual,
ErrorFallback,
ErrorScopes,
isObject,
isString,
isFunction,
Expand Down Expand Up @@ -53,8 +54,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 @@ -111,6 +113,7 @@ export const Widget = React.forwardRef((props, forwardedRef) => {
setElement(null);
if (!code) {
if (code === undefined) {
near.config.errorCallback({scope: ErrorScopes.Source, message: src});
setElement(
<div className="alert alert-danger">
Source code for "{src}" is not found
Expand Down Expand Up @@ -182,7 +185,7 @@ export const Widget = React.forwardRef((props, forwardedRef) => {
requestCommit,
confirmTransactions,
configs,
ethersProviderContext,
ethersProviderContext
]);

useEffect(() => {
Expand Down Expand Up @@ -219,6 +222,8 @@ export const Widget = React.forwardRef((props, forwardedRef) => {
try {
setElement(vm.renderCode(vmInput) ?? "Execution failed");
} catch (e) {
const message = src ? `${src}: ${e.message}` : e.message;
near.config.errorCallback({scope: ErrorScopes.Execution, message});
setElement(
<div className="alert alert-danger">
{src ? (
Expand Down Expand Up @@ -247,9 +252,13 @@ export const Widget = React.forwardRef((props, forwardedRef) => {
forwardedProps,
]);

return element !== null && element !== undefined ? (
const widget = element !== null && element !== undefined ? (
<ErrorBoundary
FallbackComponent={ErrorFallback}
FallbackComponent={ (props) => {
near.config.errorCallback({scope: ErrorScopes.Boundary, message: src});
const { error = { message: ErrorScopes.Boundary } } = props ;
return <ErrorFallback error={error}/>;
}}
onReset={() => {
setElement(null);
}}
Expand All @@ -258,9 +267,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 @@ -281,4 +296,6 @@ export const Widget = React.forwardRef((props, forwardedRef) => {
) : (
loading ?? Loading
);

return widget;
});
Loading

0 comments on commit 8dbb065

Please sign in to comment.