Part 2: Smart Contract and Angular Service Interaction

How to read and write data to the blockchain within our Angular Application.

Published in
5 min readJan 20, 2022

--

In Part 1 we took a general look at this repository that you can use as a starting point for developing your DApps with Angular.

In this part, we want to look at the Service we use to interact with the Smart Contract written in Solidity.

This is the Smart Contract that will store and read images from the blockchain:

// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;

contract Gallery {
Image[] private images;
mapping(address => Image[]) private authorToImages;

struct Image {
string title;
string imageMetaDataUrl;
}

function store(string memory title, string memory imageMetaDataUrl) public {
Image memory image = Image(title, imageMetaDataUrl);

images.push(image);
authorToImages[msg.sender].push(image);
}

function retrieveAllImages() public view returns (Image[] memory) {
return images;
}

function retrieveImagesByAuthor() public view returns (Image[] memory) {
return authorToImages[msg.sender];
}
}

Let's go through it step by step.

Here we store all uploaded images in an array:

Image[] private images;

Here we map the address of the authors to their uploaded images. We use a mapping its a reference like an array and structs.

Mappings can be seen as hash tables that are virtually initialized such that every possible key exists and is mapped to a value whose byte-representation is all zeros: a type’s default value.

mapping(address => Image[]) private authorToImages;

Here a struct is used, it's like an object, we could also store the title of the image in the metadata, I wanted to showcase how to use a struct. The parameter imageMetaDataUrl will hold the URL to IPFS to a JSON object that holds the URL to the image and the description.

struct Image {
string title;
string imageMetaDataUrl;
}

We pass the title and the imageMetaDataUrl() as strings to the store function that will push the initialized struct Image() to the images array and the authorToImages() mapping.

function store(string memory title, string memory imageMetaDataUrl) public {
Image memory image = Image(title, imageMetaDataUrl);

images.push(image);
authorToImages[msg.sender].push(image);
}

The retrieveAllImages() is a simple getter method it will return all images.

function retrieveAllImages() public view returns (Image[] memory) {
return images;
}

From the retrieveImagesByAuthor() method, we will get by msg.sender (address of the sender) all images uploaded by this address.

function retrieveImagesByAuthor() public view returns (Image[] memory) {
return authorToImages[msg.sender];
}

Now, we take a look at the Angular Service that will interact with the deployed Smart Contract.

import { Injectable } from '@angular/core';
import { ethers } from "ethers";
import { environment } from "../../environments/environment";
import Gallery from '../../../artifacts/contracts/Gallery.sol/Gallery.json'
import detectEthereumProvider from "@metamask/detect-provider";

@Injectable({
providedIn: 'root'
})
export class GalleryService {
public async getAllImages(): Promise<any[]> {
const contract = await GalleryService.getContract()

return await contract['retrieveAllImages']()
}

public async getImagesByAuthor(): Promise<any[]> {
const contract = await GalleryService.getContract(true)

return await contract['retrieveImagesByAuthor']()
}

public async addImage(title: string, fileUrl: string): Promise<boolean> {
const contract = await GalleryService.getContract(true)
const transaction = await contract['store'](
title,
fileUrl
)
const tx = await transaction.wait()

return tx.status === 1
}

private static async getContract(bySigner=false) {
const provider = await GalleryService.getWebProvider()
const signer = provider.getSigner()

return new ethers.Contract(
environment.contractAddress,
Gallery.abi,
bySigner ? signer : provider,
)
}

private static async getWebProvider(requestAccounts = true) {
const provider: any = await detectEthereumProvider()

if (requestAccounts) {
await provider.request({ method: 'eth_requestAccounts' })
}

return new ethers.providers.Web3Provider(provider)
}
}

The most interesting part of our imports gets the details of our Gallery contract.

import Gallery from '../../../artifacts/contracts/Gallery.sol/Gallery.json'

We have two helper methods on the bottom of our Service getWebProvider() will connect the Application to Metamask. We use an npm package to detect the provider.

A tiny utility for detecting the MetaMask Ethereum provider, or any provider injected at window.ethereum.

private static async getWebProvider(requestAccounts = true) {
const provider: any = await detectEthereumProvider()

if (requestAccounts) {
await provider.request({ method: 'eth_requestAccounts' })
}

return new ethers.providers.Web3Provider(provider)
}

Our other helper method getContract() returns an instance of our contract if bySigner is set to true we will give the context of the user (account) to the contract.

private static async getContract(bySigner=false) {
const provider = await GalleryService.getWebProvider()
const signer = provider.getSigner()

return new ethers.Contract(
environment.contractAddress,
Gallery.abi,
bySigner ? signer : provider,
)
}

Within getAllImages(), we get an instance of our Smart Contract and call the retrieveAllImages() method on it to get all uploaded images.

public async getAllImages(): Promise<any[]> {
const contract = await GalleryService.getContract()

return await contract['retrieveAllImages']()
}

In getImagesByAuthor(), we get the contract by the signer (logged-in user) and call the retrieveImagesByAuthor() method of the contract.

public async getImagesByAuthor(): Promise<any[]> {
const contract = await GalleryService.getContract(true)

return await contract['retrieveImagesByAuthor']()
}

The addImage() method is the only method that will save data to the blockchain. It will receive the title and the IPFS URL to the image metadata gets the contract by the signer calls the store method of the Smart Contract and passes to parameters to the contract that will be saved to the blockchain.

In the end, we will wait till the transaction is finished and the data were saved to the blockchain and returned if the transaction was successful.

public async addImage(title: string, fileUrl: string): Promise<boolean> {
const contract = await GalleryService.getContract(true)
const transaction = await contract['store'](
title,
fileUrl
)
const tx = await transaction.wait()

return tx.status === 1
}

Now, let's take a look at how we save images and data to IPFS. Here is the Angular Service we are using to communicate to IPFS.

import { Injectable } from "@angular/core"
import { create } from "ipfs-http-client"
import { environment } from "../../environments/environment"

@Injectable({
providedIn: 'root'
})
export class IpfsService {
public async uploadFile(data: any): Promise<string> {
let url = ''
const client = IpfsService.getClient()

try {
const added = await client.add(data)
url = `${environment.ipfs}/ipfs/${added.path}`
} catch (error) {
console.log(error)
}

return url
}

private static getClient(): any {
// @ts-ignore
return create(`${environment.ipfs}:5001/api/v0`)
}
}

We have a helper method getClient() on the bottom of our Service that will return an instance of the HTTP API client, that resolves to a running instance of the IPFS HTTP API. We are using an Infura Node https://ipfs.infura.io:5001/api/v0 but you could also use your own Node.

The uploadFile() method receives an URL to an image or a data object and will upload it to IPFS with client.add() that returns a hash, in the next line we return the full URL to the uploaded asset.

In our Components we just need to call the uploadFile() method with the file (image) we want to upload or JSON data object.

const fileUrl = await this.ipfs.uploadFile(eventTarget.files[0])// orconst metaDataUrl = await this.ipfs.uploadFile(JSON.stringify({
fileUrl,
description
}))

I hope this gives you a good round-up to build your own DApps with Angular. If I couldn't answer all of your questions you are welcome to add a comment.

--

--

Loving web development and learning something new. Always curious about new tools and ideas.