Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

notify telegram webhook #7

Merged
merged 2 commits into from
Jan 1, 2025
Merged

notify telegram webhook #7

merged 2 commits into from
Jan 1, 2025

Conversation

technophile-04
Copy link
Member

@technophile-04 technophile-04 commented Dec 23, 2024

Description:

Went with simpler approach. Exposed an endpoint from bot side which when hit by some server with valid secret (WEBHOOK_SECRET) will send notification to configured group.

Another approach would be using PostgreSQL's LISTEN/NOTIFY where we use ENS-PG postgres DB directly in TG bot and setup LISTEN/NOTIFY triggers wherever an insert in grant and stage table happens.

To test:

In this repo:

  1. Switch to this branch.

  2. Add the following variables in .env.local:

POSTGRES_URL="postgresql://postgres:mysecretpassword@localhost:5432/postgres"
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=somereallysecretsecret

TELEGRAM_BOT_URL=http://localhost:8080
TELEGRAM_WEBHOOK_SECRET=your_secret_here
  1. Setup the dev environment:

    Update `Stream.sol` to lower frequency
    // SPDX-License-Identifier: MIT
    pragma solidity >=0.8.0 <0.9.0;
    
    import "@openzeppelin/contracts/access/AccessControl.sol";
    
    contract Stream is AccessControl {
        bytes32 public constant OWNER_ROLE = keccak256("OWNER_ROLE");
    
        struct GrantStream {
    	    uint256 cap;
    	    uint256 last;
    	    uint256 amountLeft;
    	    uint8 grantNumber;
    	    uint8 stageNumber;
    	    address builder;
        }
    
        struct BuilderGrantData {
    	    uint256 grantId;
    	    uint8 grantNumber;
        }
    
        mapping(uint256 => GrantStream) public grantStreams;
        uint256 public nextGrantId = 1;
    
        mapping(address => BuilderGrantData[]) public builderGrants;
    
        uint256 public constant FULL_STREAM_UNLOCK_PERIOD = 60; // 1 min
        uint256 public constant DUST_THRESHOLD = 1000000000000000; // 0.001 ETH
    
        event Withdraw(
    	    address indexed to,
    	    uint256 amount,
    	    string reason,
    	    uint256 grantId,
    	    uint8 grantNumber,
    	    uint8 stageNumber
        );
        event AddGrant(uint256 indexed grantId, address indexed to, uint256 amount);
        event ReinitializeGrant(
    	    uint256 indexed grantId,
    	    address indexed to,
    	    uint256 amount
        );
        event MoveGrantToNextStage(
    	    uint256 indexed grantId,
    	    address indexed to,
    	    uint256 amount,
    	    uint8 grantNumber,
    	    uint8 stageNumber
        );
        event ReinitializeNextStage(
    	    uint256 indexed grantId,
    	    address indexed builder,
    	    uint256 amount,
    	    uint8 grantNumber,
    	    uint8 stageNumber
        );
        event UpdateGrant(
    	    uint256 indexed grantId,
    	    address indexed to,
    	    uint256 cap,
    	    uint256 last,
    	    uint256 amountLeft,
    	    uint8 grantNumber,
    	    uint8 stageNumber
        );
        event AddOwner(address indexed newOwner, address indexed addedBy);
        event RemoveOwner(address indexed removedOwner, address indexed removedBy);
    
        // Custom errors
        error NoActiveStream();
        error InsufficientContractFunds();
        error UnauthorizedWithdrawal();
        error InsufficientStreamFunds();
        error FailedToSendEther();
        error PreviousAmountNotFullyWithdrawn();
        error AlreadyWithdrawnFromGrant();
    
        constructor(address[] memory _initialOwners) {
    	    _setRoleAdmin(OWNER_ROLE, OWNER_ROLE);
    	    for (uint i = 0; i < _initialOwners.length; i++) {
    		    _grantRole(OWNER_ROLE, _initialOwners[i]);
    	    }
        }
    
        function unlockedGrantAmount(
    	    uint256 _grantId
        ) public view returns (uint256) {
    	    GrantStream memory grantStream = grantStreams[_grantId];
    	    if (grantStream.cap == 0) revert NoActiveStream();
    
    	    if (grantStream.amountLeft == 0) {
    		    return 0;
    	    }
    
    	    uint256 elapsedTime = block.timestamp - grantStream.last;
    	    uint256 unlockedAmount = (grantStream.cap * elapsedTime) /
    		    FULL_STREAM_UNLOCK_PERIOD;
    
    	    return
    		    unlockedAmount > grantStream.amountLeft
    			    ? grantStream.amountLeft
    			    : unlockedAmount;
        }
    
        function addGrantStream(
    	    address _builder,
    	    uint256 _cap,
    	    uint8 _grantNumber
        ) public onlyRole(OWNER_ROLE) returns (uint256) {
    	    // check if grantStream with same grantNumber already exists
    	    uint256 existingGrantId;
    	    BuilderGrantData[] memory existingBuilderGrants = builderGrants[
    		    _builder
    	    ];
    	    for (uint i = 0; i < existingBuilderGrants.length; i++) {
    		    GrantStream memory existingGrant = grantStreams[
    			    existingBuilderGrants[i].grantId
    		    ];
    		    if (existingGrant.grantNumber == _grantNumber) {
    			    if (existingGrant.cap != existingGrant.amountLeft) {
    				    revert AlreadyWithdrawnFromGrant();
    			    }
    			    existingGrantId = existingBuilderGrants[i].grantId;
    			    break;
    		    }
    	    }
    
    	    // update existing grant or create new one
    	    uint256 grantId = existingGrantId != 0
    		    ? existingGrantId
    		    : nextGrantId++;
    
    	    grantStreams[grantId] = GrantStream({
    		    cap: _cap,
    		    last: block.timestamp,
    		    amountLeft: _cap,
    		    grantNumber: _grantNumber,
    		    stageNumber: 1,
    		    builder: _builder
    	    });
    
    	    if (existingGrantId == 0) {
    		    builderGrants[_builder].push(
    			    BuilderGrantData({
    				    grantId: grantId,
    				    grantNumber: _grantNumber
    			    })
    		    );
    		    emit AddGrant(grantId, _builder, _cap);
    	    } else {
    		    emit ReinitializeGrant(grantId, _builder, _cap);
    	    }
    	    return grantId;
        }
    
        function moveGrantToNextStage(
    	    uint256 _grantId,
    	    uint256 _cap
        ) public onlyRole(OWNER_ROLE) {
    	    GrantStream storage grantStream = grantStreams[_grantId];
    	    if (grantStream.cap == 0) revert NoActiveStream();
    
    	    // If amountLeft equals cap, reinitialize with same stage number
    	    if (grantStream.amountLeft == grantStream.cap) {
    		    grantStream.cap = _cap;
    		    grantStream.last = block.timestamp;
    		    grantStream.amountLeft = _cap;
    		    // Stage number remains the same
    		    emit ReinitializeNextStage(
    			    _grantId,
    			    grantStream.builder,
    			    _cap,
    			    grantStream.grantNumber,
    			    grantStream.stageNumber
    		    );
    	    } else {
    		    if (grantStream.amountLeft > DUST_THRESHOLD)
    			    revert PreviousAmountNotFullyWithdrawn();
    
    		    if (grantStream.amountLeft > 0) {
    			    (bool sent, ) = payable(grantStream.builder).call{
    				    value: grantStream.amountLeft
    			    }("");
    			    if (!sent) revert FailedToSendEther();
    		    }
    
    		    grantStream.cap = _cap;
    		    grantStream.last = block.timestamp;
    		    grantStream.amountLeft = _cap;
    		    grantStream.stageNumber += 1;
    
    		    emit MoveGrantToNextStage(
    			    _grantId,
    			    grantStream.builder,
    			    _cap,
    			    grantStream.grantNumber,
    			    grantStream.stageNumber
    		    );
    	    }
        }
    
        function updateGrant(
    	    uint256 _grantId,
    	    uint256 _cap,
    	    uint256 _last,
    	    uint256 _amountLeft,
    	    uint8 _stageNumber
        ) public onlyRole(OWNER_ROLE) {
    	    GrantStream storage grantStream = grantStreams[_grantId];
    	    if (grantStream.cap == 0) revert NoActiveStream();
    	    grantStream.cap = _cap;
    	    grantStream.last = _last;
    	    grantStream.amountLeft = _amountLeft;
    	    grantStream.stageNumber = _stageNumber;
    
    	    emit UpdateGrant(
    		    _grantId,
    		    grantStream.builder,
    		    _cap,
    		    grantStream.last,
    		    grantStream.amountLeft,
    		    grantStream.grantNumber,
    		    grantStream.stageNumber
    	    );
        }
    
        function streamWithdraw(
    	    uint256 _grantId,
    	    uint256 _amount,
    	    string memory _reason
        ) public {
    	    if (address(this).balance < _amount) revert InsufficientContractFunds();
    	    GrantStream storage grantStream = grantStreams[_grantId];
    	    if (grantStream.cap == 0) revert NoActiveStream();
    	    if (msg.sender != grantStream.builder) revert UnauthorizedWithdrawal();
    
    	    uint256 totalAmountCanWithdraw = unlockedGrantAmount(_grantId);
    	    if (totalAmountCanWithdraw < _amount) revert InsufficientStreamFunds();
    
    	    uint256 elapsedTime = block.timestamp - grantStream.last;
    	    uint256 timeToDeduct = (elapsedTime * _amount) / totalAmountCanWithdraw;
    
    	    grantStream.last = grantStream.last + timeToDeduct;
    	    grantStream.amountLeft -= _amount;
    
    	    (bool sent, ) = msg.sender.call{ value: _amount }("");
    	    if (!sent) revert FailedToSendEther();
    
    	    emit Withdraw(
    		    msg.sender,
    		    _amount,
    		    _reason,
    		    _grantId,
    		    grantStream.grantNumber,
    		    grantStream.stageNumber
    	    );
        }
    
        function getBuilderGrantCount(
    	    address _builder
        ) public view returns (uint256) {
    	    return builderGrants[_builder].length;
        }
    
        function addOwner(address newOwner) public onlyRole(OWNER_ROLE) {
    	    grantRole(OWNER_ROLE, newOwner);
    	    emit AddOwner(newOwner, msg.sender);
        }
    
        function removeOwner(address owner) public onlyRole(OWNER_ROLE) {
    	    revokeRole(OWNER_ROLE, owner);
    	    emit RemoveOwner(owner, msg.sender);
        }
    
        function getGrantIdByBuilderAndGrantNumber(
    	    address _builder,
    	    uint8 _grantNumber
        ) public view returns (uint256) {
    	    for (uint256 i = 0; i < builderGrants[_builder].length; i++) {
    		    if (builderGrants[_builder][i].grantNumber == _grantNumber) {
    			    return builderGrants[_builder][i].grantId;
    		    }
    	    }
    	    return 0;
        }
    
        receive() external payable {}
    
        fallback() external payable {}
    }
    
  • Update export const MINIMAL_VOTES_FOR_FINAL_APPROVAL = 1;
  1. scaffold.config.ts => chains.hardhat

bot repo:

Follow the README instructions in https://github.com/technophile-04/ens-pg-bot

Testing:

  1. Create a grant and it should be notified in the group
  2. New stage proposal will be also notified.

Copy link

vercel bot commented Dec 23, 2024

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
ens-pg ✅ Ready (Inspect) Visit Preview 💬 Add feedback Dec 23, 2024 6:40am

Copy link
Member

@rin-st rin-st left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks and works great to me! Gj!

@rin-st
Copy link
Member

rin-st commented Dec 23, 2024

One nitpick though, lets change the "Milestone" text to "Planned milestones" for new stages?

image

Copy link
Member

@Pabl0cks Pabl0cks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome job @technophile-04 !! Working great to me 🙏

Some nitpick/improvement we could tackle in a future PR:

  • If grantee registers a github URL without http, for example: "github.com/telegramBot", we need to add an extra check to form the url correctly. Right now the bot would send "https://github.com/github.com/telegramBot"
  • It would be awesome to add ENS resolution in the "Builder" field from the bot msg.

@technophile-04
Copy link
Member Author

  • change the "Milestone" text to "Planned milestones"
  • If grantee registers a github URL without http, for example: "github.com/telegramBot", we need to add an extra check to form the url correctly. Right now the bot would send "https://github.com/github.com/telegramBot"

Updated both the things, Tysm all merging this 🙌

  • It would be awesome to add ENS resolution in the "Builder" field from the bot msg.

will create an issue for this 🙌

@technophile-04 technophile-04 merged commit 22cfc17 into main Jan 1, 2025
3 checks passed
@technophile-04
Copy link
Member Author

technophile-04 commented Jan 1, 2025

Ohh also forgot to mention, I did some research weather having await here on this line is good or not:

await notifyTelegramBot("grant", {

Like we want that part of the code to be "fire and forgot" but since we have added await their JS will wait until this telegram bot notfication is done/fail and then send feedback(response) to frontend that grant submitted successfully.

Ideally if we would have remove await from their it would have been fire and fogot but since we are in serverless land with vercel. Once the http response is send to client vercel terminates all the pending promises. So their might be cases where vercel terminates that code before making webhook request.

Digging in more I found waitUntil which tells vercel to don't terminate the promise even if the response was send.

But while playing around with waitUntil I didn't see any improvements with it or without it. So that's why didn't push that part of the code. If we get feedback from the grantees that form submission is too slow then we can tinker more with waitUntil or research some more alternate paths 🙌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants