Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Near wallet integration #5

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
562 changes: 552 additions & 10 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
"@dnd-kit/core": "^6.1.0",
"d3-force": "^3.0.0",
"echarts": "^5.5.0",
"@near-wallet-selector/core": "^8.9.7",
"@near-wallet-selector/modal-ui": "^8.9.7",
"@near-wallet-selector/my-near-wallet": "^8.9.7",
"near-api-js": "^1.1.0",
"next": "14.2.3",
"react": "^18",
"react-dom": "^18",
Expand All @@ -30,4 +34,4 @@
"typescript": "^5"
},
"prettier": {}
}
}
53 changes: 44 additions & 9 deletions src/app/components/header.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,59 @@
'use client'

import Image from "next/image";
import logo from "@/images/logo.png";
import { NearContext } from "../contexts/WalletContext";
import { useContext, useEffect, useState } from "react";
import "./header.scss";
import { Dao } from "@/domains/dao";
import Toggle from "./toggle";

interface Props {
dao?: Dao;
dao?: Dao;
setBerlin: (b: boolean) => void;
berlin: boolean;
}

export default function Header({ dao }: Props) {
export default function Header({ dao, setBerlin, berlin }: Props) {
const { signedAccountId, wallet } = useContext(NearContext);
const [action, setAction] = useState(() => { });
const [label, setLabel] = useState('Loading...');

useEffect(() => {
if (!wallet) return;

if (signedAccountId) {
setAction(() => (wallet as any).signOut);
setLabel(`Logout ${signedAccountId}`);
} else {
setAction(() => (wallet as any).signIn);
setLabel('Login');
}
}, [signedAccountId, wallet]);

return (
<div className="h-[calc(56px+10px+10px)]">
<div className="h-[calc(56px+10px+10px)] bg-transparent">
<header>
<Image src={logo} alt="" height={56} />
<h1>Governance Gladiators</h1>
{ dao &&
<div className="flex flex-row justify-between bg-transparent">
<div>
<Image src={logo} alt="" height={56} />
<h1>Governance Gladiators</h1>
<button clasName="btn btn-secondary float-right pt-4" onClick={action as any}>{label}</button>
</div>
{dao && (
<div className="welcome">
<img className="dao-logo" src={dao.logo} alt="DAO Logo" />
<h3>Welcome to {dao.name} Arena</h3>
<img className="dao-logo" src={dao.logo} alt="DAO Logo" />
<h3>Welcome to {dao.name} Arena</h3>
</div>
}
)}

<Toggle
leftLabel="Berlin"
rightLabel="Arena"
onChange={(s) => setBerlin(s == "left")}
state={berlin ? "left" : "right"}
/>
</div>
</header>
</div>
);
Expand Down
16 changes: 16 additions & 0 deletions src/app/contexts/WalletContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { createContext } from 'react';

/**
* @typedef NearContext
* @property {import('./wallets/near').Wallet} wallet Current wallet
* @property {string} signedAccountId The AccountId of the signed user
*/

/** @type {import ('react').Context<NearContext>} */
export const NearContext = createContext<{
wallet: import('../wallets/near').Wallet | undefined,
signedAccountId: string
}>({
wallet: undefined,
signedAccountId: ''
});
22 changes: 15 additions & 7 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,33 @@
import type { Metadata } from "next";
'use client';

import { Inter } from "next/font/google";
import "./globals.css";
import Header from "@/app/components/header";
import { DelegateContextProvider } from "@/providers/stateProvider";
import { NearContext } from "./contexts/WalletContext";
import { Wallet } from "./wallets/near";
import { useEffect, useState } from "react";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
const wallet = new Wallet({ createAccessKeyFor: 'bryanfullam.testnet', networkId: 'testnet' });

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const [signedAccountId, setSignedAccountId] = useState('');

useEffect(() => { wallet.startUp(setSignedAccountId) }, []);

return (
<html lang="en">
<html lang="en" className="h-full">
<body className={inter.className}>
<DelegateContextProvider>{children}</DelegateContextProvider>
<NearContext.Provider value={{ wallet, signedAccountId }}>
<Header />
<DelegateContextProvider>{children}</DelegateContextProvider>
</NearContext.Provider>
</body>
</html>
);
Expand Down
2 changes: 0 additions & 2 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
"use client";

import Image from "next/image";
// import Daos from "@/app/daos/daos";
import Daos from "@/app/daosArena/daos";
// import Proposal from "@/app/proposal/proposal";
import Proposal from "@/app/proposalArena/proposal";
import React, { useEffect, useState } from "react";
import type { Dao } from "@/domains/dao";
Expand Down
141 changes: 141 additions & 0 deletions src/app/wallets/near.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// near api js
import { providers } from 'near-api-js';

// wallet selector
import { distinctUntilChanged, map } from 'rxjs';
import '@near-wallet-selector/modal-ui/styles.css';
import { setupModal } from '@near-wallet-selector/modal-ui';
import { setupWalletSelector } from '@near-wallet-selector/core';
import { setupMyNearWallet } from '@near-wallet-selector/my-near-wallet';

const THIRTY_TGAS = '30000000000000';
const NO_DEPOSIT = '0';

export class Wallet {
/**
* @constructor
* @param {Object} options - the options for the wallet
* @param {string} options.networkId - the network id to connect to
* @param {string} options.createAccessKeyFor - the contract to create an access key for
* @example
* const wallet = new Wallet({ networkId: 'testnet', createAccessKeyFor: 'contractId' });
* wallet.startUp((signedAccountId) => console.log(signedAccountId));
*/
constructor({ networkId = 'testnet', createAccessKeyFor = undefined }) {
this.createAccessKeyFor = createAccessKeyFor;
this.networkId = networkId;
}

/**
* To be called when the website loads
* @param {Function} accountChangeHook - a function that is called when the user signs in or out#
* @returns {Promise<string>} - the accountId of the signed-in user
*/
startUp = async (accountChangeHook) => {
this.selector = setupWalletSelector({
network: this.networkId,
modules: [setupMyNearWallet()]
});

const walletSelector = await this.selector;
const isSignedIn = walletSelector.isSignedIn();
const accountId = isSignedIn ? walletSelector.store.getState().accounts[0].accountId : '';

walletSelector.store.observable
.pipe(
map(state => state.accounts),
distinctUntilChanged()
)
.subscribe(accounts => {
const signedAccount = accounts.find((account) => account.active)?.accountId;
accountChangeHook(signedAccount);
});

return accountId;
};

/**
* Displays a modal to login the user
*/
signIn = async () => {
const modal = setupModal(await this.selector, { contractId: this.createAccessKeyFor });
modal.show();
};

/**
* Logout the user
*/
signOut = async () => {
const selectedWallet = await (await this.selector).wallet();
selectedWallet.signOut();
};

/**
* Makes a read-only call to a contract
* @param {Object} options - the options for the call
* @param {string} options.contractId - the contract's account id
* @param {string} options.method - the method to call
* @param {Object} options.args - the arguments to pass to the method
* @returns {Promise<JSON.value>} - the result of the method call
*/
viewMethod = async ({ contractId, method, args = {} }) => {
const url = `https://rpc.${this.networkId}.near.org`;
const provider = new providers.JsonRpcProvider({ url });

let res = await provider.query({
request_type: 'call_function',
account_id: contractId,
method_name: method,
args_base64: Buffer.from(JSON.stringify(args)).toString('base64'),
finality: 'optimistic',
});
return JSON.parse(Buffer.from(res.result).toString());
};


/**
* Makes a call to a contract
* @param {Object} options - the options for the call
* @param {string} options.contractId - the contract's account id
* @param {string} options.method - the method to call
* @param {Object} options.args - the arguments to pass to the method
* @param {string} options.gas - the amount of gas to use
* @param {string} options.deposit - the amount of yoctoNEAR to deposit
* @returns {Promise<Transaction>} - the resulting transaction
*/
callMethod = async ({ contractId, method, args = {}, gas = THIRTY_TGAS, deposit = NO_DEPOSIT }) => {
// Sign a transaction with the "FunctionCall" action
const selectedWallet = await (await this.selector).wallet();
const outcome = await selectedWallet.signAndSendTransaction({
receiverId: contractId,
actions: [
{
type: 'FunctionCall',
params: {
methodName: method,
args,
gas,
deposit,
},
},
],
});

return providers.getTransactionLastResult(outcome);
};

/**
* Makes a call to a contract
* @param {string} txhash - the transaction hash
* @returns {Promise<JSON.value>} - the result of the transaction
*/
getTransactionResult = async (txhash) => {
const walletSelector = await this.selector;
const { network } = walletSelector.options;
const provider = new providers.JsonRpcProvider({ url: network.nodeUrl });

// Retrieve transaction result from the network
const transaction = await provider.txStatus(txhash, 'unnused');
return providers.getTransactionLastResult(transaction);
};
}
Loading