" MicromOne

Pagine

Building a Complete Collateralized Lending DApp on Ethereum Sepolia: From Smart Contract to Verified Deployment

Developing a blockchain application is not just about writing a Solidity contract. A production-ready decentralized application requires a complete lifecycle that includes smart contract engineering, automated testing, network deployment, frontend integration, wallet connectivity, contract verification, and operational troubleshooting.

This article describes the entire development process of a collateralized lending protocol deployed on Ethereum Sepolia and explains the technical decisions, common pitfalls, debugging methods, and deployment practices required to recreate the project from scratch.

System Architecture

The application is composed of three main layers:

Frontend
   │
   ▼
MetaMask Wallet
   │
   ▼
Ethers.js
   │
   ▼
CollateralizedLoan Smart Contract
   │
   ▼
Ethereum Sepolia Testnet

The frontend executes in the browser.

MetaMask manages private keys and transaction signing.

Ethers.js acts as the communication layer between the user interface and the blockchain.

The Solidity smart contract stores loan data and enforces all business rules.

The Business Model

The protocol implements a collateralized peer-to-peer lending mechanism.

There are two actors:

Borrower

The borrower deposits collateral and requests a loan.

The borrower is required to lock cryptocurrency inside the smart contract before receiving any funds.

Lender

The lender funds a loan request.

The lender receives interest when the borrower repays.

If the borrower fails to repay before the deadline, the lender can claim the collateral.

Smart Contract Design

The core data structure is a loan object:

struct Loan {
    address payable borrower;
    address payable lender;
    uint256 collateralAmount;
    uint256 loanAmount;
    uint256 interestRate;
    uint256 dueDate;
    bool isFunded;
    bool isRepaid;
}

The loan structure contains:

  • borrower address
  • lender address
  • collateral amount
  • loan amount
  • interest rate
  • due date
  • funding status
  • repayment status

These values are stored in:

mapping(uint256 => Loan) public loans;

Every loan receives a unique identifier:

uint256 public nextLoanId;

Creating Loans

A borrower creates a loan using:

depositCollateralAndRequestLoan()

The process includes:

  1. Sending collateral
  2. Defining interest rate
  3. Defining loan duration
  4. Generating loan record

Typical validation rules include:

require(
    msg.value > 0,
    "Collateral must be greater than zero"
);
require(
    duration > 0,
    "Duration must be greater than zero"
);

Funding Loans

Funding is performed through:

fundLoan()

Important protections:

Borrower cannot fund own loan

and

Loan is already funded

The lender transfers the exact loan amount.

Once funded:

loan.isFunded = true;

and the loan principal is released to the borrower.

Repayment

Borrowers repay using:

repayLoan()

The repayment amount is calculated through:

getRepaymentAmount()

Formula:

Repayment = Principal + Interest

Example:

Loan Amount = 1 ETH
Interest Rate = 500

where

500 = 5%

Results:

Repayment = 1.05 ETH

The lender automatically receives the repayment.

The collateral is returned to the borrower.

Default and Collateral Liquidation

If a borrower fails to repay before:

dueDate

the lender may execute:

claimCollateral()

This transfers the collateral to the lender.

The protocol therefore protects the lender against default risk.

Automated Testing

Hardhat test automation is critical.

The project was validated using:

Mocha
Chai
Hardhat Toolbox

Tests included:

Deployment

nextLoanId()

verification

Loan Creation

Successful loan generation

Funding

Correct funding amount validation

Repayment

Interest calculations

Liquidation

Collateral claim after expiration

Error Handling

Validation of all:

require()

conditions

including:

Loan is not funded
Only borrower can repay
Only lender can claim collateral
Loan is already funded
Collateral must be greater than zero

Deploying to Sepolia

Deployment uses Hardhat.

Example:

npx hardhat clean
npx hardhat compile

followed by

npx hardhat run scripts/deploy.js --network sepolia-testnet

A successful deployment outputs:

CollateralizedLoan deployed successfully

and returns the contract address.

Example:

0x574483ba69EaE314988cC0eDBFbfea17B53759fE

Frontend Integration

The frontend uses:

loaded directly via CDN.

Connection process:

provider =
    new ethers.BrowserProvider(
        window.ethereum
    );

Wallet connection:

await provider.send(
    "eth_requestAccounts",
    []
);

Contract binding:

contract =
    new ethers.Contract(
        CONTRACT_ADDRESS,
        ABI,
        signer
    );

Common Problem: Wrong Network

One of the most frequent mistakes is confusing:

Ethereum Mainnet

with:

Sepolia Testnet

Chain IDs:

1          = Ethereum Mainnet
11155111   = Sepolia

The frontend should enforce:

wallet_switchEthereumChain

to guarantee the correct network.

Common Problem: OutOfFunds

A transaction can fail with:

OutOfFunds

for two reasons:

  1. Insufficient balance

or

  1. Wrong network selected

A user may own:

4 ETH on Sepolia

and simultaneously:

0 ETH on Mainnet

because balances are network-specific.

Understanding Smart Contract Errors

Smart contract reverts are useful diagnostic tools.

Example:

Loan is not funded

indicates that a repayment is attempted before funding.

Example:

Only lender can claim collateral

indicates that an unauthorized account attempted liquidation.

Proper interpretation of these errors significantly accelerates debugging.

Etherscan Contract Verification

Deployment alone stores bytecode on-chain.

Verification publishes the source code.

Verified contracts provide:

Read Contract
Write Contract
Contract Source Code
ABI
Events

To verify correctly:

  • use the exact Solidity version
  • use the same optimization settings
  • use the original deployment source

A very common failure is:

Unable to find matching Contract Bytecode and ABI

which indicates that the uploaded source generates a different bytecode than the deployed contract.

Optimizer Settings

A critical lesson is understanding compiler optimization.

Two compilations of the same Solidity file can generate different bytecode:

Optimizer OFF

versus

Optimizer ON

Although functionality remains identical, Etherscan compares deployed bytecode bit-for-bit.

If deployment used:

Optimizer = ON
Runs = 200

then verification must use exactly:

Optimizer = ON
Runs = 200

Otherwise verification fails.

Operational Workflow

The complete lifecycle of a collateralized loan is:

Borrower
    ↓
Create Loan
    ↓
Lender
    ↓
Fund Loan
    ↓
Borrower
    ↓
Repay Loan
OR
Borrower
    ↓
Default
    ↓
Lender
    ↓
Claim Collateral

This lifecycle demonstrates a complete decentralized financial workflow implemented through immutable smart contract logic rather than traditional server-side software.

Building a lending DApp is far more than writing Solidity. The real engineering effort lies in connecting multiple domains:

  • smart contracts
  • blockchain security
  • automated testing
  • wallet integration
  • frontend development
  • network deployment
  • verification pipelines
  • debugging infrastructure

By combining Hardhat, Solidity, MetaMask, Ethers.js, Sepolia, and Etherscan, it is possible to create a full-stack decentralized application that mirrors real-world financial workflows while maintaining transparency, auditability, and trustless execution.


How to Add a Character Counter to a Text Field in Microsoft Dynamics 365 CRM Using JavaScript

 When customizing Microsoft Dynamics 365 Customer Engagement, small user interface improvements can make a big difference in the overall user experience.

One common requirement is to display a character counter for a text field. This feature allows users to see how many characters they have entered and how many are still available before reaching the maximum length.

In this article, we will explore how to implement a dynamic character counter for the Title field (dps_title) in Microsoft Dynamics 365 CRM using JavaScript.



Why Add a Character Counter?

In Dynamics 365 CRM, text fields often have a maximum length. Although the platform enforces this limit, users may not always know how many characters they have entered while typing.

A character counter provides immediate feedback and helps users:

  • Avoid exceeding the maximum character limit.

  • Write more concise and accurate titles.

  • Improve data quality and consistency.

  • Enjoy a better user experience when completing CRM forms.

For example, if the maximum length is 100 characters and the user has entered 35 characters, the counter displays:

35/100

The counter updates dynamically whenever the user changes the field value.

Understanding the Implementation

The solution uses JavaScript to locate the HTML input associated with the dps_title field, create a counter element, and update it whenever the user types.

The implementation consists of three main steps:

  1. Initialize the character counter when the CRM form loads.

  2. Locate the title input element in the form's document.

  3. Update the counter whenever the input value changes.

Step 1: Initialize the Character Counter

First, we define a function called InitTitleCharCounter.

_self.InitTitleCharCounter = function () {
    var dataId = "dps_title.fieldControl-text-box-text";

    var interval = setInterval(function () {
        var parentDoc = window.parent.document;

        var input = parentDoc.querySelector(
            '[data-id="' + dataId + '"]'
        );

        if (!input) {
            return;
        }

        clearInterval(interval);

        if (input.dataset.charCounterInitialized === "true") {
            return;
        }

        input.dataset.charCounterInitialized = "true";

        var maxLength =
            parseInt(input.getAttribute("maxlength"), 10) || 100;

        var counter = parentDoc.createElement("div");

        counter.style.fontSize = "12px";
        counter.style.color = "#666";
        counter.style.textAlign = "right";
        counter.style.marginTop = "2px";

        var updateCounter = function () {
            var length = input.value ? input.value.length : 0;
            counter.textContent = length + "/" + maxLength;
        };

        if (input.parentNode) {
            input.parentNode.insertBefore(
                counter,
                input.nextSibling
            );
        }

        updateCounter();

        input.addEventListener("input", updateCounter);
    }, 500);
};

Let's look at the most important parts of the code.

Locating the input field

var dataId = "dps_title.fieldControl-text-box-text";

This variable identifies the HTML input element associated with the Title field.

The following selector searches for that element:

var input = parentDoc.querySelector(
    '[data-id="' + dataId + '"]'
);

The selector is specific to the rendered Dynamics 365 form and may need to be adjusted if the field's HTML structure changes.

Waiting for the field to become available

var interval = setInterval(function () {
    // Search for the input field
}, 500);

Dynamics 365 forms can render their controls asynchronously. The input may not be available immediately when the form's OnLoad event executes.

Using setInterval allows the script to check every 500 milliseconds until the field is found.

Once the input becomes available, clearInterval(interval) stops further checks.

Preventing duplicate initialization

if (input.dataset.charCounterInitialized === "true") {
    return;
}

input.dataset.charCounterInitialized = "true";

This check prevents the same input from being initialized more than once.

It is particularly useful when form events or custom scripts can be triggered repeatedly.

Reading the maximum length

var maxLength =
    parseInt(input.getAttribute("maxlength"), 10) || 100;

The script reads the maxlength attribute from the input element.

If the attribute is missing or its value cannot be parsed, the counter uses 100 as a fallback.

It is important to configure the fallback according to the actual maximum length of your field.

Creating the counter element

var counter = parentDoc.createElement("div");

counter.style.fontSize = "12px";
counter.style.color = "#666";
counter.style.textAlign = "right";
counter.style.marginTop = "2px";

A new HTML div is created to display the character count.

The styling keeps the counter small, discreet, and right-aligned beneath the field.

The counter is then inserted immediately after the input element:

input.parentNode.insertBefore(
    counter,
    input.nextSibling
);

Updating the counter dynamically

var updateCounter = function () {
    var length = input.value ? input.value.length : 0;
    counter.textContent = length + "/" + maxLength;
};

This function calculates the current length of the input value and displays it alongside the maximum allowed length.

The input event ensures that the counter updates when the user types, pastes text, or otherwise changes the input value.

input.addEventListener("input", updateCounter);

Finally, calling updateCounter() immediately initializes the counter with the current field value.

Step 2: Register the Function on the Form OnLoad Event

After defining the counter function, we need to call it when the Dynamics 365 form loads.

_self.OnLoad = async function (executionContext) {

    if (executionContext !== null) {
        _formContext = executionContext.getFormContext();
        _executionContext = executionContext;
    }

    utilities = DPS.utilities;
    utilities.load(_formContext);

    _self.InitTitleCharCounter();
};

In this example, the existing form initialization logic is preserved.

The execution context provides access to the current form context, while the existing DPS.utilities initialization remains unchanged.

The following line starts the character counter initialization:

_self.InitTitleCharCounter();

Make sure that the JavaScript web resource is registered with the appropriate form event handler and that the execution context is passed to the OnLoad function.

If your existing application uses a different initialization pattern, integrate the call into that pattern instead of replacing other required form logic.

Step 3: Expected Result

Once the form loads and the Title input becomes available, the counter appears directly below the field.

For example:

User actionCounter display
The field is empty0/100
The user enters 12 characters12/100
The user enters 47 characters47/100
The user reaches the limit100/100

The counter updates immediately as the user edits the field.

Note: The character counter is an informational UI enhancement. It does not replace Dynamics 365's built-in maximum-length validation or server-side validation.

Important Considerations

Although this approach can work in a specific Dynamics 365 environment, there are a few important considerations.

1. Accessing the parent document

The code uses:

var parentDoc = window.parent.document;

This assumes that the parent document contains the target input and that browser security restrictions allow access to it.

However, Dynamics 365 model-driven apps use a managed user interface, and accessing internal DOM elements is not a supported public form customization API. Such selectors and DOM manipulations may break after platform updates.

For a production implementation, test the solution carefully in your specific environment and consider a supported custom page or PCF control when a durable custom UI is required.

2. Keep the fallback consistent

If the actual field limit is 255 characters, change the fallback accordingly:

var maxLength =
    parseInt(input.getAttribute("maxlength"), 10) || 255;

The counter should reflect the actual configured limit, rather than an arbitrary value.

3. Consider field re-rendering

The implementation stops polling after finding the input. If Dynamics 365 later replaces the input element during a form refresh or UI re-render, the counter and event listener may no longer be attached to the active element.

A more robust implementation would account for the lifecycle of the control and avoid unnecessary repeated polling.

4. Consider Unicode characters

JavaScript's string.length counts UTF-16 code units, not necessarily user-perceived characters. Some emoji and other Unicode symbols can therefore count as two or more units.

For standard CRM text input, value.length is often sufficient, but the counting method should match the validation rules and expected behavior of the application.

Adding a character counter to a Dynamics 365 CRM text field is a simple way to improve usability and data entry.

By locating the input, creating a small counter element, and listening for the input event, we can provide users with immediate feedback about the number of characters they have entered.

The key benefits are improved visibility, fewer input errors, and a more user-friendly form experience.

For production solutions, however, it is important to consider the limitations of direct DOM manipulation and prefer supported customization options whenever possible.



Building Lightweight Internal Applications with Object Storage Instead of a Database

 One of the most common misconceptions in software development is the assumption that every application requires a database.

In reality, many internal business solutions do not manage millions of records, complex relationships, transactional workloads, or analytical queries. They typically collect information from users, store it, retrieve it, and occasionally allow searches or updates.

Examples include:

  • Event registrations
  • Internal campaigns
  • Employee check-ins
  • Training enrollments
  • Surveys
  • Feedback platforms
  • Department initiatives
  • Innovation challenges

For these scenarios, a simpler architecture may provide significant advantages in terms of cost, deployment speed, maintainability, and portability.

The solution described in this article was designed around a core principle:

Use object storage as the persistence layer and store business entities as structured JSON documents.

This approach eliminates the need for a traditional database while preserving the ability to manage records, files, searches, and updates.

The Core Idea

The entire architecture revolves around three components:

Static Frontend
Serverless API
Object Storage

The frontend provides the user experience.

The API provides validation and business logic.

The storage layer provides persistence.

The key design decision is that storage becomes the system's database.

Why JSON Instead of SQL

A traditional database introduces several additional components:

Database Server
Database Configuration
Database Security
Schema Management
Backups
Migrations
Maintenance

For small and medium internal applications, these components often represent more complexity than actual business value.

Instead of storing records inside tables, records can be stored inside a structured JSON document.

Example:

{
  "id": "CHK-001",
  "name": "John Doe",
  "email": "john@example.com",
  "location": "Milan",
  "createdAt": "2026-10-19T10:00:00Z"
}

A collection of registrations becomes:

[
  {
    "id": "CHK-001"
  },
  {
    "id": "CHK-002"
  }
]

Conceptually, the JSON file becomes the application's database.

Using Object Storage as the Database

Instead of creating tables, a storage container is used.

Example:

storage
│
├── checkins.json
└── photos/
``

The file:

checkins.json

contains all structured records.

The folder:

photos/

contains uploaded images.

The application therefore manages only two types of information:

Structured Data
Files

This greatly simplifies the architecture.

Creating a New Record

When a user submits a registration form, the process is intentionally straightforward.

The API performs four operations:

Step 1

Read the current JSON file.

Step 2

Deserialize the JSON structure into memory.

Step 3

Append a new record.

Example:

{
  "id": "CHK-005",
  "name": "Jane Smith"
}

Step 4

Serialize the updated collection and overwrite the JSON file.

The workflow becomes:

Read JSON
Modify Data
Write JSON

This is effectively the same logical process performed by many database systems but implemented using a single structured file.

Updating Existing Records

Updating follows the same principle.

The API:

  1. Loads the current JSON.
  2. Locates the target record.
  3. Updates the fields.
  4. Saves the file again.

Example:

Before:

{
  "status": "Pending"
}

After:

{
  "status": "Approved"
}

No database transactions are required for small workloads.

Reading and Searching Information

When users perform a search:

Search by Name
Search by Email
Search by Location

the API loads the JSON content and applies filters.

Example:

record["name"].contains(searchTerm)

Matching records are then returned to the frontend.

For moderate datasets, this approach remains highly efficient and significantly simpler than maintaining a database server.

Managing Uploaded Images

Images are intentionally kept separate from business records.

A photo uploaded by the user is saved as an independent object.

Example:

photos/
 ├── 8742.jpg
 ├── 8743.jpg
 └── 8744.jpg

The JSON record stores only the reference.

{
  "photoUrl": "photos/8742.jpg"
}

This provides several benefits:

  • smaller JSON files;
  • faster loading;
  • simpler backup strategy;
  • independent file lifecycle management.

Why an API Is Still Necessary

A common question is:

"Why not let the frontend modify the JSON directly?"

The answer is control.

The API performs:

Validation
Authorization
Business Rules
Error Handling
Concurrency Management

Without an API, every client would need direct write access to storage, introducing significant security and data consistency risks.

The API becomes the only component allowed to modify persistent information.

Benefits of This Approach

The architecture offers several practical advantages.

Low Cost

Only storage and API execution are required.

Rapid Development

Applications can be delivered in days rather than weeks.

Minimal Maintenance

There are no database servers to administer.

Easy Backup

The entire dataset can be backed up simply by copying:

checkins.json
photos/

Easy Migration

Moving platforms becomes straightforward because all information already exists in portable formats.


Portability and Self-Hosting

One of the strongest aspects of this architecture is that it remains platform-independent.

The same logical design can run:

In the Cloud

Storage Account
Function App
Static Website

On a Home Server

Nginx
Python API
JSON File
Photos Folder

On a Virtual Machine

Apache
Node.js API
Local Storage

Because the application relies on standard concepts rather than platform-specific services, migration becomes relatively simple.

The storage provider may change, but the application logic remains identical.

When This Architecture Makes Sense

This model is particularly effective when:

  • the number of records is limited;
  • the application is internal;
  • development speed is important;
  • operational costs should remain low;
  • relational data structures are unnecessary.

Typical examples include:

Registration Systems
Check-In Platforms
Training Portals
Employee Surveys
Internal Campaigns
Event Management Applications

The most important architectural decision in this solution was recognizing that a database was not a business requirement.

The real requirement was persistence.

By using object storage to manage:

checkins.json
photos/

and delegating all operations to a lightweight API layer, it was possible to build a solution that remains simple, maintainable, portable, and inexpensive.

Rather than starting with infrastructure, the design starts with the data itself and asks a fundamental question:

"What is the simplest reliable mechanism capable of storing, retrieving, updating, and protecting this information?"

For a large category of internal applications, the answer is often not a database, but a well-structured storage architecture built around JSON documents and object repositories.

Why AI Could Push Out Bitcoin Miners, According to Vitalik Buterin

The cryptocurrency mining landscape is facing a massive structural shift, and the catalyst isn’t a change in Bitcoin’s code, but the explosive rise of Artificial Intelligence (AI). Ethereum co-founder Vitalik Buterin recently highlighted a fundamental economic divergence that is driving Bitcoin miners to convert their data centers into AI inference hubs.

At the heart of this transition is a stark difference in how costs are structured between securing a blockchain and running AI models.
The Economic Split: Bitcoin vs. AI
According to Buterin, traditional Bitcoin mining operates on a relatively balanced cost structure. The total expenses are split almost equally:
  • 50% goes toward purchasing specialized hardware (ASICs).
  • 50% goes toward ongoing electricity and energy consumption.
Artificial Intelligence inference—the process of running live data through a trained AI model—follows a completely different financial model. AI workloads are heavily front-loaded, where hardware costs dominate almost the entire budget, while electricity represents a fraction of the operational cost.
Why Miners Are Switching Gears
This economic disparity creates a unique opportunity for data center operators. Bitcoin mining is a highly competitive, low-margin business where success depends entirely on securing the cheapest possible electricity. If energy prices spike, a mining operation can instantly become unprofitable.
AI infrastructure, while requiring massive upfront investments in high-end GPUs, is less sensitive to fluctuating energy grid costs. Because the profit margins for AI compute are currently much higher than Bitcoin mining, operators with existing data center infrastructure (like cooling systems and high-voltage grid connections) are finding it incredibly lucrative to pivot.
The Bottom Line
As demand for AI processing power continues to skyrocket, Bitcoin miners holding valuable data center real estate are realizing they can get a better return on investment by serving the tech sector rather than mining crypto. While this doesn't mean Bitcoin mining will disappear, it does mean traditional miners are facing unprecedented competition for infrastructure, potentially pushing smaller players out of the market entirely.