From fb6b07f5a92a672b7d58e211332d0598aeb3190c Mon Sep 17 00:00:00 2001
From: Chad Ostrowski <221614+chadoh@users.noreply.github.com>
Date: Fri, 28 Jan 2022 21:59:36 -0500
Subject: [PATCH] feat: add fully-typed contract wrapper
Demonstrate a way to wrap a contract's interface with a fully-typed
TypeScript interface by avoiding `naj.Contract` and using `viewFunction`
and `functionCall` instead.
---
template.json | 1 +
template/src/components/App/App.tsx | 7 +++
template/src/contracts/guest-book.ts | 67 ++++++++++++++++++++++++++++
template/src/utils/near.ts | 42 +++++++++--------
4 files changed, 99 insertions(+), 18 deletions(-)
create mode 100644 template/src/contracts/guest-book.ts
diff --git a/template.json b/template.json
index 0a3b8b5..a4c7e5f 100644
--- a/template.json
+++ b/template.json
@@ -13,6 +13,7 @@
"dependencies": {
"buffer": "^6.0.3",
"near-api-js": "0.44.2",
+ "near-units": "^0.1.9",
"web-vitals": "^2.1.0"
},
"eslintConfig": {
diff --git a/template/src/components/App/App.tsx b/template/src/components/App/App.tsx
index 7cf15a2..70ea7b0 100644
--- a/template/src/components/App/App.tsx
+++ b/template/src/components/App/App.tsx
@@ -1,7 +1,14 @@
import React from "react";
import { Nav } from "../Nav";
+import * as GuestBook from "../../contracts/guest-book";
export function App() {
+ React.useEffect(() => {
+ // this is showing an overly-simple way to use the GuestBook import by just
+ // logging the result of the call to `getMessages`
+ GuestBook.getMessages().then(console.log);
+ }, []);
+
return (
<>
diff --git a/template/src/contracts/guest-book.ts b/template/src/contracts/guest-book.ts
new file mode 100644
index 0000000..7fb061c
--- /dev/null
+++ b/template/src/contracts/guest-book.ts
@@ -0,0 +1,67 @@
+import { Gas, NEAR } from "near-units";
+import { Buffer } from "buffer";
+import { view, wallet } from "../utils/near";
+
+// We *could* use `process.env.REACT_APP_CONTRACT_NAME` in this file, since the
+// template started with that environment variable as `guest-book.testnet`.
+//
+// BUT, the idea of files in `src/contracts` is that they each wrap a specific
+// contract. If the env var `REACT_APP_CONTRACT_NAME` changes, this file is
+// still a wrapper around the guest book contract.
+const CONTRACT = "guest-book.testnet";
+
+/**
+ * Fully-typed interface to the guest-book.testnet contract
+ * (see https://github.com/near-examples/guest-book)
+ *
+ * If you're familiar with ABIs in Ethereum: sorry, NEAR doesn't have them! (yet)
+ *
+ * `naj.Contract` gives you the ability to do this:
+ *
+ * export const GuestBook = new naj.Contract(
+ * wallet.account(),
+ * process.env.REACT_APP_CONTRACT_NAME!,
+ * {
+ * viewMethods: ["getMessages"],
+ * changeMethods: ["addMessage"],
+ * }
+ * );
+ *
+ * But this comes with the drawback that `GuestBook.addMessage` and
+ * `GuestBook.getMessages` have no typing. Having good TypeScript types will
+ * make it easier to use and collaborate on your code, so below is an
+ * alternative way.
+ */
+interface Message {
+ premium: boolean;
+ sender: string;
+ text: string;
+}
+
+export async function getMessages(): Promise {
+ return view(CONTRACT, "getMessages");
+}
+
+export async function addMessage(
+ args: {
+ text: string;
+ },
+ options?: {
+ gas?: Gas;
+ attachedDeposit?: NEAR;
+ walletMeta?: string;
+ walletCallbackUrl?: string;
+ stringify?: (input: any) => Buffer;
+ }
+): Promise {
+ const currentUser = wallet.account();
+ if (!currentUser) {
+ throw new Error("You must sign in before you can add a message");
+ }
+ await currentUser.functionCall({
+ contractId: CONTRACT,
+ methodName: "addMessage",
+ args,
+ ...(options ?? {}),
+ });
+}
diff --git a/template/src/utils/near.ts b/template/src/utils/near.ts
index 1852e6a..e463a12 100644
--- a/template/src/utils/near.ts
+++ b/template/src/utils/near.ts
@@ -29,24 +29,30 @@ export const wallet = new naj.WalletConnection(
);
/**
- * Interface to a contract.
+ * Make a view call to a NEAR smart contract.
+ * @see {@link https://docs.near.org/docs/develop/front-end/rpc#call-a-contract-function}
*
- * If you're familiar with ABIs in Ethereum: sorry, NEAR doesn't have them! (yet)
+ * near-api-js requires instantiating an "account" object, but this is NOT
+ * used to sign view functions. This `view` function will instantiate an
+ * account object for the provided `contract`, essentially causing it to view
+ * itself.
*
- * You need to know the contract's interface. When you instantiate the contract
- * here, you can specify view methods (those you'd check with `near view` in
- * NEAR CLI) and change methods (those you'd call with `near call` in NEAR CLI).
- *
- * You can always skip instantiating a `naj.Contract` altogether and call
- * `wallet.account().viewFunction()` and `wallet.account().functionCall()`
- * directly. This is what `naj.Contract` does under the hood, and these
- * lower-level functions have more explicit (and typed!) APIs.
+ * @param contract NEAR account where the contract is deployed
+ * @param method The view-only method (no state mutations) name on the contract as it is written in the contract code
+ * @param args Any arguments to the view contract method, wrapped in JSON
+ * @param options.parse Parse the result of the call. Receives a Buffer (bytes array) and converts it to any object. By default result will be treated as json.
+ * @param options.stringify Convert input arguments into a bytes array. By default the input is treated as a JSON.
+ * @returns {Promise}
*/
-export const contract = new naj.Contract(
- wallet.account(),
- process.env.REACT_APP_CONTRACT_NAME!,
- {
- viewMethods: ["getMessages"],
- changeMethods: ["addMessage"],
- }
-);
+export const view = async (
+ contract: string,
+ method: string,
+ args: Record = {},
+ options: {
+ parse?: (response: Uint8Array) => any;
+ stringify?: (input: any) => Buffer;
+ } = {}
+): Promise => {
+ const account = await near.account(contract);
+ return account.viewFunction(contract, method, args, options);
+};