Building a Decentralized Exchange on Celo with Golang

Building a Decentralized Exchange on Celo with Golang https://celo.academy/uploads/default/optimized/2X/4/412cc97c7c8c8a8cc1b8640a260ee9cc9fcbed3a_2_1024x576.jpeg
none 0.0 0

Introduction

Decentralized exchanges (DEXs) are a crucial component of the decentralized finance (DeFi) ecosystem. They allow for peer-to-peer trading of cryptocurrencies and other digital assets without the need for a centralized authority.

Celo is a blockchain platform that is designed to send, receive, and store digital assets securely on a mobile phone. The Celo blockchain is built using the Solidity programming language and is fully compatible with Ethereum. The Celo Golang package provides developers with a set of tools and libraries to interact with the Celo blockchain using the Go programming language.

In this tutorial, we will walk you through the process of building a simple decentralized exchange on Celo using Golang.

Prerequisites

In this tutorial, we’re going to build a Decentralized Exchange on Celo with Golang, so before we begin, you should have some familiarity with the following technologies:

  • Go programming language
  • Solidity programming language
  • Celo blockchain

Requirements

Before we get started, make sure you have the following installed:

Step 1: Set up the project

On your terminal, create a new directory for this project:

mkdir my-dex
cd my-dex

Initialize a new Go module:

go mod init my-dex

Install the Celo blockchain package:

go get -u github.com/ethereum/go-ethereum

Note: This Go Ethereum package created by Celo is a fork of the main Go Ethereum package. If you are having any import errors, just go to your terminal and run:

go mod tidy

Step 2: Smart contract development

Next, we have to build a smart contract for the decentralized exchange. Go into your project directory and create a file called “Exchange.sol”:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

contract Exchange {

    // Define variables
    address payable public owner;
    uint public orderCount = 0;
    mapping(uint => Order) public orders;
    mapping(address => uint) public balances;

    // Define struct for order
    struct Order {
        uint id;
        address payable seller;
        address payable buyer;
        uint price;
        bool completed;
    }

    // Define events for when an order is created and when an order is completed
    event OrderCreated(uint id, address payable seller, address payable buyer, uint price, bool completed);
    event OrderCompleted(uint id, address payable seller, address payable buyer, uint price, bool completed);

    // Constructor function
    constructor() {
        owner = payable(msg.sender);
    }

    // Sell function - create a new order
    function sell(uint _price) public {
        // Increment order count
        orderCount++;

        // Create new order
        orders[orderCount] = Order(orderCount, payable(msg.sender), payable(address(0)), _price, false);

        // Emit event
        emit OrderCreated(orderCount, payable(msg.sender), payable(address(0)), _price, false);
    }

    // Buy function - complete an existing order
    function buy(uint _id) public payable {
        // Get order from mapping
        Order memory _order = orders[_id];

        // Make sure order exists and is not completed
        require(_order.id > 0 && _order.completed == false, "Order does not exist or is already completed.");

        // Make sure buyer has enough ether to purchase order
        require(msg.value >= _order.price, "Not enough ether sent to purchase order.");

        // Transfer ether to seller
        _order.seller.transfer(msg.value);

        // Update order to completed
        _order.buyer = payable(msg.sender);
        _order.completed = true;
        orders[_id] = _order;

        // Emit event
        emit OrderCompleted(_id, _order.seller, payable(msg.sender), _order.price, true);
    }

    // Deposit function - add ether to user's balance
    function deposit() public payable {
        // Add ether to user's balance
        balances[msg.sender] += msg.value;
    }

    // Withdraw function - withdraw ether from user's balance
    function withdraw(uint _amount) public {
        // Make sure user has enough ether in their balance
        require(balances[msg.sender] >= _amount, "Not enough ether in balance to withdraw.");

        // Subtract ether from user's balance and transfer to user
        balances[msg.sender] -= _amount;
        payable(msg.sender).transfer(_amount);
    }

    // Order function - get order details
    function getOrder(uint _id) public view returns (uint, address, address, uint, bool) {
        // Get order from mapping and return details
        Order memory _order = orders[_id];
        return (_order.id, _order.seller, _order.buyer, _order.price, _order.completed);
    }
}

The smart contract above has 5 basic functionalities that every decentralized exchange should have and I’ll highlight each of them.

  • The sell function: This function enables users to create a sell order and provide the price they want to sell for and then a new order is created and added to the mapping of the order which is the order IDs mapped to order structs. It then emits an event to let the smart contract know when a new order is placed by a user.
  • The buy function: This function allows a user to finish an existing order by providing the order ID and sending enough ether to cover the item’s price. The function verifies that the order exists and has not already been completed, as well as that the user has sent an adequate amount of ether. If all checks are successful, the function sends the ether to the seller and updates the order to indicate that it has been completed. In addition, the function emits an event to notify the rest of the world that the order has been completed.
  • The deposit function: A user can deposit ether into their account balance using this function. The function adds the ether sent to the user’s balance.
  • The withdraw function: A user can use this function to withdraw ether from their account balance. The function determines whether the user has enough ether in their balance to withdraw the requested amount, then subtracts it from their balance and transfers it to their address.
  • The getOrder function: This function allows a user to obtain order details by inputting the order ID. The order is retrieved from the orders mapping and its ID, seller address, buyer address, price, and completion status are returned.

Step 3: Compile and deploy the smart contract

Next, we need to compile the smart contract to generate the ABI and Bytecodes of the smart contract. Before we do that, let’s install the solidity compiler which compiles our solidity code.

npm init -y
npm install solc@0.8.0

Create a file called “compile.js” and paste the following code:

compile.js

const fs = require("fs");
const solc = require("solc");

const input = fs.readFileSync("Exchange.sol", "utf8");

const output = solc.compile(
  JSON.stringify({
    language: "Solidity",
    sources: {
      "Exchange.sol": {
        content: input,
      },
    },
    settings: {
      outputSelection: {
        "*": {
          "*": ["*"],
        },
      },
    },
  })
);

const { Exchange } = JSON.parse(output).contracts["Exchange.sol"];
fs.writeFileSync("Exchange.abi", JSON.stringify(Exchange.abi));
fs.writeFileSync("Exchange.bin", Exchange.evm.bytecode.object);

The code above compiles our smart contract and creates two files; “Exchange.abi” and “Exchange.bin”

Run the following command on your terminal to compile the code:

node compile.js

To deploy the contract, go to your root directory and create a new file called “deploy.go”

deploy.go

package main

import (
	"context"
	"crypto/ecdsa"
	"fmt"
	"io/ioutil"
	"log"
	"math/big"

	"github.com/ethereum/go-ethereum/accounts/abi/bind"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/ethereum/go-ethereum/ethclient"
)

// Replace this with your own private key and Celo node URL
const privateKey = "your-private-key"
const nodeURL = "https://alfajores-forno.celo-testnet.org"

func main() {
	// Connect to the Celo network
	client, err := ethclient.Dial(nodeURL)
	if err != nil {
		log.Fatalf("Failed to connect to the Celo network: %v", err)
	}
	defer client.Close()

	// Load the private key
	key, err := crypto.HexToECDSA(privateKey)
	if err != nil {
		log.Fatalf("Failed to load the private key: %v", err)
	}

	// Load the contract ABI
	abiBytes, err := ioutil.ReadFile("Exchange.abi")
	if err != nil {
		log.Fatalf("Failed to read the contract ABI: %v", err)
	}
	fmt.Println(abiBytes)

	// Load the contract bytecode
	bytecode, err := ioutil.ReadFile("Exchange.bin")
	if err != nil {
		log.Fatalf("Failed to read the contract bytecode: %v", err)
	}

	// Get the public address associated with the private key
	publicKey := key.Public().(*ecdsa.PublicKey)
	address := crypto.PubkeyToAddress(*publicKey)

	// Get the nonce associated with the address
	nonce, err := client.PendingNonceAt(context.Background(), address)
	if err != nil {
		log.Fatalf("Failed to get the nonce: %v", err)
	}

	// Get the gas price
	gasPrice, err := client.SuggestGasPrice(context.Background())
	if err != nil {
		log.Fatalf("Failed to get the gas price: %v", err)
	}

	// Create a new transaction
	tx := types.NewContractCreation(nonce, big.NewInt(0), 3000000, gasPrice, common.FromHex(string(bytecode)))

	// Sign the transaction
	signedTx, err := types.SignTx(tx, types.NewEIP155Signer(big.NewInt(44787)), key)
	if err != nil {
		log.Fatalf("Failed to sign the transaction: %v", err)
	}

	// Broadcast the transaction
	err = client.SendTransaction(context.Background(), signedTx)
	if err != nil {
		log.Fatalf("Failed to broadcast the transaction: %v", err)
	}

	// Wait for the transaction receipt
	receipt, err := bind.WaitMined(context.Background(), client, signedTx)
	if err != nil {
		log.Fatalf("Failed to get the transaction receipt: %v", err)
	}

	// Print the contract address
	fmt.Printf("Smart contract deployed at address: %s\n", receipt.ContractAddress.Hex())
}

Let’s go through the code step by step.

package main

import (
	"context"
	"crypto/ecdsa"
	"fmt"
	"io/ioutil"
	"log"
	"math/big"

	"github.com/ethereum/go-ethereum/accounts/abi/bind"
	"github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/crypto"
	"github.com/ethereum/go-ethereum/ethclient"
)

The “package main” initializes this file as our main Go package and the “import” command is used to import all the package dependencies.

const privateKey = "your-private-key"
const nodeURL = "https://alfajores-forno.celo-testnet.org"

Replace the string with your Celo wallet private key and the node URL is the Celo Alfrajores or Testnet URL.

// Connect to the Celo network
client, err := ethclient.Dial(nodeURL)
	if err != nil {
		log.Fatalf("Failed to connect to the Celo network: %v", err)
	}
	defer client.Close()

	// Load the private key
	key, err := crypto.HexToECDSA(privateKey)
	if err != nil {
		log.Fatalf("Failed to load the private key: %v", err)
	}

	// Load the contract ABI
	abiBytes, err := ioutil.ReadFile("Exchange.abi")
	if err != nil {
		log.Fatalf("Failed to read the contract ABI: %v", err)
	}
	fmt.Println(abiBytes)

	// Load the contract bytecode
	bytecode, err := ioutil.ReadFile("Exchange.bin")
	if err != nil {
		log.Fatalf("Failed to read the contract bytecode: %v", err)
	}

The code above allows us to connect to the Celo blockchain, load our private key, and load our contract ABI and byte codes which were generated when our contract was compiled.

// Get the public address associated with the private key
	publicKey := key.Public().(*ecdsa.PublicKey)
	address := crypto.PubkeyToAddress(*publicKey)

	// Get the nonce associated with the address
	nonce, err := client.PendingNonceAt(context.Background(), address)
	if err != nil {
		log.Fatalf("Failed to get the nonce: %v", err)
	}

	// Get the gas price
	gasPrice, err := client.SuggestGasPrice(context.Background())
	if err != nil {
		log.Fatalf("Failed to get the gas price: %v", err)
	}

From the code above, our contract address and public key are gotten from the crypto package. The nonce and “gasPrice” are needed for a transaction to occur on the blockchain.

// Create a new transaction
	tx := types.NewContractCreation(nonce, big.NewInt(0), 3000000, gasPrice, common.FromHex(string(bytecode)))

	// Sign the transaction
	signedTx, err := types.SignTx(tx, types.NewEIP155Signer(big.NewInt(44787)), key)
	if err != nil {
		log.Fatalf("Failed to sign the transaction: %v", err)
	}

	// Broadcast the transaction
	err = client.SendTransaction(context.Background(), signedTx)
	if err != nil {
		log.Fatalf("Failed to broadcast the transaction: %v", err)
	}

	// Wait for the transaction receipt
	receipt, err := bind.WaitMined(context.Background(), client, signedTx)
	if err != nil {
		log.Fatalf("Failed to get the transaction receipt: %v", err)
	}

	// Print the contract address
	fmt.Printf("Smart contract deployed at address: %s\n", receipt.ContractAddress.Hex())
}

A new transaction is created and signed and a receipt is generated to validate that the smart contract is actually deployed and has a particular contract address attached to it.

On your terminal, run the following command:

go run deploy.go

You should see something like this on your terminal:

Conclusion

In this tutorial, we have learned how to implement a decentralized exchange on Celo using Golang. We explored topics such as setting up a development environment, creating a smart contract, and finally compiling and deploying the contract on Celo Testnet or Afrajores with Golang.

Next Steps

After building a decentralized exchange on Celo using Golang, you can improve the smart contract by adding other features such as an order book, market orders, limit orders, etc. You could also integrate with other Defi protocols on the Celo blockchain.

About the Author

I am a Software engineer with an interest in Blockchain technology. I love picking up new technologies and sharing my knowledge as I learn as a way of giving back to the tech community. You can find one on GitHub and Twitter.

References

  1. Celo Developer Documentation
  2. Go Programming Language Documentation
  3. Solidity Documentation
  4. Github repo
7 Likes

Nice piece :clap:

2 Likes

As much as I do not understand Golang but general programming, this tutorial is cool. I love the syntaxes and I’ve always wanted to know how to use it. Can you recommend where to begin? @divinef837

3 Likes

Hi Bobelr, sorry for the late response and thank you for the feedback on my tutorial. I’d recommend you to start Golang with learn code online playlist on YouTube Let's go with golang - YouTube

You can also learn Golang (at least the basics) from the official Golang documentation)

1 Like

Thank for the link. Good job.

2 Likes

Golang emphasizes simplicity and readabilty and im glad this tutorial has that, welldone brother.

2 Likes

Wow, this tutorial provides a clear and concise overview of building a decentralized exchange on Celo using Golang. Nd even though I don’t write code in Golang I can see it covers the necessary steps, including setting up the project, smart contract development, and deployment. The code examples and explanations are well-structured, making it easier for developers to follow along :clap:and implement their own DEX on Celo.

3 Likes