Skip to content

Commit

Permalink
Merge pull request #44 from NearSocial/support-ethers-js
Browse files Browse the repository at this point in the history
- Support `ethers.js` based on NearSocial/viewer#130
  - Expose `Ethers` and `ethers` in the global scope.
  - Add custom `Web3Connect` component that renders Web3 connect/disconnect button. Currently, the API is heavily influenced by Web3Onboard API.
  - VM now exports `EthersProviderContext` React context type. A gateway that wants to support Ethers.js should wrap the app with `EthersProviderContext.Provider` component with the object value `{provider}`. Provider is Web3 provider that can be used to create an Ethers.js provider.
  • Loading branch information
Evgeny Kuzyakov authored Apr 24, 2023
2 parents e420243 + f34875f commit d25338c
Show file tree
Hide file tree
Showing 10 changed files with 502 additions and 4 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Pending

- Support `ethers.js` based on https://github.com/NearSocial/viewer/pull/130
- Expose `Ethers` and `ethers` in the global scope.
- Add custom `Web3Connect` component that renders Web3 connect/disconnect button. Currently, the API is heavily influenced by Web3Onboard API.
- VM now exports `EthersProviderContext` React context type. A gateway that wants to support Ethers.js should wrap the app with `EthersProviderContext.Provider` component with the object value `{provider}`. Provider is Web3 provider that can be used to create an Ethers.js provider.
- Fix `initNear` logic to assign provided `config` values on top of the default values, instead of reverse.
- Update `near-api-js` dependency to ^2.1.0
- Fix `elliptic` library by doing a lazy `deepClone` when it's first requested a VM instance.
Expand Down
2 changes: 1 addition & 1 deletion dist/index.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"collections": "^5.1.12",
"deep-equal": "^2.2.0",
"elliptic": "^6.5.4",
"ethers": "^5.7.2",
"idb": "^7.1.1",
"iframe-resizer-react": "^1.1.0",
"local-storage": "^2.0.0",
Expand Down
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Widget } from "./lib/components/Widget";
import { useCache } from "./lib/data/cache";
import * as utils from "./lib/data/utils";
import { CommitButton } from "./lib/components/Commit";
import { EthersProviderContext } from "./lib/components/ethers";

export {
Widget,
Expand All @@ -15,4 +16,5 @@ export {
useAccount,
useAccountId,
utils,
EthersProviderContext,
};
5 changes: 5 additions & 0 deletions src/lib/components/Widget.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, {
useCallback,
useContext,
useEffect,
useLayoutEffect,
useState,
Expand All @@ -25,6 +26,7 @@ import { useAccountId } from "../data/account";
import Big from "big.js";
import uuid from "react-uuid";
import { isFunction } from "react-bootstrap-typeahead/types/utils";
import { EthersProviderContext } from "./ethers";

const AcornOptions = {
ecmaVersion: 13,
Expand Down Expand Up @@ -85,6 +87,7 @@ 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 cache = useCache();
const near = useNear();
Expand Down Expand Up @@ -210,6 +213,7 @@ export const Widget = React.forwardRef((props, forwardedRef) => {
requestCommit,
version: uuid(),
widgetConfigs: configs,
ethersProviderContext,
});
setVm(vm);
return () => {
Expand All @@ -223,6 +227,7 @@ export const Widget = React.forwardRef((props, forwardedRef) => {
requestCommit,
confirmTransactions,
configs,
ethersProviderContext,
]);

useEffect(() => {
Expand Down
27 changes: 27 additions & 0 deletions src/lib/components/ethers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import React, { useContext } from "react";

export const EthersProviderContext = React.createContext(null);

export const Web3ConnectButton = (props) => {
const ethersProviderContext = useContext(EthersProviderContext);
const [{ wallet, connecting }, connect, disconnect] =
ethersProviderContext?.useConnectWallet
? ethersProviderContext?.useConnectWallet()
: [{}];

return (
<button
className={`btn ${props.className} ${
connecting || wallet ? "btn-outline-secondary" : "btn-outline-primary"
}`}
disabled={(wallet ? !disconnect : !connect) || connecting}
onClick={() => (wallet ? disconnect?.(wallet) : connect?.())}
>
{connecting
? props.connectingLabel ?? "Connecting"
: wallet
? props.disconnectLabel ?? "Disconnect Web3 Wallet"
: props.connectLabel ?? "Connect Web3 Wallet"}
</button>
);
};
16 changes: 16 additions & 0 deletions src/lib/data/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const Action = {
Block: "Block",
LocalStorage: "LocalStorage",
CustomPromise: "CustomPromise",
EthersCall: "EthersCall",
};

const CacheStatus = {
Expand Down Expand Up @@ -386,6 +387,21 @@ class Cache {
this.invalidateCallbacks(cached, false);
}
}

cachedEthersCall(ethersProvider, callee, args, invalidate) {
if (!ethersProvider) {
return null;
}
return this.cachedPromise(
{
action: Action.EthersCall,
callee,
args,
},
() => ethersProvider[callee](...args),
invalidate
);
}
}

const defaultCache = new Cache();
Expand Down
3 changes: 3 additions & 0 deletions src/lib/data/utils.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Big from "big.js";
import React from "react";
import equal from "deep-equal";
import { ethers } from "ethers";

export const TGas = Big(10).pow(12);
export const MaxGasPerTransaction = TGas.mul(250);
Expand Down Expand Up @@ -372,6 +373,8 @@ export const deepCopy = (o) => {
return new Blob([o], { type: o.type });
} else if (o instanceof Uint8Array || o instanceof ArrayBuffer) {
return o.slice(0);
} else if (o instanceof ethers.BigNumber) {
return o;
} else if (isObject(o)) {
if (isReactObject(o)) {
return o;
Expand Down
37 changes: 37 additions & 0 deletions src/lib/vm/vm.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ import * as Toggle from "@radix-ui/react-toggle";
import * as ToggleGroup from "@radix-ui/react-toggle-group";
import * as Toolbar from "@radix-ui/react-toolbar";
import * as RadixTooltip from "@radix-ui/react-tooltip";
import { ethers } from "ethers";
import { Web3ConnectButton } from "../components/ethers";

const frozenNacl = Object.freeze({
randomBytes: deepFreeze(nacl.randomBytes),
Expand All @@ -69,6 +71,12 @@ const frozenNacl = Object.freeze({
verify: deepFreeze(nacl.verify),
});

const frozenEthers = Object.freeze({
utils: deepFreeze(ethers.utils),
BigNumber: deepFreeze(ethers.BigNumber),
Contract: deepFreeze(ethers.Contract),
});

// `nanoid.nanoid()` is a but odd, but it seems better to match the official
// API than to create an alias
const frozenNanoid = Object.freeze({
Expand Down Expand Up @@ -173,6 +181,7 @@ const ApprovedTagsCustom = {
OverlayTrigger: true,
Files: true,
iframe: false,
Web3Connect: false,
};

// will be dynamically indexed into for fetching specific elements
Expand Down Expand Up @@ -238,6 +247,7 @@ const Keywords = {
Map,
Set,
clipboard: true,
Ethers: true,
};

const ReservedKeys = {
Expand Down Expand Up @@ -636,6 +646,8 @@ class VmStack {
return <Files {...attributes}>{children}</Files>;
} else if (element === "iframe") {
return <SecureIframe {...attributes} />;
} else if (element === "Web3Connect") {
return <Web3ConnectButton {...attributes} />;
} else if (RadixComp) {
if (element.includes("Portal")) {
throw new Error(
Expand Down Expand Up @@ -963,6 +975,11 @@ class VmStack {
return this.isTrusted
? navigator.clipboard.writeText(...args)
: Promise.reject(new Error("Not trusted (not a click)"));
} else if (keyword === "Ethers") {
if (callee === "provider") {
return this.vm.ethersProvider;
}
return this.vm.cachedEthersCall(callee, args);
}
} else {
const f = callee === keyword ? keywordType : keywordType[callee];
Expand Down Expand Up @@ -1651,6 +1668,7 @@ export default class VM {
requestCommit,
version,
widgetConfigs,
ethersProviderContext,
} = options;

if (!code) {
Expand All @@ -1671,6 +1689,11 @@ export default class VM {
this.version = version;
this.cachedStyledComponents = new Map();
this.widgetConfigs = widgetConfigs;
this.ethersProviderContext = ethersProviderContext;

this.ethersProvider = ethersProviderContext?.provider
? new ethers.providers.Web3Provider(ethersProviderContext.provider)
: null;

this.timeouts = new Set();
this.intervals = new Set();
Expand Down Expand Up @@ -1743,6 +1766,19 @@ export default class VM {
return this.near.viewCall(contractName, methodName, args, blockId);
}

cachedEthersCall(callee, args, subscribe) {
return this.cachedPromise(
(invalidate) =>
this.cache.cachedEthersCall(
this.ethersProvider,
callee,
args,
invalidate
),
subscribe
);
}

cachedNearView(contractName, methodName, args, blockId, subscribe) {
return this.cachedPromise(
(invalidate) =>
Expand Down Expand Up @@ -1823,6 +1859,7 @@ export default class VM {
this.elliptic = _.cloneDeep(elliptic);
return this.elliptic;
},
ethers: frozenEthers,
nanoid: frozenNanoid,
};
this.forwardedProps = forwardedProps;
Expand Down
Loading

0 comments on commit d25338c

Please sign in to comment.