从0开始构造一个near的dapp:
连接钱包
获得accountId和keyPair
调用合约方法
准备 首先通过npm安装near-api-js
:
然后准备配置项:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import {keyStores} from "near-api-js" ;export const environment = { nearWalletConfig : { keyStore : new keyStores.BrowserLocalStorageKeyStore (), contractName : 'asset-manager.orderly-dev.testnet' , methodNames : ['user_announce_key' , 'user_request_set_trading_key' , 'create_user_account' ], networkId : process.env .NODE_ENV === 'development' ? 'testnet' : 'NEAR_NETWORK_ENV' , nodeUrl : 'https://rpc.testnet.near.org' , walletUrl : 'https://wallet.testnet.near.org' , helperUrl : 'https://helper.testnet.near.org' , explorerUrl : 'https://explorer.testnet.near.org' , headers : {}, connectCallback : { success : '' , failure : '' , }, }, }
KeyStore
是当connect钱包后,存储密钥的地方。
连接near钱包 构建连接:
1 2 3 4 5 6 7 8 9 10 11 12 import {connect, WalletConnection } from "near-api-js" ;import {environment} from "./environment/environment" ;const nearConfig = environment.nearWalletConfig ;const nearConnection = await connect (nearConfig);const connection = new WalletConnection (nearConnection);if (!connection.isSignedIn ()) { walletConnection?.requestSignIn (nearConfig) } else { const accountId = walletConnection.getAccountId (); console .log ('accountId' , accountId); }
运行这个程序的时候,当检测到未连接的时候,就会跳转到钱包页面去让用户授权连接,连接成功后跳转回我们的页面,我们就可以拿到当前连接钱包的accountId。
封装 ConnctionContext 上面是连接钱包程序的总览,之后的代码会多次用到accountId和当前的连接对象connection
。所以我们来封装为一个Context。
看一下结构,我们的ConnectionContext大致为以下结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const ConnectionContext = React .createContext <ConnectionContextValue | null >(null )export const ConnectionContextProvider = (children ) => { return ( <ConnectionContext.Provider value ={providerValue} > {children} </ConnectionContext > ) } export function useConnection ( ) { const context = useContext (ConnectionContext ); if (!context) { throw new Error ('useConnection must be used within a ConnectionContextProvider' ); } return context; }
先定义一些类型:
1 2 3 4 5 6 7 8 9 10 interface ConnectionContextProviderProps { children : any; } interface ConnectionContextValue { accountId : string | null ; walletConnection : WalletConnection | null ; nearConfig : typeof environment.nearWalletConfig ; }
然后我们来完善ConnectionContextProvider
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import React , {useCallback, useContext, useEffect, useMemo, useState} from "react" ;import {connect, WalletConnection } from "near-api-js" ;import {environment} from "./environment/environment" ;import {LoadingComponent } from "./loading.component" ;import {NearConfig } from "near-api-js/lib/near" ;export const ConnectionContextProvider = ({children}: ConnectionContextProviderProps ) => { const [isLoading, setLoading] = useState<boolean>(true ); const [accountId, setAccountId] = useState<string | null >(null ); const [walletConnection, setWalletConnection] = useState<WalletConnection | null >(null ); const [walletUrl, setWalletUrl] = useState<string>('https://testnet.mynearwallet.com' ) const [nearConfig, setNearConfig] = useState<any>() const init = useCallback (async () => { const nearConfig = environment.nearWalletConfig ; nearConfig.walletUrl = walletUrl; setNearConfig (nearConfig); const nearConnection = await connect (nearConfig); let appKeyPrefix = 'near-app' ; const connection = new WalletConnection (nearConnection, appKeyPrefix); setWalletConnection (connection); console .log ('is sign in' , connection.isSignedIn ()); if (connection.isSignedIn ()) { setAccountId (connection.getAccountId ()); } setLoading (false ) }, []); useEffect (() => { init ().then (); }, [init]) const providerValue = useMemo (() => ({ accountId, walletConnection, nearConfig, }), [accountId, walletConnection, nearConfig]) return ( <ConnectionContext.Provider value ={providerValue} > {isLoading ? <LoadingComponent /> : children } </ConnectionContext.Provider > ) }
PS: 我们这里连接的钱包换为myNearWallet
。myNearWallet
在链接成功后会将key存放在localstroage里面的near-app_wallet_auth_key
。要想获得这个key,只需要在new WalletConnection(nearConnection, appKeyPrefix);
的时候声明appKeyPrefix
为near-app
就行了。
这里顺便总结下如果连接near-wallet和myNearWallet的区别: near-wallet:
walletUrl 为 https://wallet.testnet.near.org
KeyStore 要初始化。keyStore: new keyStores.BrowserLocalStorageKeyStore()
myNearWallet:
walletUrl 为 https://testnet.mynearwallet.com
KeyStore 要初始化。keyStore: new keyStores.BrowserLocalStorageKeyStore()
需要在建立连接的地方设置appKeyPrefix
为near-app
:new WalletConnection(nearConnection,'near-app');
然后在App.tsx
中使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import React from 'react' ;import './App.css' ;import {ConnectionContextProvider } from "./ConnectionContext" ;import {HomePage } from "./home.page" ;function App ( ) { return ( <div className ="App" > <ConnectionContextProvider > <HomePage /> </ConnectionContextProvider > </div > ); } export default App ;
然后,我们可以在home.page.tsx
中以是否能获取到accountId来判断是否已连接:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import {useConnection} from "./ConnectionContext" ;import {ConnectComponent } from "./connect.component" ;import {AccountInfoComponent } from "./account-info.component" ;export function HomePage ( ) { const {accountId} = useConnection (); return ( <div > { accountId ? <AccountInfoComponent /> : <ConnectComponent /> } </div > ) }
如果没有accountId,就说明没有连接,然后我们可以控制connect.component.tsx
来连接钱包:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import React from "react" ;import {useConnection} from "./ConnectionContext" ;import {environment} from "./environment/environment" ;export const ConnectComponent = ( ) => { const {walletConnection} = useConnection (); const onConnect = ( ) => { walletConnection?.requestSignIn ({ contractId : environment.nearWalletConfig .contractName , methodNames : environment.nearWalletConfig .methodNames , }); } return ( <div > <button onClick ={onConnect} > connect</button > </div > ) }
如果已连接,就用account-info.component.tsx
来展示账号:
1 2 3 4 5 6 7 8 9 10 import {useConnection} from "./ConnectionContext" ;export function AccountInfoComponent ( ) { const {accountId} = useConnection (); return ( <div > <p > accountId: {accountId} </p > </div > ) }
获得账户的near的金额 前面我们已经连接了钱包,现在看看如何获得钱包的余额。
我们来构造一个contract.service.ts
文件存放和钱包合约交互的代码。
near的token余额是24位精度的,我们需要使用bignumber.js来格式化金额,所以先安装bignumber.js
:
查询账户余额是查询行为,我们可以用near-api-js提供的provider.JsonRpcProvider.query
方法来查询:
1 2 3 4 5 6 7 8 9 10 11 12 13 import {providers} from "near-api-js" ;import {AccountView } from "near-api-js/lib/providers/provider" ;import {environment} from "../environment/environment" ;export const getNearBalance = async (accountId: string ) => { const provider = new providers.JsonRpcProvider ({ url : environment.nearWalletConfig .nodeUrl }); const balance = await provider.query <AccountView >({ request_type : 'view_account' , account_id : accountId, finality : 'optimistic' , }); return balance.amount ; };
然后在account-info.component.tsx
中使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import {useConnection} from "./ConnectionContext" ;import {useEffect, useState} from "react" ;import {getNearBalance} from "./services/contract.service" ;import BigNumber from "bignumber.js" ;export function AccountInfoComponent ( ) { const {accountId} = useConnection (); const [nearBalance, setNearBalance] = useState<string | null >(null ); useEffect (() => { getNearBalance (accountId!).then (res => { setNearBalance (new BigNumber (res).shiftedBy (-24 ).toFixed (8 )) }) }, []) return ( <div > <p > accountId: {accountId} </p > <p > near balance: {nearBalance} near</p > </div > ) }
调用合约方法 合约有user_announce_key
方法,我们需要在connect之后调用这个方法。该如何调用?
near合约有viewMethods
和changeMethods
两种类型的方法,viewMethods
是查询类型的方法,changeMethods
是操作类型的方法。
user_announce_key
方法是将key设置到合约里面,这个是changeMethod
。
Account.functionCall() 我们可以用Account.functionCall
去调用changeMethod
方法,但是这需要实例化的Account
。
我们修改前面的ConnectionContextProvider
方法,将account
对象暴露出来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 interface ConnectionContextValue { account : Account | null ; } export const ConnectionContextProvider = ({children}: ConnectionContextProviderProps ) => { const [account, setAccount] = useState<Account | null >(null ) if (connection.isSignedIn ()) { const nearAccount = await nearConnection.account (connection.getAccountId ()); setAccount (nearAccount) } const providerValue = useMemo (() => ({ accountId, walletConnection, nearConfig, account, }), [accountId, walletConnection, nearConfig, account]) }
然后在contract.service.ts
中添加方法:
1 2 3 4 5 6 7 8 9 10 const MAX_GAS = '300000000000000' ;export const setAnnounceKey = (account: Account ) => { return account.functionCall ({ contractId : environment.nearWalletConfig .contractName , methodName : 'user_announce_key' , args : {}, gas : MAX_GAS , }); }
然后在account-info.component.tsx
中调用这个方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import {useConnection} from "./ConnectionContext" ;import {useEffect, useState} from "react" ;import {getNearBalance, setAnnounceKey} from "./services/contract.service" ;import BigNumber from "bignumber.js" ;export function AccountInfoComponent ( ) { const {accountId, account} = useConnection (); const onAnnounceKey = ( ) => { setAnnounceKey (account!).then (res => { console .log ('announce key res' , res); }); } return ( <div > <button onClick ={onAnnounceKey} > announce key</button > </div > ) }
点击这个announce key
按钮,就会发送transaction到合约,具体可以在explorer里面查询到这个签名:
以合约的实例调用 我们可以通过构造一个合约对象的方式,去调用合约方法:
1 2 3 4 5 6 7 8 export const setAnnounceKey = (account: Account ) => { const contract = new Contract (account, environment.nearWalletConfig .contractName , { viewMethods : [], changeMethods : ['user_announce_key' ], }); return contract.user_announce_key (); }
以walletConnection的方式调用 我们也可以使用WalletConnection.requestSignTransaction
方法来调用合约方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 export const setAnnounceKey = async (walletConnection: WalletConnection ) => { const accountId = walletConnection.getAccountId (); const provider = new providers.JsonRpcProvider ({url : environment.nearWalletConfig .nodeUrl }); const keyPair = await environment.nearWalletConfig .keyStore .getKey (environment.nearWalletConfig .networkId , accountId); const publicKey = keyPair.getPublicKey (); const accessKey = await provider.query <AccessKeyViewRaw >( `access_key/${accountId} /${publicKey.toString()} ` , '' ); console .log (accessKey); const nonce = ++accessKey.nonce ; const recentBlockHash = serialize.base_decode (accessKey.block_hash ); const transactions : Transaction [] = []; transactions.push (createTransaction ( walletConnection.getAccountId (), publicKey, environment.nearWalletConfig .contractName , nonce, [ functionCall ( 'user_announce_key' , {}, MAX_GAS , 0 , ) ], recentBlockHash, )) return walletConnection.requestSignTransactions ({ transactions, }) }
使用requestSignTransactions
这种方式调用比较复杂,可以看到我们通过query方式查询了当前key的nonce
和blockHash
,然后构造Transaction
,可以传递Transaction
数组,这样当需要batch多个transaction的时候非常好用。
但需要注意的是,这种调用方式会强制让用户签名。比如我们这里使用myNearWallet的时候会跳转到钱包页面去手动签名。
特意留意一下获得accessKey返回的数据:
1 2 3 4 block_hash block_height nonce permission : {FunctionCall : {allowance, method_names, receiver_id}}
可以看到这里能查询到当前keyPair的权限。
PS:但是很奇怪,发现目前创建的key是没有设置好method_names。好像之前是没这个问题的。why?但这个不影响使用。