Developer friendly store with experimental user2user payment system

Hello, i just want to show you guys what I’ve been working on lately for the jme community (link at the end of the post).

I’ve been writing an alternative to store.jmonkeyengine.org that provides what i believe is a more convenient experience and solves some issues of the current solution.

Improvements / Features

  • Written as a webapp on top of easy to use http apis that can be integrated into existing CI/CD pipelines
  • Automatic import from github repos (and store.jmonkeyengine.org for migration)
  • No more need of approval for submissions and changes
  • No fixed categories, just free text search and hashtags
  • Uses hub.jmonkeyengine.org for authentication and permissions
  • User to user payments for donations and paid assets

The payment system

Payments are implemented using cryptocurrencies, and handled entirely on the public blockchain using smart contracts, for those who are not following the blockchain development, in short, a smart contract is something like a program that becomes immutable once submitted to the blockchain.

If you are not familiar with current year crypto you might dislike this since you might remember how hard it was to use crypto some years ago, but nowadays it is super easy and common to convert crypto into FIAT and all major crypto exchanges offer visa cards that can autoconvert crypto to native currencies, banks are also starting to support crypto wallets and some places (online and offline) are also simply starting to accept major cryptos (we’ve seen opencollective starting to accept them for instance).

So, back to the technical stuff, the inner workings of the payment system are pretty straightforward:

The factory-contract

pragma solidity ^0.8.0;
import "./SellerContract.sol";

contract FactoryContract {

    mapping(address => SellerContract) private addressXContract;
    mapping(address => bool) private addressXContractLock;


    string public baseUrlData;
    string public baseUrlProof;
    string public prefix;
    
    function _compareStrings(string memory a, string memory b) private pure returns(bool) {
        return (keccak256(abi.encodePacked((a))) == keccak256(abi.encodePacked((b))));
    }

    //"https://library.jmonkeyengine.org/nft/" "jme:"
    constructor(string memory _baseUrlData, string memory _baseUrlProof,string memory _prefix){
        baseUrlData=_baseUrlData; 
        baseUrlProof=_baseUrlProof; 
        prefix=_prefix;
    }
     
    /**
     * Create a new contract
     */
    function createContract(string memory ownerUserId) public returns(address contractAddr){
        address payable seller=payable(msg.sender);
        require(!addressXContractLock[seller],"Contract already created for this address.");
        SellerContract sellContract=new SellerContract(seller,ownerUserId,string(abi.encodePacked(prefix,ownerUserId)),baseUrlData,baseUrlProof);
        addressXContract[seller]=sellContract;
        addressXContractLock[seller]=true;
        return address(sellContract);
    }
    
    /**
     * Get contract for address
     */
    function getContractAddr(address  owner,string memory ownerUserId ) public view returns(address contractAddr){
        if(!addressXContractLock[owner])return address(0);
        SellerContract ctr=addressXContract[owner];
        require(_compareStrings(ctr.getSellerId(),ownerUserId),"Invalid owner id?");
        return address(ctr);
    }
    
}

provides a reliable way to spawn predefined seller-contracts for the users

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Burnable.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

contract SellerContract is ERC721Burnable,ERC721Enumerable{
    struct Entry{
        string id;
        uint256 price;
        bool active;
        bool exists;
        string message;
    }
    
     struct Purchase{
        string entryId;
        uint256 price;
        uint refundable;
        bool pendingWithdraw;
        bool exists;
        string buyerId;
        string message;
    }
    
    using Counters for Counters.Counter;

    address payable public seller;
    string public baseUrlData;
    string public baseUrlProof;
    string public userId;
    bool public active;
    
    
    Counters.Counter private purchaseIDCounter;
    mapping(string => Entry) private entries;
    mapping(uint256 => Purchase) private purchases;


    
    // INTERNALS

    /**
     * _seller: address that is selling the token
     * _userId: unique ID of the seller in the store
     * _name: name of this contract
     * _baseuri: base url to get token infos
     */
    constructor(address payable _seller,string memory _userId,string memory _name,string memory _baseUrlData,string memory _baseUrlProof) ERC721(_name,_name) {
        seller=_seller;
        baseUrlData=_baseUrlData;
        baseUrlProof=_baseUrlProof;
        active=true;
        userId=_userId;
        purchaseIDCounter.increment();
    }
    
    function _strcmp(string memory a, string memory b) public pure returns (bool) {
        return (keccak256(abi.encodePacked((a))) == keccak256(abi.encodePacked((b))));
    }
    
    function _getEntry(string memory entryId) private view returns(Entry memory){
        Entry memory entry=entries[entryId];
        require(entry.exists,"Entry not found.");
        return entry;
    }
    
    function _getPurchase(uint256 purchaseId) private view returns(Purchase memory){
        Purchase memory purchase=purchases[purchaseId];
        require(purchase.exists,"Purchase not found.");
        return purchase;
    }
    
    
    function _isRefundable(Purchase memory purchase) private view returns( bool){
        return purchase.exists&&purchase.refundable>0&&purchase.refundable+1200>=block.timestamp;
    }
    
      
    function _isWithdrawPending(Purchase memory purchase) private pure returns( bool){
        return purchase.exists&&purchase.pendingWithdraw;
    }
    
    function _isWithdrawable(Purchase memory purchase) private view returns( bool){
        return  !_isRefundable(purchase)&&_isWithdrawPending(purchase);
    }
    
    
    // ----
    
   // READ ONLY
    
    function getSellerId() public view returns(string memory){
        return userId;
    }
    
        
    function getSellerAddr() public view returns(address){
        return seller;
    }
    
    
    function isContractActive() public view returns(bool){
        return active;
    }
    
    
    function isEntryBuyable(string memory entryId) public view returns(bool){
        Entry memory entry=entries[entryId];
        return entry.exists&&entry.active;
    }
    
    function getEntryPrice(string memory entryId) public view returns(uint256){
        Entry memory entry=_getEntry(entryId);
        return entry.price;
    }

    function getPurchasePrice(uint256 purchaseId) public view returns(uint256){
        Purchase memory purchase = _getPurchase(purchaseId);
        return purchase.price;
    }    
    
    function getPurchaseOwner(uint256 purchaseId) public view returns(address){
        return ownerOf(purchaseId);
    }
    
    function isSeller(address _seller) public view returns(bool){
        return seller==_seller;
    }
    
    
    function getPurchaseId(string memory entryId,address buyer) public view returns(uint256){
        uint256 n=countPurchases(buyer);
        for(uint256 i=0;i<n;i++){
            uint256 id=getPurchaseId(buyer,i);
            Purchase memory purchase = _getPurchase(id);
            if(_strcmp(purchase.entryId,entryId))return id;
        }
        return 0;
    }
    
    
    function countPurchases(address owner) public view returns(uint256){
        return balanceOf(owner);
    }
    
    function getPurchaseId(address owner,uint256 localIndex) public view returns(uint256){
        return tokenOfOwnerByIndex(owner,localIndex);
    }
    
    function countPurchases() public view returns(uint256){
        return purchaseIDCounter.current();
    }

    function isValidPurchase(uint256 purchaseId) public view returns(bool){
        Purchase memory purchase=purchases[purchaseId];
        return purchase.exists;
    }
    

    
    /**
     * Return true of the token is within the withdraw period
     */
    function isRefundable(uint256 purchaseId) public view returns(bool){
        Purchase memory purchase = _getPurchase(purchaseId);
        return _isRefundable(purchase);
    }
    
    
    /**
     * Return true if the payment for this nft is waiting for withdrawal
     */
    function isWithdrawPending(uint256 purchaseId) public view returns(bool){
        Purchase memory purchase=_getPurchase(purchaseId);
        return _isWithdrawPending(purchase);
    }
    
    /**
     * Return true if the payment is withdrawdable and the refund period is expired. 
     * Meaning the payment is ready to be withdrawed
     */
    function isWithdrawable(uint256 purchaseId) public view returns(bool){
        Purchase memory purchase=_getPurchase(purchaseId);
        return _isWithdrawable(purchase);
    }
    
    
    function _baseURI() internal view override returns (string memory) {
        return baseUrlData;
    }
    
    function tokenURI(uint256 purchaseId) public view override returns (string memory) {
        require(_exists(purchaseId), "ERC721Metadata: URI query for nonexistent token");
        Purchase memory purchase=_getPurchase(purchaseId);
        return  string(abi.encodePacked(_baseURI(), "entry=",userId,"/",purchase.entryId));
    }
    
    function purchaseProofURI(uint256 purchaseId)public view  returns (string memory) {
        Purchase memory purchase=_getPurchase(purchaseId);
        return  string(abi.encodePacked(baseUrlProof, "entry=",userId,"/",purchase.entryId,"&owner=",purchase.buyerId));
    }
    

    function getEntryMessage(string memory entryId) public view returns(string memory ){
        Entry memory entry=_getEntry(entryId); 
        return entry.message;
    }
    

    function getPurchaseMessage(uint256 purchaseId) public view returns(string memory){
        Purchase memory purchase=_getPurchase(purchaseId);
        return purchase.message;
    }
   

    //  WRITE
    
    function setEntry(string memory _id,uint256 _price,bool _active,string memory message) public{
        require(active,"This contract has been deactivated by the seller.");
        require(_msgSender()==seller,"Only seller can edit registered entries.");
        require(_price>1,"Price must be >1");
        Entry memory entry=Entry(_id,_price,_active,true,message);
        entries[_id]=entry;
    }
    
    
    /**
     * Active or deactive minting of new tokens
     */
    function setContractActive(bool v) public{
        require(_msgSender()==seller,"Only the seller can activate or deactivate the contract.");
        active=v;
    }
    
    // /**
    // * Active or deactive an entry 
    // */
    // function setEntryActive(string memory entryId,bool v) public{
    //     require(_msgSender()==seller,"Only seller can active or deactive an entry.");
    //     Entry memory entry=_getEntry(entryId);
    //     entry.active=v;
    //     entries[entryId]=entry;
    // }



    /**
    * Buy a token
    */
    function buy(string memory entryId,string memory buyerId) public payable returns(uint256){
        require(active,"This contract has been deactivated by the seller.");
        Entry memory entry=_getEntry(entryId); // get entry 
        require(entry.active,"Entry is not actived."); // check if active
        require(msg.value==entry.price,"Invalid payment. Price is not met"); // check if right price
        
        // Create a purchase 
        uint refundLimit=block.timestamp+(60);
        Purchase memory purchase=Purchase(entry.id,entry.price,refundLimit,true,true,buyerId,entry.message);
        
        // Generate new id for the purchase
        purchaseIDCounter.increment();
        uint256 purchaseId=purchaseIDCounter.current();
        
        // Mint token representing the purchase
        _safeMint(msg.sender,purchaseId);
        
        // Register purchase
        purchases[purchaseId]=purchase;

        return purchaseId;
    }  
    
    /**
     * Burn and refund. If refund is not possible, the token cannot be burnt
     */
    function burn(uint256 purchaseId) public override {
        require(_isApprovedOrOwner(_msgSender(), purchaseId), "ERC721Burnable: caller is not owner nor approved"); // check if caller has the right to burn
       
        Purchase memory purchase=_getPurchase(purchaseId); // get purchase
        require(_isRefundable(purchase),"Can't burn a non refundable token."); // can't burn what we can't refund.
        
        // purchase.refundable=0; // reset refundable state
        // purchase.pendingWithdraw=false; // reset withdrawdable state 
        // purchases[purchaseId]=purchase; // update state

        delete purchases[purchaseId]; // delete purchase proof
        require(purchase.price>0,"Price is <=0?");

        super.burn(purchaseId); // destroy token 
        
        payable(_msgSender()).transfer(purchase.price); // send money back
    }
    
    /**
     * Alias to burn(uint256)
     */
    function refund(uint256 purchaseId) public {
        burn(purchaseId);
    }
    
    /**
     * Withdraw. Can be called by anyone, the payment is submited to seller
     */ 
    function withdraw(uint256 purchaseId) public  {
        Purchase memory purchase=_getPurchase(purchaseId); // get purchase
        require(_isWithdrawable(purchase),"Not withdrawdable"); // check if withdraw is possible.
        
        purchase.pendingWithdraw=false;  // reset withdrawdable state 
        purchase.refundable=0; // reset refundable state
        
        purchases[purchaseId]=purchase; // update state
        require(purchase.price>0,"Price is <=0?");

        seller.transfer(purchase.price); // pay
    }
    
    /**
    * Withdraw multiple payments at once. See withdraw(uint256)
    */ 
    function batchWithdraw(uint256[] memory purchaseIds) public {
        uint256 tot=0;
        for(uint i=0;i<purchaseIds.length;i++){// check if withdraw is possible for everyitem in the array.
            uint256 purchaseId=purchaseIds[i];
            Purchase memory purchase=_getPurchase(purchaseId); // get purchase
            require(_isWithdrawable(purchase),"At least one payment is not withdrawdable"); // check if withdraw is possible.
            
            purchase.pendingWithdraw=false; // reset withdrawdable state 
            purchase.refundable=0;// reset refundable state
            
            purchases[purchaseId]=purchase;// update state

            require(purchase.price>0,"Price is <=0?");

            tot+=purchase.price;// sum total
        }
        
        seller.transfer(tot); // pay all
    }



    // Overrides
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 tokenId
    ) internal virtual override(ERC721, ERC721Enumerable) {
        super._beforeTokenTransfer(from, to, tokenId);
    }

    /**
     * @dev See {IERC165-supportsInterface}.
     */
    function supportsInterface(bytes4 interfaceId)
        public
        view
        virtual
        override( ERC721, ERC721Enumerable)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}

(As you might have noticed this contract is based on ERC721, that’s right, that’s the standard for NFTs.)

This seller contracts allows the user to register entries to sell and set their prices, it also provides refund capabilities since it will lock the funds for 14 days (in the code here i reduced it to some minutes for testing) after a purchase has been made, allowing the buyer to request a refund within this time, consider this like an amazon prime kind of policy.

Obviously all this is done in the background and hidden by the webapp interface, the point here is that this becomes a peer to peer anonymous trustless secure transaction since money never transit on our system and both the seller and buyer are bound only by the contract code and they don’t need to provide any other information to each other, or to anyone, besides their blockchain address.

For now I implemented the bare bone system, more thoughts need to be placed in regard of the distribution of paid assets

Demo

There is a working demo hosted here: https://library.jmonkeyengine.frk.wf/
Feel free to do what you want with it and play around, if it breaks i will restart it :moyai:.
It needs some more love mostly on the layout and code quality, but i wanted to push something out asap so you guys can give some feedback.

The demo payments run on the ethereum testnet, you can get free test coins to try it out (read the message on the top of the page and you’ll find a link to get some), ideally this will be migrated to a mainnet that has lower fees (eg. BSC: BNB mainnet). To use the payment features you’ll need the https://metamask.io/ extension, the website will guide you though setting up the things you need.

Apis

If you want to explore the apis, you can start from here: https://library.jmonkeyengine.frk.wf/apidoc
Eg, to send a valid https://library.jmonkeyengine.frk.wf/entry/get request you’ll need to compile this json document
https://library.jmonkeyengine.frk.wf/apidoc/entry/get/request
send it via POST and you’ll receive this response
https://library.jmonkeyengine.frk.wf/apidoc/entry/get/response
I’ll write a proper visualization for the documentation or something at a later time

4 Likes

Technically, this all sounds great.

Personally, I’m not interested in buying or selling open-source software. But I understand why others might want to do so.

Psychologically, I wonder what might discourage buyers from requesting a refund and then continuing to use the software they’d bought.

Legally, I wonder how value-added tax and/or sales tax are collected in such a system. Also, if we provide such a storefront, what responsibilities (if any) do we have to screen the products, address abuse and complaints, and ensure legal compliance?

1 Like

Good faith, unless some form of drm is implemented in the software, they will just be breaking the license agreement and nothing else, but that is in line with the european refund law that forces you to grant refunds within 14 days for any reason whatsoever.
The difference here is that the smart contract emits a token that can be used to prove the purchase, so it might be required by the license to show it.

Blockchains offer something that never existed before, so we won’t find a clear answer to that, however since those trades happen in a peer to peer fashion among users directly with private contracts, i can see this being like having a section on the forum where users can publish job offers or ads, or having a paypal donation button in your github profile, payments are settled outside the platform so we won’t have any responsibility regarding them. The seller will have to take care of paying its own contributes, if required by its country law.

3 Likes

While I’m not opposed to any blockchain currency, I do not think we should directly implement it. Instead we should use a payment provider who accepts multiple types of currencies. This would put off any legal responsibilities onto such payment provider for having knowledge of local taxes and other currency related regulations. This is especially important considering that many jme users are not based in the US.

As a community, jme does not have the expertise (nor manpower) in legal ramifications on handling payment processing. It would be devastating to the community to be caught in a court case over improper processing of payments, especially for tax compliance for overseas transactions (or any other type of transaction).

1 Like

The whole point here is that with this system we do not process payments

1 Like

Blockchains were invented in 1982 and have been used in currency (Bitcoin) since 2009, so it’s likely the tax questions have been resolved, at least in the United States. If we don’t look for answers, of course we won’t find any.

1 Like

Peer to peer trustless transactions regulated by software contracts that are executed on a distributed network of computers that spans across the entire planet is something that was never done before.
“Never done before” doesn’t mean never done before today 8/12/21, it means that it is part of the innovation to which the mainstream hasn’t caught up yet.

Your government just today had an hearing with several crypto exchanges CEOs specifically to talk about regulation surrounding the crypto space, just as a proof of how much this is new for them.

But in any case, as i said before, a transaction in this system is a transaction between users, it doesn’t transit on the store backend, it transit on the blockchain as it would transit through paypal with the difference that the blockchain isn’t owned by anyone.

The users own their funds, their wallets that give them the authority to spend them and the seller owns the contract that the buyer accepts, transaction also happen outside of the store itself, as they are entirely settled on the blockchain, we just provide half of the graphical interface to that, the other half is provided by the browser extension metamask. With “own” i mean literally own, we won’t have any access to them, the store can just ask the blockchain for public informations and figure out who bought what.

The users who are involved in the transaction need to pay their own contributions according to their country law. I don’t know how things work in usa and I don’t care, please remember that usa is just one of the many countries in the world, if you are from usa you will have to figure out how to pay your contributions if you need to, the same way you would have to do if you were to accept paypal payments.

You are correct, bitcoin was invented in 2009 and blockchains as database concept long before that, however i want to specify that we are talking about ethereum and ethereum-like blockchains here and what they offer as part of their environment today.

1 Like

In my opinion, any store front of any kind, should not push tax and legal responsibilities to the end user(s). This will just cause a massive number of unknown legal problems for everyone involved. This is why most store fronts use third party services who handle that for them. Almost all end users do not know what the tax laws are that pertain to them.

A simple example is the Unity store, if I was responsible for knowing what tax had to be collected for international transactions, I would never sell things on the store.

Being cryptocurrency does not change anything, a currency transaction of any kind is still a currency transaction in many countries, and soon to be in more, including as the U.S. as mentioned above. The U.S. already considers cryptocurrencies as a legitimate and taxable, and there are more rules to come.
See this intro article using bitcoin as an example: Bitcoin Taxes in 2021: A Guide to Tax Rules for Cryptocurrency - NerdWallet

4 Likes

Well, you have a valid point, however i am not aware of any platforms that would work for us without putting extra legal burden on us or the users, if you do, please feel free to share some links.

Alternatively we can frame this as tips or donations, that would avoid all the complexity of sales tax that you mentioned, but will carry some limitations.

I know it isn’t the same, but it seems we can’t have nice things due to the need to comply to our obsolete and poorly integrated legal framework.

2 Likes

Also then just defer them to other platforms of their choice for tips/donations. Then those platforms can sort out whether it’s legally payment for a good or just a donation. (For example, Patreon is very clear about this when setting up perks.)

Perhaps we could provide the submitters a convenient list or something.

2 Likes