0%

构造Near Dapp

从0开始构造一个near的dapp:

  1. 连接钱包
  2. 获得accountId和keyPair
  3. 调用合约方法

准备

首先通过npm安装near-api-js

1
npm i 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
// 声明一个contrext上下文。
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: 我们这里连接的钱包换为myNearWalletmyNearWallet在链接成功后会将key存放在localstroage里面的near-app_wallet_auth_key。要想获得这个key,只需要在new WalletConnection(nearConnection, appKeyPrefix);的时候声明appKeyPrefixnear-app就行了。

这里顺便总结下如果连接near-wallet和myNearWallet的区别:
near-wallet:

  1. walletUrl 为 https://wallet.testnet.near.org
  2. KeyStore 要初始化。keyStore: new keyStores.BrowserLocalStorageKeyStore()

myNearWallet:

  1. walletUrl 为 https://testnet.mynearwallet.com
  2. KeyStore 要初始化。keyStore: new keyStores.BrowserLocalStorageKeyStore()
  3. 需要在建立连接的地方设置appKeyPrefixnear-appnew 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

1
npm i 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合约有viewMethodschangeMethods两种类型的方法,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里面查询到这个签名:
near dapp transaction

以合约的实例调用

我们可以通过构造一个合约对象的方式,去调用合约方法:

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'],
});
// @ts-ignore
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的nonceblockHash,然后构造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?但这个不影响使用。

码字辛苦,打赏个咖啡☕️可好?💘