Skip to content

Commit

Permalink
feat: add fully-typed contract wrapper
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
chadoh committed Jan 29, 2022
1 parent d597aea commit fb6b07f
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 18 deletions.
1 change: 1 addition & 0 deletions template.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
7 changes: 7 additions & 0 deletions template/src/components/App/App.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Nav />
Expand Down
67 changes: 67 additions & 0 deletions template/src/contracts/guest-book.ts
Original file line number Diff line number Diff line change
@@ -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<Message[]> {
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<void> {
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 ?? {}),
});
}
42 changes: 24 additions & 18 deletions template/src/utils/near.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>}
*/
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<string, any> = {},
options: {
parse?: (response: Uint8Array) => any;
stringify?: (input: any) => Buffer;
} = {}
): Promise<any> => {
const account = await near.account(contract);
return account.viewFunction(contract, method, args, options);
};

0 comments on commit fb6b07f

Please sign in to comment.