7 | This app demonstrates a key element of NEAR’s UX: once an app has
8 | permission to make calls on behalf of a user (that is, once a user
9 | signs in), the app can make calls to the blockchain for them without
10 | prompting extra confirmation. So you’ll see that if you don’t
11 | include a donation, your message gets posted right to the guest book.
12 |
13 |
14 | But, if you do add a donation, then NEAR will double-check that
15 | you’re ok with sending money to this app.
16 |
17 |
18 | Go ahead and sign in to try it out!
19 |
20 | >
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/frontend/near-interface.js:
--------------------------------------------------------------------------------
1 | /* Talking with a contract often involves transforming data, we recommend you to encapsulate that logic into a class */
2 |
3 | import { utils } from 'near-api-js';
4 |
5 | export class GuestBook {
6 |
7 | constructor({ contractId, walletToUse }) {
8 | this.contractId = contractId;
9 | this.wallet = walletToUse
10 | }
11 |
12 | async getMessages() {
13 | const messages = await this.wallet.viewMethod({ contractId: this.contractId, method: "get_messages" })
14 | console.log(messages)
15 | return messages
16 | }
17 |
18 | async addMessage(message, donation) {
19 | const deposit = utils.format.parseNearAmount(donation);
20 | return await this.wallet.callMethod({ contractId: this.contractId, method: "add_message", args: { text: message }, deposit });
21 | }
22 |
23 | }
--------------------------------------------------------------------------------
/frontend/index.js:
--------------------------------------------------------------------------------
1 | // React
2 | import React from 'react';
3 | import ReactDOM from 'react-dom';
4 | import App from './App';
5 |
6 | // NEAR
7 | import { GuestBook } from './near-interface';
8 | import { Wallet } from './near-wallet';
9 |
10 | // When creating the wallet you can choose to create an access key, so the user
11 | // can skip signing non-payable methods when talking wth the contract
12 | const wallet = new Wallet({ createAccessKeyFor: process.env.CONTRACT_NAME })
13 |
14 | // Abstract the logic of interacting with the contract to simplify your flow
15 | const guestBook = new GuestBook({ contractId: process.env.CONTRACT_NAME, walletToUse: wallet });
16 |
17 | // Setup on page load
18 | window.onload = async () => {
19 | const isSignedIn = await wallet.startUp()
20 |
21 | ReactDOM.render(
22 | ,
23 | document.getElementById('root')
24 | );
25 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "guest-book-js",
3 | "version": "1.0.0",
4 | "license": "(MIT AND Apache-2.0)",
5 | "scripts": {
6 | "start": "cd frontend && npm run start",
7 | "deploy": "cd contract && npm run deploy",
8 | "build": "npm run build:contract && npm run build:web",
9 | "build:web": "cd frontend && npm run build",
10 | "build:contract": "cd contract && npm run build",
11 | "test": "npm run build:contract && npm run test:unit && npm run test:integration && npm run test:e2e",
12 | "test:unit": "cd contract && npm test",
13 | "test:integration": "cd integration-tests && npm test -- -- \"./contract/build/contract.wasm\"",
14 | "test:e2e": "cd frontend && npm run test:e2e",
15 | "postinstall": "cd frontend && npm install && cd .. && cd integration-tests && npm install && cd .. && cd contract && npm install"
16 | },
17 | "devDependencies": {
18 | "near-cli": "^3.3.0"
19 | },
20 | "dependencies": {}
21 | }
--------------------------------------------------------------------------------
/contract/src/contract.ts:
--------------------------------------------------------------------------------
1 | import { NearBindgen, near, call, view, Vector } from 'near-sdk-js'
2 | import { POINT_ONE, PostedMessage } from './model'
3 |
4 | @NearBindgen({})
5 | class GuestBook {
6 | messages: Vector = new Vector("v-uid");
7 |
8 | @call({ payableFunction: true })
9 | // Public - Adds a new message.
10 | add_message({ text }: { text: string }) {
11 | // If the user attaches more than 0.1N the message is premium
12 | const premium = near.attachedDeposit() >= BigInt(POINT_ONE);
13 | const sender = near.predecessorAccountId();
14 |
15 | const message: PostedMessage = { premium, sender, text };
16 | this.messages.push(message);
17 | }
18 |
19 | @view({})
20 | // Returns an array of messages.
21 | get_messages({ from_index = 0, limit = 10 }: { from_index: number, limit: number }): PostedMessage[] {
22 | return this.messages.toArray().slice(from_index, from_index + limit);
23 | }
24 |
25 | @view({})
26 | total_messages(): number { return this.messages.length }
27 | }
28 |
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | NEAR Guest Book
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Examples for building on the NEAR blockchain
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/frontend/components/Form.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 |
4 | export default function Form({ onSubmit, currentAccountId }) {
5 | return (
6 |
35 | );
36 | }
37 |
38 | Form.propTypes = {
39 | onSubmit: PropTypes.func.isRequired,
40 | currentUser: PropTypes.shape({
41 | accountId: PropTypes.string.isRequired,
42 | balance: PropTypes.string.isRequired
43 | })
44 | };
--------------------------------------------------------------------------------
/frontend/App.js:
--------------------------------------------------------------------------------
1 | import 'regenerator-runtime/runtime';
2 | import React, { useState, useEffect } from 'react';
3 | import Form from './components/Form';
4 | import SignIn from './components/SignIn';
5 | import Messages from './components/Messages';
6 |
7 | const App = ({ isSignedIn, guestBook, wallet }) => {
8 | const [messages, setMessages] = useState([]);
9 |
10 | useEffect(() => {
11 | guestBook.getMessages().then(setMessages);
12 | }, []);
13 |
14 | onSubmit = async (e) => {
15 | e.preventDefault();
16 |
17 | const { fieldset, message, donation } = e.target.elements;
18 |
19 | fieldset.disabled = true;
20 |
21 | await guestBook.addMessage(message.value, donation.value)
22 | const messages = await guestBook.getMessages()
23 |
24 | setMessages(messages);
25 | message.value = '';
26 | donation.value = '0';
27 | fieldset.disabled = false;
28 | message.focus();
29 | };
30 |
31 | const signIn = () => { wallet.signIn() }
32 |
33 | const signOut = () => { wallet.signOut() }
34 |
35 | return (
36 |
37 |
38 |
39 |
📖 NEAR Guest Book
40 |
{ isSignedIn
41 | ?
42 | :
43 | }
44 |
45 |
46 |
47 |
48 | { isSignedIn
49 | ?
50 | :
51 | }
52 |
53 |
54 |
55 | { !!messages.length && }
56 |
57 |
58 | );
59 | };
60 |
61 | export default App;
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Guest Book 📖
2 | [](https://docs.near.org/tutorials/welcome)
3 | [](https://gitpod.io/#/https://github.com/near-examples/guest-book-js)
4 | [](https://docs.near.org/develop/contracts/anatomy)
5 | [](https://docs.near.org/develop/integrate/frontend)
6 | [](https://actions-badge.atrox.dev/near-examples/guest-book-js/goto)
7 |
8 |
9 | The Guest Book is a simple app that stores messages from users, allowing to pay for a premium message.
10 |
11 | 
12 |
13 |
14 | # What This Example Shows
15 |
16 | 1. How to receive $NEAR on a contract.
17 | 2. How to store and retrieve information from the blockchain.
18 | 3. How to use a `Vector`.
19 | 4. How to interact with a contract from `React JS`.
20 |
21 |
22 |
23 | # Quickstart
24 |
25 | Clone this repository locally or [**open it in gitpod**](https://gitpod.io/#/github.com/near-examples/guest_book-js). Then follow these steps:
26 |
27 | ### 1. Install Dependencies
28 | ```bash
29 | npm install
30 | ```
31 |
32 | ### 2. Test the Contract
33 | Deploy your contract in a sandbox and simulate interactions from users.
34 |
35 | ```bash
36 | npm test
37 | ```
38 |
39 | ### 3. Deploy the Contract
40 | Build the contract and deploy it in a testnet account
41 | ```bash
42 | npm run deploy
43 | ```
44 |
45 | ### 4. Start the Frontend
46 | Start the web application to interact with your smart contract
47 | ```bash
48 | npm start
49 | ```
50 |
51 | ---
52 |
53 | # Learn More
54 | 1. Learn more about the contract through its [README](./contract/README.md).
55 | 2. Check [**our documentation**](https://docs.near.org/develop/welcome).
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "my-near-app",
3 | "version": "1.0.0",
4 | "license": "(MIT AND Apache-2.0)",
5 | "scripts": {
6 | "start": "./start.sh",
7 | "start:headless": "env-cmd -f '../contract/neardev/dev-account.env' parcel index.html",
8 | "build": "parcel build index.html --public-url ./",
9 | "test:e2e": "npm run start:headless & npm run cypress:run",
10 | "cypress:run": "cd .cypress && cypress run",
11 | "cypress:open": "cypress open --browser chromium"
12 | },
13 | "devDependencies": {
14 | "@babel/core": "^7.18.2",
15 | "@babel/preset-env": "^7.18.2",
16 | "@babel/preset-react": "^7.17.12",
17 | "@parcel/transformer-sass": "^2.8.0",
18 | "@types/node": "^18.6.2",
19 | "cypress": "^11.2.0",
20 | "env-cmd": "^10.1.0",
21 | "nodemon": "^2.0.16",
22 | "parcel": "^2.6.0",
23 | "process": "^0.11.10",
24 | "react-test-renderer": "^18.1.0",
25 | "ts-node": "^10.8.0",
26 | "typescript": "^4.7.2"
27 | },
28 | "dependencies": {
29 | "@near-wallet-selector/core": "^7.0.0",
30 | "@near-wallet-selector/ledger": "^7.0.0",
31 | "@near-wallet-selector/math-wallet": "^7.0.0",
32 | "@near-wallet-selector/meteor-wallet": "^7.0.0",
33 | "@near-wallet-selector/modal-ui": "^7.0.0",
34 | "@near-wallet-selector/my-near-wallet": "^7.0.0",
35 | "@near-wallet-selector/near-wallet": "^7.0.0",
36 | "@near-wallet-selector/nightly": "^7.0.0",
37 | "@near-wallet-selector/nightly-connect": "^7.0.0",
38 | "@near-wallet-selector/sender": "^7.0.0",
39 | "@near-wallet-selector/wallet-connect": "^7.0.0",
40 | "near-api-js": "^0.44.2",
41 | "prop-types": "^15.8.1",
42 | "react": "^18.1.0",
43 | "react-dom": "^18.1.0",
44 | "regenerator-runtime": "^0.13.9"
45 | },
46 | "resolutions": {
47 | "@babel/preset-env": "7.13.8"
48 | },
49 | "browserslist": {
50 | "production": [
51 | ">0.2%",
52 | "not dead",
53 | "not op_mini all"
54 | ],
55 | "development": [
56 | "last 1 chrome version",
57 | "last 1 firefox version",
58 | "last 1 safari version"
59 | ]
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/integration-tests/src/main.ava.ts:
--------------------------------------------------------------------------------
1 | import { Worker, NEAR, NearAccount } from 'near-workspaces';
2 | import anyTest, { TestFn } from 'ava';
3 |
4 | const test = anyTest as TestFn<{
5 | worker: Worker;
6 | accounts: Record;
7 | }>;
8 |
9 | test.beforeEach(async (t) => {
10 | // Init the worker and start a Sandbox server
11 | const worker = await Worker.init();
12 |
13 | // deploy contract
14 | const root = worker.rootAccount;
15 |
16 | // some test accounts
17 | const alice = await root.createSubAccount("alice", {
18 | initialBalance: NEAR.parse("30 N").toJSON(),
19 | });
20 | const contract = await root.createSubAccount("contract", {
21 | initialBalance: NEAR.parse("30 N").toJSON(),
22 | });
23 |
24 | // Get wasm file path from package.json test script in folder above
25 | await contract.deploy(process.argv[2]);
26 |
27 | // Save state for test runs, it is unique for each test
28 | t.context.worker = worker;
29 | t.context.accounts = { root, contract, alice };
30 | });
31 |
32 | test.afterEach(async (t) => {
33 | // Stop Sandbox server
34 | await t.context.worker.tearDown().catch((error) => {
35 | console.log("Failed to stop the Sandbox:", error);
36 | });
37 | });
38 |
39 | test("send one message and retrieve it", async (t) => {
40 | const { root, contract } = t.context.accounts;
41 | await root.call(contract, "add_message", { text: "aloha" });
42 | const msgs = await contract.view("get_messages");
43 | const expectedMessagesResult = [
44 | { premium: false, sender: root.accountId, text: "aloha" },
45 | ];
46 | t.deepEqual(msgs, expectedMessagesResult);
47 | });
48 |
49 | test("send two messages and expect two total", async (t) => {
50 | const { root, contract, alice } = t.context.accounts;
51 | await root.call(contract, "add_message", { text: "aloha" });
52 | await alice.call(contract, "add_message", { text: "hola" }, { attachedDeposit: NEAR.parse('1') });
53 |
54 | const total_messages = await contract.view("total_messages");
55 | t.is(total_messages, 2);
56 |
57 | const msgs = await contract.view("get_messages");
58 | const expected = [
59 | { premium: false, sender: root.accountId, text: "aloha" },
60 | { premium: true, sender: alice.accountId, text: "hola" },
61 | ];
62 |
63 | t.deepEqual(msgs, expected);
64 | });
65 |
--------------------------------------------------------------------------------
/contract/README.md:
--------------------------------------------------------------------------------
1 | # Guest Book Contract
2 |
3 | The smart contract stores messages from users. Messages can be `premium` if the user attaches sufficient money (0.1 $NEAR).
4 |
5 | ```ts
6 | this.messages = [];
7 |
8 | @call
9 | // Public - Adds a new message.
10 | add_message({ text }: { text: string }) {
11 | // If the user attaches more than 0.1N the message is premium
12 | const premium = near.attachedDeposit() >= BigInt(POINT_ONE);
13 | const sender = near.predecessorAccountId();
14 |
15 | const message = new PostedMessage({premium, sender, text});
16 | this.messages.push(message);
17 | }
18 |
19 | @view
20 | // Returns an array of messages.
21 | get_messages({ fromIndex = 0, limit = 10 }: { fromIndex: number, limit: number }): PostedMessage[] {
22 | return this.messages.slice(fromIndex, fromIndex + limit);
23 | }
24 | ```
25 |
26 |
27 |
28 | # Quickstart
29 |
30 | 1. Make sure you have installed [node.js](https://nodejs.org/en/download/package-manager/) >= 16.
31 | 2. Install the [`NEAR CLI`](https://github.com/near/near-cli#setup)
32 |
33 |
34 |
35 | ## 1. Build and Deploy the Contract
36 | You can automatically compile and deploy the contract in the NEAR testnet by running:
37 |
38 | ```bash
39 | npm run deploy
40 | ```
41 |
42 | Once finished, check the `neardev/dev-account` file to find the address in which the contract was deployed:
43 |
44 | ```bash
45 | cat ./neardev/dev-account
46 | # e.g. dev-1659899566943-21539992274727
47 | ```
48 |
49 |
50 |
51 | ## 2. Retrieve the Stored Messages
52 | `get_messages` is a read-only method (`view` method) that returns a slice of the vector `messages`.
53 |
54 | `View` methods can be called for **free** by anyone, even people **without a NEAR account**!
55 |
56 | ```bash
57 | near view get_messages '{"from_index":0, "limit":10}'
58 | ```
59 |
60 |
61 |
62 | ## 3. Add a Message
63 | `add_message` adds a message to the vector of `messages` and marks it as premium if the user attached more than `0.1 NEAR`.
64 |
65 | `add_message` is a payable method for which can only be invoked using a NEAR account. The account needs to attach money and pay GAS for the transaction.
66 |
67 | ```bash
68 | # Use near-cli to donate 1 NEAR
69 | near call add_message '{"text": "a message"}' --amount 0.1 --accountId
70 | ```
71 |
72 | **Tip:** If you would like to add a message using your own account, first login into NEAR using:
73 |
74 | ```bash
75 | # Use near-cli to login your NEAR account
76 | near login
77 | ```
78 |
79 | and then use the logged account to sign the transaction: `--accountId `.
80 |
--------------------------------------------------------------------------------
/frontend/global.scss:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | html {
6 | --bg: #f4f4f4;
7 | --fg: #25282A;
8 | --gray: #888;
9 | --royal: #0072CE;
10 | --blue: #6AD1E3;
11 | --primary: #93b0df;
12 | --secondary: var(--royal);
13 | --tertiary: #FF585D;
14 |
15 | background-color: var(--bg);
16 | color: var(--fg);
17 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;
18 | font-size: calc(.65em + 0.7vw);
19 | line-height: 1.3;
20 |
21 | ::selection {
22 | background: var(--secondary);
23 | color: var(--bg);
24 | }
25 |
26 | @media (prefers-color-scheme: dark) {
27 | --bg: #25282A;
28 | --fg: #fff;
29 | --secondary: var(--blue);
30 |
31 | ::selection {
32 | background: var(--secondary);
33 | color: var(--fg);
34 | }
35 | }
36 | }
37 |
38 | body {
39 | margin: 0 auto;
40 | padding: 0 1em;
41 | max-width: 40em;
42 | }
43 |
44 | fieldset {
45 | border: none;
46 | margin: 0;
47 | padding: 0;
48 | }
49 |
50 | .highlight {
51 | align-items: center;
52 | display: flex;
53 | margin-bottom: 0.5em;
54 | width: 100%;
55 | label {
56 | margin-right: 0.5em;
57 | }
58 | input {
59 | caret-color: var(--secondary);
60 | }
61 | }
62 |
63 | label {
64 | color: var(--gray);
65 | }
66 |
67 | button, .highlight {
68 | border-radius: 5px;
69 | border-color: var(--primary);
70 | border: 0.1em solid var(--primary);
71 | padding: 0.5em 1em;
72 |
73 | &:hover, &:focus, &:focus-within {
74 | border-color: var(--secondary);
75 | }
76 | }
77 |
78 | input {
79 | border: none;
80 | flex: 1;
81 | &:read-only {
82 | color: var(--primary)
83 | }
84 | }
85 |
86 | input[type="number"] {
87 | text-align: center;
88 | border-bottom: 0.1em solid var(--primary);
89 | margin: 0 1em;
90 | width: 4em;
91 | padding-left: 0.5em;
92 | &:hover, &:focus {
93 | border-color: var(--secondary);
94 | }
95 | }
96 |
97 | button, input {
98 | background: transparent;
99 | color: inherit;
100 | cursor: pointer;
101 | font: inherit;
102 | outline: none;
103 | }
104 |
105 | button {
106 | position: relative;
107 | transition: top 50ms;
108 | &:hover, &:focus {
109 | top: -1px;
110 | }
111 | background: var(--primary);
112 |
113 | &:active {
114 | background: var(--secondary);
115 | border-color: var(--secondary);
116 | top: 1px;
117 | }
118 | }
119 |
120 | .is-premium {
121 | border-left: 0.25em solid var(--secondary);
122 | padding-left: 0.25em;
123 | margin-left: -0.5em;
124 | }
125 |
126 | table button{
127 | margin-left: 1rem;
128 | }
--------------------------------------------------------------------------------
/frontend/.cypress/e2e/guest-book.cy.ts:
--------------------------------------------------------------------------------
1 | // Load string from environment variable
2 | const SEED = Cypress.env('seed')
3 |
4 | context("Main Page", () => {
5 | beforeEach(() => {
6 | cy.visit("/");
7 | });
8 |
9 | it("should display the guest book", () => {
10 | cy.get("h1").contains("NEAR Guest Book");
11 | });
12 |
13 | it("should log in and sign message with MyNEARWallet", () => {
14 | // generate a random short string
15 | const messageString = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
16 |
17 | // Log in with NEAR Wallet by clicking on the "Log in" button
18 | cy.get("button").contains("Log in").click();
19 | // Select element from left modal list titled: "MyNearWallet" and click on it
20 | cy.get("div").contains("MyNearWallet").click();
21 | // Wait for new page to load
22 | cy.wait(5000);
23 | // Click on the "Import Existing Account" button
24 | cy.get("button").contains("Import Existing Account").click();
25 | // Click on the "Recover Account" button
26 | cy.get("button").contains("Recover Account").click();
27 | // Fill in SEED from the environment variable into the input field
28 | cy.get("input").type(SEED);
29 | // Click on the "Find My Account" button
30 | cy.get("button").contains("Find My Account").click();
31 | // Wait for new page to load
32 | cy.wait(10000);
33 | // Click on the "Next" button
34 | cy.get("button").contains("Next").click();
35 | // Click on the "Connect" button
36 | cy.get("button").contains("Connect").click();
37 | // Wait for new page to load
38 | cy.wait(10000);
39 | // Check if the "Log out" and "Sign" buttons are visible
40 | cy.get("button").contains("Log out").should("be.visible");
41 | // Check if there is an input field with the label "Message:" and id="message"
42 | cy.get("label").contains("Message:").should("be.visible");
43 | // Check if there is a button with the label "Sign"
44 | cy.get("button").contains("Sign").should("be.visible");
45 | // Check if there is a number input field for donations with a 0 minimum and 0.01 step
46 | cy.get("input[type=number]").should("have.attr", "min", "0").and("have.attr", "step", "0.01");
47 | // Fill in the "Message:" labelled input field with id="donation" with the text from the messageString variable
48 | cy.get("input[id=message]").type(messageString);
49 | // Set the donation amount to 0.01
50 | cy.get("input[id=donation]").type("0.01");
51 | // Click on the "Sign" button
52 | cy.get("button").contains("Sign").click();
53 | // Wait for new page to load
54 | cy.wait(10000);
55 | // Click on the "Approve" button
56 | cy.get("button").contains("Approve").click();
57 | // Wait for new page to load
58 | cy.wait(10000);
59 | // Check if the messageString variable is visible in the "Messages" section
60 | cy.get("div").contains(messageString).should("be.visible");
61 | });
62 | });
--------------------------------------------------------------------------------
/frontend/near-wallet.js:
--------------------------------------------------------------------------------
1 | /* A helper file that simplifies using the wallet selector */
2 |
3 | // near api js
4 | import { providers } from 'near-api-js';
5 |
6 | // wallet selector UI
7 | import '@near-wallet-selector/modal-ui/styles.css';
8 | import { setupModal } from '@near-wallet-selector/modal-ui';
9 | import LedgerIconUrl from '@near-wallet-selector/ledger/assets/ledger-icon.png';
10 | import MyNearIconUrl from '@near-wallet-selector/my-near-wallet/assets/my-near-wallet-icon.png';
11 |
12 | // wallet selector options
13 | import { setupWalletSelector } from '@near-wallet-selector/core';
14 | import { setupLedger } from '@near-wallet-selector/ledger';
15 | import { setupMyNearWallet } from '@near-wallet-selector/my-near-wallet';
16 |
17 | const THIRTY_TGAS = '30000000000000';
18 | const NO_DEPOSIT = '0';
19 |
20 | // Wallet that simplifies using the wallet selector
21 | export class Wallet {
22 | walletSelector;
23 | wallet;
24 | network;
25 | createAccessKeyFor;
26 |
27 | constructor({ createAccessKeyFor = undefined, network = 'testnet' }) {
28 | // Login to a wallet passing a contractId will create a local
29 | // key, so the user skips signing non-payable transactions.
30 | // Omitting the accountId will result in the user being
31 | // asked to sign all transactions.
32 | this.createAccessKeyFor = createAccessKeyFor
33 | this.network = network
34 | }
35 |
36 | // To be called when the website loads
37 | async startUp() {
38 | this.walletSelector = await setupWalletSelector({
39 | network: this.network,
40 | modules: [setupMyNearWallet({ iconUrl: MyNearIconUrl }),
41 | setupLedger({ iconUrl: LedgerIconUrl })],
42 | });
43 |
44 | const isSignedIn = this.walletSelector.isSignedIn();
45 |
46 | if (isSignedIn) {
47 | this.wallet = await this.walletSelector.wallet();
48 | this.accountId = this.walletSelector.store.getState().accounts[0].accountId;
49 | }
50 |
51 | return isSignedIn;
52 | }
53 |
54 | // Sign-in method
55 | signIn() {
56 | const description = 'Please select a wallet to sign in.';
57 | const modal = setupModal(this.walletSelector, { contractId: this.createAccessKeyFor, description });
58 | modal.show();
59 | }
60 |
61 | // Sign-out method
62 | signOut() {
63 | this.wallet.signOut();
64 | this.wallet = this.accountId = this.createAccessKeyFor = null;
65 | window.location.replace(window.location.origin + window.location.pathname);
66 | }
67 |
68 | // Make a read-only call to retrieve information from the network
69 | async viewMethod({ contractId, method, args = {} }) {
70 | const { network } = this.walletSelector.options;
71 | const provider = new providers.JsonRpcProvider({ url: network.nodeUrl });
72 |
73 | let res = await provider.query({
74 | request_type: 'call_function',
75 | account_id: contractId,
76 | method_name: method,
77 | args_base64: Buffer.from(JSON.stringify(args)).toString('base64'),
78 | finality: 'optimistic',
79 | });
80 | return JSON.parse(Buffer.from(res.result).toString());
81 | }
82 |
83 | // Call a method that changes the contract's state
84 | async callMethod({ contractId, method, args = {}, gas = THIRTY_TGAS, deposit = NO_DEPOSIT }) {
85 | // Sign a transaction with the "FunctionCall" action
86 | const outcome = await this.wallet.signAndSendTransaction({
87 | signerId: this.accountId,
88 | receiverId: contractId,
89 | actions: [
90 | {
91 | type: 'FunctionCall',
92 | params: {
93 | methodName: method,
94 | args,
95 | gas,
96 | deposit,
97 | },
98 | },
99 | ],
100 | });
101 |
102 | return providers.getTransactionLastResult(outcome)
103 | }
104 |
105 | // Get transaction result from the network
106 | async getTransactionResult(txhash) {
107 | const { network } = this.walletSelector.options;
108 | const provider = new providers.JsonRpcProvider({ url: network.nodeUrl });
109 |
110 | // Retrieve transaction result from the network
111 | const transaction = await provider.txStatus(txhash, 'unnused');
112 | return providers.getTransactionLastResult(transaction);
113 | }
114 | }
--------------------------------------------------------------------------------