Ex05: Wallet

In this exercise we connect Ethereum and NEAR wallets to a dapplet using Core.wallet interface.

The dapplet that we implement will send some amount of tokens from your wallet to itself.

Here is the initial code for this example: ex05-wallet-exercise.

1. Get the wallet object and common variables#

The Core.wallet() method receives config with one parameter - authMetods and returns Promise. AuthMethods is an array of strings that are combinations of chain and net names. At the moment three combinations are available: ethereum/goerli, near/testnet and near/mainnet. You can define one or more combinations if you want users to choose the chain and the net they want. However, you have to understand that APIs of different chains are not equal. So when you provide a choice your code becomes more complex. Let's consider this situation and get the ethereum/goerli or near/testnet wallet:

const wallet = await Core.wallet({ authMethods: ['ethereum/goerli', 'near/testnet'] });

On this stage we are not connected to any wallet so our wallet is typed with IEthWallet | INearWallet interfaces. Each of them consists of chain-specific API and WalletConnection interface.

WalletConnection contains authMethod parameter and three methods: isConnected(), connect() and disconnect(). wallet.authMethod appears after connecting to a wallet and shows which one is connected.

We also have to set transfer amounts of ETH and NEAR. It's convenient to define appropriate values in the config and get them in the dapplet. In this case the user would set any amounts they like.

1// ./config/default.json
2{
3 "main": {
4 "transferAmountEth": 0.001,
5 "transferAmountNear": 0.5
6 },
7 "test": {
8 "transferAmountEth": 0.001,
9 "transferAmountNear": 0.5
10 },
11 "dev": {
12 "transferAmountEth": 0.001,
13 "transferAmountNear": 0.5
14 }
15}
1// ./config/schema.json
2{
3 "type": "object",
4 "required": ["transferAmountEth", "transferAmountNear"],
5 "properties": {
6 "transferAmountEth": {
7 "type": "number",
8 "title": "Transfer amount Ethereum"
9 },
10 "transferAmountNear": {
11 "type": "number",
12 "title": "Transfer amount NEAR"
13 }
14 }
15}
// ./src/index.ts
const transferAmountEth: number = await Core.storage.get('transferAmountEth')
const transferAmountNear: number = await Core.storage.get('transferAmountNear');

At last define the variable to save and reuse Ethereum addresses that we will get from the connected Ethereum wallet.

let currentEthAddresses: string[];

2. Define the button states#

There is a button in the POST. We want the wallet to connect when the button is clicked, to see the transfer amount and currency near the button, and on second click to send the tokens. We consider a few states of the button besides the DEFAULT state. Look at the scheme that describes all the states.

Button states

Firstly, we need to describe button with a label. But we remember that wallets' APIs are different. We have two ways: create a CONNECTED state and make several forks for Ethereum and NEAR wallets inside, or create two separate states. Let's choose the second option and create ETH_CONNECTED and NEAR_CONNECTED states.

1ETH_CONNECTED: {
2 label: `Send ${transferAmountEth} ETH`,
3 img: EXAMPLE_IMG,
4 loading: false,
5 // LP: 4. Send the necessary data to Ethereum wallet and listen to the response
6 exec: async (_, me) => {
7
8 },
9 //
10},
11NEAR_CONNECTED: {
12 label: `Send ${transferAmountNear} NEAR`,
13 img: EXAMPLE_IMG,
14 loading: false,
15 // LP: 5. Send the necessary data to NEAR wallet and listen to the response
16 exec: async (_, me) => {
17
18 },
19 //
20},

We implement the logic of sending transactions later, after creating all the states.

Next we want to see if the transaction succeeded or failed. Accordingly create COMPLETED and FAILURE states.

1COMPLETED: {
2 label: 'Completed',
3 img: EXAMPLE_IMG,
4 loading: false,
5 exec: (_, me) => {
6 me.state = wallet.authMethod === 'ethereum/goerli'
7 ? 'ETH_CONNECTED'
8 : 'NEAR_CONNECTED';
9 },
10},
11FAILURE: {
12 label: 'Failure',
13 img: EXAMPLE_IMG,
14 loading: false,
15 exec: (_, me) => {
16 me.state = wallet.authMethod === 'ethereum/goerli'
17 ? 'ETH_CONNECTED'
18 : 'NEAR_CONNECTED';
19 },
20},

These states appear after successful wallet connections, so wallets have authMetod value.

We can also reject any transaction. So we need the REJECTED state.

1REJECTED: {
2 label: 'Rejected',
3 img: EXAMPLE_IMG,
4 loading: false,
5 exec: async (_, me) => {
6 me.state = await wallet.isConnected()
7 ? wallet.authMethod === 'ethereum/goerli'
8 ? 'ETH_CONNECTED'
9 : 'NEAR_CONNECTED'
10 : 'DEFAULT';
11 },
12},

The other two states PENDING and MINING, are for when the button is waiting for a transaction to be approved or mined. These states are intermediate and the button has to be disabled, thats why they have no exec functions and the loading perameter is true. The last one disables the button and shows the loader instead of the picture on it.

1PENDING: {
2 label: 'Pending',
3 loading: true,
4 exec: null,
5},
6MINING: {
7 label: 'Mining',
8 loading: true,
9 exec: null,
10},

3. Connect the wallet#

We've defined all of the button's states and now we are going back to the DEFAULT. Here we have to add the button click action that connects the wallet.

This is a wallet.connect() method. It returns Promise<void>.

1exec: async (_, me) => {
2 me.state = 'PENDING';
3 try {
4 await wallet.connect();
5 } catch (err) {
6 console.log('Login ERROR:', err)
7 me.state = 'REJECTED';
8 return;
9 }
10 me.state = wallet.authMethod === 'ethereum/goerli' ? 'ETH_CONNECTED' : 'NEAR_CONNECTED';
11},

After a successful connection the button switches the state to ETH_CONNECTED' or NEAR_CONNECTED depending on which wallet has been chosen.

If the connection was cancelled we switch the state to REJECTED.

4. Send the necessary data to Ethereum wallet and listen to the response#

Firstly, add a check of the wallet.authMethod. If we implement the states' switching correctly it will always be ethereum/goerli. But if we make a mistake the check will save us from an error. Also, after the check the typescript compiler will understand exactly the type of the wallet we use.

1exec: async (_, me) => {
2 if (wallet.authMethod === 'ethereum/goerli') {
3 /*
4 ...
5 */
6 } else if (wallet.authMethod === 'near/testnet') {
7 me.state = 'NEAR_CONNECTED';
8 } else {
9 me.state = 'DEFAULT';
10 }
11},

Switch the button's state to PENDING.

me.state = 'PENDING';

If we haven't got the currentEthAddresses yet, we need to get it using the wallet.request() method. This method is used for all requests to the Ethereum wallet. It recieves a config with two required parameters: method and params. The method parameter of the string type is one of the Ethereum JSON-RPC methods. params is an array of parameters that are passed to the method.

We use the eth_accounts JSON-RPC method with an empty array as the params value.

1if (!currentEthAddresses) {
2 try {
3 currentEthAddresses = await wallet.request({ method: 'eth_accounts', params: [] });
4 } catch (err) {
5 console.log('Get ETH accounts ERROR:', err)
6 me.state = 'REJECTED';
7 return;
8 }
9}

The next step is to send tokens. We use the eth_sendTransaction method. It requires three parameters: from, to and value. From and to are the same in our example. It is the current Ethereum address. Value is a transfer amount in Wei with a string representation of a hexadecimal number.

1try {
2 const transferAmount = BigInt(transferAmountEth * 1_000_000) * BigInt(1_000_000_000_000);
3 const transactionHash = await wallet.request({
4 method: 'eth_sendTransaction',
5 params: [
6 {
7 from: currentEthAddresses[0],
8 to: currentEthAddresses[0],
9 value: transferAmount.toString(16),
10 },
11 ],
12 });
13 console.log('transactionHash', transactionHash)
14 me.state = 'MINING';
15 /*
16 ...
17 */
18} catch (err) {
19 console.log('Transaction ERROR:', err)
20 me.state = 'REJECTED';
21}

When the transaction is approved by the user we will get the transaction hash. It's time to switch the button's state to MINING.

Now all we can do is wait for the the chain to confirm the transaction. We have the wallet.waitTransaction() method. It receives two parameters:

  • txHash: string - (required) a transaction hash returned from wallet.request();
  • confirmations?: number - (optional, default === 1) the number of blocks confirming the transaction;

and returns Promise<ITransactionReceipt>. ITransactionReceipt has a status property that can tell us if the transaction has been completed successfully ("0x1") or failed ("0x0"). We can set an appropriate state for the button.

1try {
2 const transactionReceipt = await wallet.waitTransaction(transactionHash, 2);
3 console.log('transactionReceipt', transactionReceipt)
4 await wallet.disconnect();
5 me.state = transactionReceipt.status === "0x1" ? 'COMPLETED' : 'FAILURE';
6} catch (err) {
7 console.log('Transaction waiting ERROR:', err)
8 me.state = 'FAILURE';
9}

After getting the transactionReceipt we disconnect the wallet. It is not required but convenient for our example. We can connect the wallet to another chain after sending the transaction.

The entire code for this step:

1exec: async (_, me) => {
2 if (wallet.authMethod === 'ethereum/goerli') {
3 me.state = 'PENDING';
4 if (!currentEthAddresses) {
5 try {
6 currentEthAddresses = await wallet.request({ method: 'eth_accounts', params: [] });
7 } catch (err) {
8 console.log('Get ETH accounts ERROR:', err)
9 me.state = 'REJECTED';
10 return;
11 }
12 }
13 try {
14 const transferAmount = BigInt(transferAmountEth * 1_000_000) * BigInt(1_000_000_000_000);
15 const transactionHash = await wallet.request({
16 method: 'eth_sendTransaction',
17 params: [
18 {
19 from: currentEthAddresses[0],
20 to: currentEthAddresses[0],
21 value: transferAmount.toString(16),
22 },
23 ],
24 });
25 console.log('transactionHash', transactionHash)
26 me.state = 'MINING';
27 try {
28 const transactionReceipt = await wallet.waitTransaction(transactionHash, 2);
29 console.log('transactionReceipt', transactionReceipt)
30 await wallet.disconnect();
31 me.state = transactionReceipt.status === "0x1" ? 'COMPLETED' : 'FAILURE';
32 } catch (err) {
33 console.log('Transaction waiting ERROR:', err)
34 me.state = 'FAILURE';
35 }
36 } catch (err) {
37 console.log('Transaction ERROR:', err)
38 me.state = 'REJECTED';
39 }
40 } else if (wallet.authMethod === 'near/testnet') {
41 me.state = 'NEAR_CONNECTED';
42 } else {
43 me.state = 'DEFAULT';
44 }
45},

5. Send the necessary data to a NEAR wallet and listen to the response#

Start this step by checking wallet.authMethod and switching the button's state to PENDING.

1exec: async (_, me) => {
2 if (wallet.authMethod === 'near/testnet') {
3 me.state = 'PENDING';
4 /*
5 ...
6 */
7 } else if (wallet.authMethod === 'ethereum/goerli') {
8 me.state = 'ETH_CONNECTED';
9 } else {
10 me.state = 'DEFAULT';
11 }
12},

We don't need to make a request to get an account ID when working with a NEAR wallet. It's already available in the connected wallet.

There are methods of NEAR-API-JS library in the INearWallet.

In the example we will use the wallet.sendMoney() method. It receives two parameters:

  • receiverId — NEAR account receiving Ⓝ;
  • amount — Amount to send in yoctoⓃ;

and returns Promise<FinalExecutionOutcome>.

To convert the transfer amount to yoctoⓃ we use bn.js library and the parseNearAmount() method from utils/format module of NEAR-API-JS library.

Let's add the library.

npm i bn.js
npm i -D @types/bn.js

Import it to the ./src/index.ts module and get the amount value.

1// ./src/index.ts
2import BN from 'bn.js';
3const { parseNearAmount } = Core.near.utils.format;
4/*
5 ...
6*/
7const amount = new BN(parseNearAmount(transferAmountNear.toString()));

Now we can call the wallet.sendMoney() method.

1try {
2 const finalExecutionOutcome = await wallet.sendMoney(
3 wallet.accountId,
4 amount
5 );
6 console.log('finalExecutionOutcome', finalExecutionOutcome);
7 await wallet.disconnect();
8 /*
9 ...
10 */
11} catch (err) {
12 console.log('Transaction ERROR:', err)
13 me.state = 'REJECTED';
14}

finalExecutionOutcome also has a status property. It is an object with a single key-value property. If the key is 'SuccessValue' or 'SuccessReceiptId', we consider that the transaction was successful.

1const status = Object.keys(finalExecutionOutcome.status)[0];
2me.state = status === 'SuccessValue' || status === 'SuccessReceiptId'
3 ? 'COMPLETED'
4 : 'FAILURE';

Result of this step:

1exec: async (_, me) => {
2 if (wallet.authMethod === 'near/testnet') {
3 me.state = 'PENDING';
4 try {
5 const amount = new BN(parseNearAmount(transferAmountNear.toString()));
6 const finalExecutionOutcome = await wallet.sendMoney(
7 wallet.accountId,
8 amount
9 );
10 console.log('finalExecutionOutcome', finalExecutionOutcome);
11 await wallet.disconnect();
12 const status = Object.keys(finalExecutionOutcome.status)[0];
13 me.state = status === 'SuccessValue' || status === 'SuccessReceiptId'
14 ? 'COMPLETED'
15 : 'FAILURE';
16 } catch (err) {
17 console.log('Transaction ERROR:', err)
18 me.state = 'REJECTED';
19 }
20 } else if (wallet.authMethod === 'ethereum/goerli') {
21 me.state = 'ETH_CONNECTED';
22 } else {
23 me.state = 'DEFAULT';
24 }
25},

The dapplet is finished. 🎉

Don't forget to install dependencies and run the dapplet:

npm i
npm start

Currently you need to reload the page to select a different wallet after disconnecting it. We will fix it soon.

Here is the result code of the example: ex05-wallet-solution.

tip

There is another way to get a Wallet connection by using Core.login(). It's described in Ex14: Core Login.