Create Publication

We are looking for publications that demonstrate building dApps or smart contracts!
See the full list of Gitcoin bounties that are eligible for rewards.

Tutorial Thumbnail
Intermediate · 30 minutes

Deploying a Scheduling Service for Automated Payments

The tutorial can be used to learn how to automate a scheduled employee payment system on the Algorand Network. With an automated or scheduled payment system, you do not need to stress yourself over making payments to staff/employees manually. With this tutorial you will learn how to:

  • Save time and resources
  • Make stress-free task delegation
  • Use scheduling for prioritization of tasks

Requirements

Background

This is a Payment Scheduler DApp using the C#/.NET framework, Algorand Package/SDK, alongside Hangfire to process automated payments on the Algorand Network.

Additional Resources:

Alert

This code uses account restore from a private key, this is not recommended for production use, and is provided for only for tutorial purposes. To integrate a web application use a wallet SDK to sign and send transactions, such as the AlgoSigner SDK for Chrome Extension. See tutorial here. Replace SRC_ACCOUNT With your passphrase from creating an account, the quickest way to do this is see example here, or create visually with AlgoDEA IDE here.

Steps

1. Setup, Configure, and Create Application

Open Visual Studio, Create and set up project template with .Net Core 3.1 as shown below in Fig 1-1
Select the ASP.NET Core Web API project template and proceed to create the project.
EditorImages/2021/05/14 11:18/2021-05-14_10_49_15-Window.png
Figure 1-1 Create Project

Name the Project “AlgorandAutomatedPayment” or give it a preferred name of your choice.
Configure the project target framework under Additional Information Window with the selected/checked options as shown in Fig 1-2 below:
EditorImages/2021/05/14 11:19/2021-05-14_11_09_10-Window.png
Figure 1-2 Configure Project Information

2. Install Packages/SDKs

In order to configure Hangfire and Algorand payment system, we need to install Hangfire related packages and Algorand package as shown below in Fig 2-1.

EditorImages/2021/05/14 12:21/2021-05-14_13_19_24-Window.png
Figure 2-1 Install Project Packages

EditorImages/2021/05/14 12:22/2021-05-14_13_19_54-Window.png
Figure 2-2 Install Project Packages

3. Configure Services

Proceed to Configure the services that would be used to process the payment functionalities.
Add a Services folder and add a new interface, IPayments.cs and a class Payments.cs

EditorImages/2021/05/14 12:25/2021-05-14_13_23_52-Window.png
Figure 3-1 Create Project Services, IPayment Interface and PaymentService Class

IPayments.cs Interface

using System.Threading.Tasks;

namespace AlgorandAutomatedPayment.Services
{
    public interface IPayments
    {
        Task OneTimePayment();
        Task GroupedPayments();
    }
}

We declare the related interfaces for both the One Time Payment and Grouped payments in the code snippet above.

Payments.cs Class

This will implement the IPayment Interface and process the payment functionalities for both Atomic and One Time payments on the Algorand Network.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Algorand;
using Account = Algorand.Account;
using Algorand.V2;
using Algorand.Client;
using Algorand.V2.Model;

namespace AlgorandAutomatedPayment.Services
{
    public class Payments : IPayments
    {
        public async Task GroupedPayments()
        {
            string ALGOD_API_ADDR = "https://testnet-algorand.api.purestake.io/ps2"; //find in algod.net
            string ALGOD_API_TOKEN = "B3SU4KcVKi94Jap2VXkK83xx38bsv95K5UZm2lab"; //find in algod.token 
            string SRC_ACCOUNT = "table worth young female soft alert example shock pepper bind peanut amazing omit claw escape agree turn people symbol keen tooth cancel submit abstract fantasy";
            string DEST_ADDR = "VTQPQJTPTPER6WENW5Q4HEFXOVOGPAYWXJGBE7ECSME2NUYPYVIDMOP5I4";
            string DEST_ADDR2 = "TMY2LDQTLDAKZXORR5OKOKT57PIAPACEXTSLQNEIFDCSC3YTNXXY4REFQA";
            Account src = new Account(SRC_ACCOUNT);
            var algodApiInstance = new AlgodApi(ALGOD_API_ADDR, ALGOD_API_TOKEN);
            Algorand.V2.Model.TransactionParametersResponse transParams;
            try
            {
                transParams = algodApiInstance.TransactionParams();
            }
            catch (ApiException e)
            {
                throw new Exception("Could not get params", e);
            }
            // let's create a transaction group
            var amount = Utils.AlgosToMicroalgos(1);
            var tx = Utils.GetPaymentTransaction(src.Address, new Address(DEST_ADDR), amount, "pay message", transParams);
            var tx2 = Utils.GetPaymentTransaction(src.Address, new Address(DEST_ADDR2), amount, "pay message", transParams);
            //SignedTransaction signedTx2 = src.SignTransactionWithFeePerByte(tx2, feePerByte);
            Digest gid = TxGroup.ComputeGroupID(new Algorand.Transaction[] { tx, tx2 });
            tx.AssignGroupID(gid);
            tx2.AssignGroupID(gid);
            // already updated the groupid, sign
            var signedTx = src.SignTransaction(tx);
            var signedTx2 = src.SignTransaction(tx2);
            try
            {
                //contact the signed msgpack
                List<byte> byteList = new List<byte>(Algorand.Encoder.EncodeToMsgPack(signedTx));
                byteList.AddRange(Algorand.Encoder.EncodeToMsgPack(signedTx2));
                var id = algodApiInstance.RawTransaction(byteList.ToArray());
                Console.WriteLine("Successfully sent tx group with first tx id: " + id);
                Console.WriteLine("Confirmed Round is: " +
                    Utils.WaitTransactionToComplete(algodApiInstance, id.TxId).ConfirmedRound);
            }
            catch (ApiException e)
            {
                // This is generally expected, but should give us an informative error message.
                Console.WriteLine("Exception when calling algod#rawTransaction: " + e.Message);
            }
        }

        public async Task OneTimePayment()
        {
            string ALGOD_API_ADDR = "https://testnet-algorand.api.purestake.io/ps2"; //find in algod.net
            string ALGOD_API_TOKEN = "B3SU4KcVKi94Jap2VXkK83xx38bsv95K5UZm2lab"; //find in algod.token          
            string SRC_ACCOUNT = "table worth young female soft alert example shock pepper bind peanut amazing omit claw escape agree turn people symbol keen tooth cancel submit abstract fantasy";
            string DEST_ADDR = "VTQPQJTPTPER6WENW5Q4HEFXOVOGPAYWXJGBE7ECSME2NUYPYVIDMOP5I4";
            Account src = new Account(SRC_ACCOUNT);
            AlgodApi algodApiInstance = new AlgodApi(ALGOD_API_ADDR, ALGOD_API_TOKEN);
            try
            {
                var trans = algodApiInstance.TransactionParams();
            }
            catch (ApiException e)
            {
                Console.WriteLine("Exception when calling algod#getSupply:" + e.Message);
            }

            TransactionParametersResponse transParams;
            try
            {
                transParams = algodApiInstance.TransactionParams();
            }
            catch (ApiException e)
            {
                throw new Exception("Could not get params", e);
            }
            var amountsent = Utils.AlgosToMicroalgos(2);
            var tx = Utils.GetPaymentTransaction(src.Address, new Address(DEST_ADDR), amountsent, "pay message", transParams);
            var signedTx = src.SignTransaction(tx);

            Console.WriteLine("Signed transaction with txid: " + signedTx.transactionID);

            // send the transaction to the network
            try
            {
                var id = Utils.SubmitTransaction(algodApiInstance, signedTx);
                Console.WriteLine("Successfully sent tx with id: " + id.TxId);
                Console.WriteLine(Utils.WaitTransactionToComplete(algodApiInstance, id.TxId));
            }
            catch (ApiException e)
            {
                // This is generally expected, but should give us an informative error message.
                Console.WriteLine("Exception when calling algod#rawTransaction: " + e.Message);
            }
        }
    }
}

The PaymentService class will implement the functionalities of both the Grouped Transactions and One Time Payment.
The GroupedPayments method handles the processing of the grouped/atomic transactions on the Algorand Network, while the OneTimePayment will process the One Time Payment which will be scheduled/automated using the Hangfire package.

Note:
Note this code uses account restore from a private key, this is not recommended for production use, and is provided for only for tutorial purposes. To integrate a web application use a wallet SDK to sign and send transactions, such as the AlgoSigner SDK for Chrome Extension. See tutorial here .
Replace SRC_ACCOUNT With your passphrase from creating an account, the quickest way to do this is see example here
Or create visually with AlgoDEA IDE.

Or create from the Algorand Wallet (see App store or Google play) you can change the setting to TestNet, and create a new account and copy off the passphrase.

Now, create a Job.cs class in the Project solution as shown below:
EditorImages/2021/04/25 16:15/2021-04-25_17_14_08-Window.png
Figure 3-2 Create a class called Job.cs

The Job.cs class will implement the Payment.cs functionalities which would be used for automated transactions.

Job.cs Class

using AutomatedPaymentAlgorand.Services;
using System.Threading.Tasks;

namespace AutomatedPaymentAlgorand
{
    public class Job
    {
        private readonly IPaymentService _payments;

        public Job(IPaymentService payments)
        {
            _payments = payments;
        }
        public Task OneTimePaymentJobAsync()
        {
            var result = _payments.OneTimePayment();
            return result;
        }

        public Task GroupedPaymentsJobAsnyc()
        {
            var result = _payments.GroupedPayments();
            return result;
        }
    }
}

4. Configure/Setup Database

In this step, we will Setup the database that will work with Hangfire.
Hangfire has an option to store all the job-related information in a database. For this, we just need to create a database.
Follow the steps as shown below to create/setup a database.

Open the Server Explorer tab and click on the database icon as shown in Fig 4-1 below to connect to a database.
EditorImages/2021/05/14 12:44/2021-05-14_11_58_52-Window.png
Figure 4-1 Open Server Explorer to Setup/Create Database

Proceed to setup the database following the steps in Figure 4-2 below
Fill in the server name as shown below as shown in Figure 4-2 if you are using the in-built MSSQL database with Visual Studio.
Select or Enter the database name you want to use for the project and create the database.
EditorImages/2021/05/14 12:48/2021-05-14_12_05_35-Window.png
Figure 4-2 Setup Database Name and Information
Click Yes to proceed as shown in Fig 4-3. This will create the database.
EditorImages/2021/05/14 12:54/2021-05-14_12_09_45-Window.png
Figure 4-3 Create Database

5. Configure/Setup the Startup class

In this step, we will Configure the connection string, Hangfire, services injection in the startup file and also process the background tasks with Hangfire.

Startup.cs Class

using AutomatedPaymentAlgorand.Services;
using Hangfire;
using HangfireBasicAuthenticationFilter;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace AutomatedPaymentAlgorand
{
    public class Startup
    {
        private static IPaymentService paymentService;
        private readonly Job jobscheduler = new Job(paymentService);
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {

            services.AddControllers();
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "AutomatedPaymentAlgorand", Version = "v1" });
            });

            #region Configure Hangfire  
            services.AddHangfire(c => c.UseSqlServerStorage(Configuration.GetConnectionString("myconn")));
            GlobalConfiguration.Configuration.UseSqlServerStorage(Configuration.GetConnectionString("myconn")).WithJobExpirationTimeout(TimeSpan.FromDays(7));
            #endregion

            #region Services Injection  
            services.AddTransient<IPaymentService, PaymentService>();
            #endregion
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IBackgroundJobClient backgroundJobClient, IRecurringJobManager recurringJobManager)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseSwagger();
                app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "AutomatedPaymentAlgorand v1"));
            }

            #region Configure Hangfire  
            app.UseHangfireServer();

            //Basic Authentication added to access the Hangfire Dashboard  
            app.UseHangfireDashboard("/hangfire", new DashboardOptions()
            {
                AppPath = null,
                DashboardTitle = "Hangfire Dashboard",
                Authorization = new[]{
                new HangfireCustomBasicAuthenticationFilter{
                    User = Configuration.GetSection("HangfireCredentials:UserName").Value,
                    Pass = Configuration.GetSection("HangfireCredentials:Password").Value
                }
            },
                //Authorization = new[] { new DashboardNoAuthorizationFilter() },  
                //IgnoreAntiforgeryToken = true  
            }); ;
            #endregion

            app.UseHttpsRedirection();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
            #region Job Scheduling Tasks
            // Recurring One Time Payment Job for every 5 min
            recurringJobManager.AddOrUpdate("Insert Tasks : Runs Every 5 Min", () => jobscheduler.OneTimePaymentJobAsync(), "*/5 * * * *");

            // Recurring Atomic Transfer Job for every 5 min
            recurringJobManager.AddOrUpdate("Insert Tasks : Runs Every 5 Min", () => jobscheduler.GroupedPaymentsJobAsnyc(), "*/5 * * * *");

            //Fire and forget job 
            var jobId = backgroundJobClient.Enqueue(() => jobscheduler.OneTimePaymentJobAsync());

            //Schedule Job / Delayed Job

            backgroundJobClient.Schedule(() => jobscheduler.GroupedPaymentsJobAsnyc(), TimeSpan.FromDays(29));
            #endregion
        }
    }
}

This will process the necessary tasks needed for our automated transactions on the Algorand Network.
There are 4 types of jobs that mostly we will use. We have created all 4 jobs in a startup.cs file.

  • Recurring job - Every 5 minutes the background task runs and processes payments.
  • Fire and Forget - This job runs only once when we run the application.
  • Schedule Job - If you want to schedule a task to run at a particular time.

6. Configure/Setup the appsettings.json file

In this step, we will setup our appsettings.json file to configure our hangfire dashboard (username and password) for authentication and our database configuration Connection string.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "ConnectionStrings": {
    "myconn": "Server=(localdb)\\mssqllocaldb;Database=Hangfiretest.Dev;Trusted_Connection=True;MultipleActiveResultSets=true"
  },
  "AllowedHosts": "*",
  "HangfireCredentials": {
    "UserName": "admin",
    "Password": "admin@123"
  }
}

To Secure the hangfire dashboard we setup login authentication in order to access the hangfire dashboard. We have hardcoded the username and password in the appsettings.js file to consume those in the startup.cs as shown in step 4.

7. Fund Account on Testnet

The following tutorial is for demo purposes and is being worked on the TestNet, no real transactions will be made.
Visit the: TestNet Bank to fund the account.
Check the “I am not a Robot box”, paste in your account address (master account address used to fund other accounts) in the field below as shown in Figure 7-1:
EditorImages/2021/04/25 16:38/2021-02-26_22_39_25-Window_1.png
Figure 7-1 Fund the Accounts Using Algorand Dispenser

Visit https://testnet.algoexplorer.io/
As shown in Fig 6-2 below, search for your account details via the Search bar and make sure it is running on Testnet.
This is to keep track of ongoing transactions as we run the application.

EditorImages/2021/04/25 16:41/2021-04-25_17_40_29-Window.png
Figure 7-2 Algo Explorer Dashboard

8. Run the application

By default, the swagger endpoint will open.
Now type hangfire in the URL by removing the swagger. It will ask for a username and password as we had already set up the authentication mechanism.
EditorImages/2021/04/25 16:43/h12.png
Figure 8-1 Hangifre Dashboard Authentication

If you click on jobs and succeed then we will see the job execution status and its time, and also we can see the historical graph in the dashboard as well.

EditorImages/2021/05/14 13:34/2021-05-14_14_30_47-Window.png
Figure 8-2 Hangifre Dashboard

EditorImages/2021/05/14 13:34/2021-05-14_14_32_00-Window.png
Figure 8-3 Hangifre Dashboard

9. Download Complete Source Code

Source Code can be found on GitHub

10. View Complete Tutorial Video