In the two previous posts of this serie, we have learned how to build a house-swap ethereum smart-contract using solidity and how to test it using hardhat.
In this post, we will create an angular application and we will install ethers to interact with the smart-contract and ethers-typechain to generate typescript types from our contract json abi.
The contract json abi is generated after compiling the contract using hardhat as we did in the previous post.
In the last post, we also used typechain to generate contract types since we needed them to build the tests.
Before starting
This article assumes that the reader has a basic knowledge about the angular framework. This means that the reader knows basic framework concepts like components, services and the angular injector. In the same way, the article assumes that the reader knows basic concepts about cryptocurrencies like what custodial and non custodial wallets are.
I will try to push the angular app code to my github account as soon as possible.
Creating an ethereum account
We will need to get an ethereum address and fund it with some GoerliETH. Goerli is an ethereum testnet. We are going to deploy our contract to this network.
To create an ethereum account, we are going to install metamask. Metamask is an easy-to-use non custodial wallet which we can install as an extension in our browser.
Use this kind of wallets only for testing purposes or for holding few assets.
Go to metamask site and click on the Download button. In the next page, you will see a button for installing metamask as an extension of your browser. Follow the instructions to install and creating your account.
Selecting the test network
After having metamask installed as a browser extension, we have to select the Goerli testnet to ensure we are working on a test network. To select it, click on the top left corner button and you will see a networks list. Select the Show test networks checkbox and then select Goerli network.
Funding the account with GoerliETH
After having created the account and selected Goerli, we will see that we hold 0 GoerliETH. We need some GoerliETH to test our smart-contract. The fastest way to get them is to use a faucet.
An ethereum faucet is a tool to get test ETH. In our case, we are going to use the goerli faucet. Go to the Goerli faucet link and you will se the following page:
Before requesting GoerliETH you will have to follow the next steps:
- Creating an account on Alchemy. Alchemy is a web3 platform that allows developers to create and deploy decentralized applications and also provides tools, sdk's and api's to scale them. Enter into alchemy, click on Sign up button and follow the steps to create your account.
-
Funding your account on the mainnet: In order to prevent abuse, goerli faucet requires accounts to have a minimum of 0.001 ETH on the mainnet (actually 1,55$). To fund your account with real ETH you will have to follow the next steps:
- Go to metamask, switch to mainnet and copy your address.
- Go to an exchange (coinmotion, coinbase, binance or the the one of your choice), buy some ETH and send them to your metamask address.
After receiving ETH on your address, you are ready to get some GoerliETH. Go back to the goerli faucet and click on signup or login with Alchemy. After being logged, paste your address on the input and click on Send me ETH. If everything works, you will see a new transaction and after a few seconds you should have received your GoerliETH in your metamask account.
Now, we are ready to start our angular app.
Creating the angular application
We are going to use angular-cli to create a new angular application so, if you have not installed it yet, install it using npm.
npm install -g @angular/cli
Now that we have angular-cli installed, let's create a new angular application using it:
ng new <the_app_name_of_your_choice>
Open the project folder with an editor of your choice (I am using vscode) and you will see the folders and files created by the angular-cli command which are required to start developing.
Before starting to write code, let's install ethers and typechain since we will need them to complete the rest of the article.
npm install ethers @typechain/ethers-v6
Creating the services
We are going to create three services each of them will be responsible for an specific task:
The WalletService
The wallet service will be in charge of prompting metamask so we can select the account we want to use to deploy the contract.
Let's create the wallet service using angular-cli:
ng g service service/walletService
This will create an app/service folder and will create the service into it. Let's see now the code of the service:
import { Injectable } from '@angular/core';
import { BrowserProvider, Signer } from 'ethers';
@Injectable({
providedIn: 'root'
})
export class WalletService {
constructor() { }
getSigner(): Promise<Signer> {
const provider = new BrowserProvider(window.ethereum);
return provider.getSigner();
}
}
The getSigner method contains two lines, the first one opens the metamask prompt using BrowserProvider. The BrowserProvider allows us to access a wallet installed on the browser. The next line, gets the signer from the provider. We need the account signer to deploy and interact with the contract.
Notice that, the function does not return the signer itself but a Promise. The caller will have to resolve it and save the signer.
The ContractDeployerService
This service will be in charge of getting the contract abi and deploying the contract.
ng g service service/contractDeployerService
Let's see the code:
import { Injectable } from '@angular/core';
import { BaseContract, ContractFactory, ContractRunner } from 'ethers';
import { HttpClient } from '@angular/common/http'
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ContractDeployerService {
private contractUrl: string = '/assets/house_swap.json';
constructor(private httpClient: HttpClient) { }
getContractFromSolidity(solidityOutput: any, signer: ContractRunner): ContractFactory {
return ContractFactory.fromSolidity(solidityOutput, signer);
}
deployContract(contract: ContractFactory): Promise<BaseContract>{
return contract.deploy();
}
getContractAbi(): Observable<any> {
return this.httpClient.get(this.contractUrl);
}
}
The getContractFromSolidity method gets an ethers ContractFactory object which we will need later to deploy the contract. The solidityOutput parameter is the json generated after compiling the solidity contract as we did in the previous post using hardhat.
We used the npx hardhat compile command to compile the solidity contract. This command generated an artifacts folder which contained that json file. In this post, we have to copy that file and paste it in our angular project assets folder.
The deployContract method uses the ContractFactory deploy method to deploy the contract. Notice again that the deployContract method returns a promise so the caller will have to resolve it.
The getContractAbi method uses angular http client to get the contract abi json from the assets folder url.
The ContractInteractorService
This service is responsible of interacting with the contract, that is, calling the contract functions passing the required parameters. We can generate as we did for the last ones.
import { Injectable } from '@angular/core';
import { ContractTransactionResponse, Signer } from 'ethers';
import { House_swap__factory, House_swap } from '../types';
import { HouseSwap } from '../types/House_swap';
@Injectable({
providedIn: 'root'
})
export class ContractInteractorService {
contract!: House_swap;
constructor() {}
load(contractAddress: string, signer: Signer): void {
this.contract = House_swap__factory.connect(contractAddress, signer);
}
initialize(houseType: string, link: string, value: number, address: string): Promise<ContractTransactionResponse> {
const house: HouseSwap.HouseStruct = {
houseType: houseType,
value: value,
link: link,
propietary: address
};
return this.contract.initialize(house);
}
}
You can notice here that there are two imports (the last ones) which import objects from a types folder. This objects are contract mappings generated with typechain which allow us to call contract methods as if they were class methods.
To generate types folder, you will have to execute this command in your project folder:
./node_modules/.bin/typechain --target ethers-v6 src/assets/*.json --out-dir ./src/app/types
Let's see now the service methods.
The load method gets the contract deployed address and the signer and creates a House_Swap contract using House_swap__factory connect method. After connecting it, every call to the contract will be made by the signer passed as parameter (that must be the signer provided by metamask prompt).
The initialize method creates a HouseStruct object (which is a map for the solidity contract House struct). Remember that the contract initialize method requires a House struct as a parameter. Then it calls the contract initialize method passing the created HouseStruct object as a parameter. The method returns a promise which will hold a ContractTransactionResponse after resolving.
Creating the component to interact with the services
So far, we have created the services which interact with the service and the contract. In this section we will create an angular component which will interact with the services for getting the signer, deploying the contract and interacting with it.
To create the component, use angular-cli:
ng g component component/home
This will create a component folder and the component files into it. Let's see now the component code:
import { Component, OnInit } from '@angular/core';
import { BaseContract, Signer, ContractTransactionResponse } from 'ethers';
import { lastValueFrom } from 'rxjs';
import { ContractDeployerService } from 'src/app/service/contract-deployer.service';
import { ContractInteractorService } from 'src/app/service/contract-interactor.service';
import { WalletService } from 'src/app/service/wallet.service';
import { HouseSwap } from 'src/app/types/House_swap';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {
protected signer!: Signer;
constructor(private walletSrv: WalletService, private contractDeployerSrv: ContractDeployerService, private contractInteractorSrv: ContractInteractorService){
}
ngOnInit(): void {
}
connectWallet() {
this.walletSrv.getSigner().then(
(signer: Signer) => {
this.signer = signer;
console.log(this.signer);
}
)
}
async deployContract() {
const contractAbi$ = this.contractDeployerSrv.getContractAbi();
const contractAbi = await lastValueFrom(contractAbi$);
const abiRaw: string = JSON.stringify(contractAbi);
const contractFactory = this.contractDeployerSrv.getContractFromSolidity(abiRaw, this.signer);
this
.contractDeployerSrv
.deployContract(contractFactory)
.then(
(contract: BaseContract) => {
contract.waitForDeployment().then(
async () => {
const contractAddress = await contract.getAddress();
this.contractInteractorSrv.load(contractAddress, this.signer);
console.log(contractAddress);
}
).catch(
(error: any) => {
console.log(error);
}
);
}
)
.catch(
(error: any) => {
console.log(error);
}
)
}
async initializeContract() {
const address = await this.signer.getAddress();
const house: HouseSwap.HouseStruct = {
houseType: 'apartment',
link: 'https://www.infolink.com',
value: 52665,
propietary: address
}
this
.contractInteractorSrv
.initialize(house)
.then(
async (callResult: ContractTransactionResponse) => {
console.log(callResult.toJSON())
})
}
}
As we can see, the component constructor injects the three services from the last section. Let's see each method one by one:
connectWallet() {
this.walletSrv.getSigner().then(
(signer: Signer) => {
this.signer = signer;
}
)
}
The connectWallet method uses walletService getSigner method to promt metamask and gets the signer. After resolving the promise, the signer is saved on component signer variable.
async deployContract() {
const contractAbi$ = this.contractDeployerSrv.getContractAbi();
const contractAbi = await lastValueFrom(contractAbi$);
const abiRaw: string = JSON.stringify(contractAbi);
const contractFactory = this.contractDeployerSrv.getContractFromSolidity(abiRaw, this.signer);
this
.contractDeployerSrv
.deployContract(contractFactory)
.then(
(contract: BaseContract) => {
contract.waitForDeployment().then(
async () => {
const contractAddress = await contract.getAddress();
this.contractInteractorSrv.load(contractAddress, this.signer);
}
).catch(
(error: any) => {
console.log(error);
}
);
}
)
.catch(
(error: any) => {
console.log(error);
}
)
}
The deployContract method is a little more extensive. Let's explain it step by step:
The first line uses contractDeployerService to get an observable. The next line uses RxJS lastValueFrom to get the contract json data.
The next two lines generate an string json from the contract json abi and uses contractDeployerService getContractFromSolidity to generate an ethers ContractFactory object.
Then, we use contractDeployerService deployContract method to deploy the contract to Goerli. The deployContract method returns a promise which (after resolving) returns an ethers BaseContract object. We also have to use BaseContract waitForDeployment method to ensure contract has been fully deployed. Then, we get the contract address and use contractInteractorService load method to create the House_Swap object to be ready to interact with the contract. If there are errors during the process, we show them in the console.
async initializeContract() {
const address = await this.signer.getAddress();
const house: HouseSwap.HouseStruct = {
houseType: 'apartment',
link: 'https://www.infolink.com',
value: 52665,
propietary: address
}
this
.contractInteractorSrv
.initialize(house)
.then(
async (callResult: ContractTransactionResponse) => {
console.log(callResult.toJSON())
})
}
The initializeContract uses contractInteractorService initialize to invoke contract initialize method. The service method returns a promise which, after resolving, holds an ethers ContractTransactionResponse object. It we log it, we will see many parameters. The next image shows some of them:
In the image, we can see three parameters, the gas limit, the gas price (transaction fee in wei) and the hash. Now, we can copy the hash a go to the goerli etherscan and search for the transaction information using the hash.
As we can see in the image, the transaction has been resolved successfully which means we've been invoked initialize method without errors.
Conclusion
In this third post, we have seen how to create an account in metamask and how to get some Goerli ETH from the goerli faucet. Then, we have learned how to install ethers and typechain within an angular application and use them to deploy and invoke our contract.
In order not to extend this article too much, I am going to create one last article for this serie which will show how to call the other contract functions and follow the contract flow.
Top comments (0)