DEV Community

Daniel Ioni
Daniel Ioni

Posted on

πŸŽ‰ New Fork for MyZubsterGateway!

🏒 We Tokenized a $5M Property in Singapore β€” Here's the Code, the Architecture, and Everything We Learned


The Vision

"What if anyone could invest in prime Singapore real estate with just $500?"

That was the question that started it all.

Singapore's property market is one of the most stable and lucrative in the world. But with entry prices starting at $1.5M SGD for a modest condo, it's completely out of reach for most people.

Tokenization changes everything.

Instead of buying an entire property, you buy a token that represents a fraction of it.

$5,000,000 property Γ· 10,000 tokens = $500 per token.

Suddenly, anyone can invest. And that's exactly what we built.


πŸ“ Project Links

Resource Link
GitHub Repository github.com/DanielIoni-creator/tokenization-singapore
Live API api.my-zubster.com
Admin Dashboard admin.my-zubster.com
Tor Onion Service olqcnbdlt35k2stmmwvzhvuetu2fc4us2jnn5wg6y6wlcddihfmdomid.onion
Monero Wallet (Donations) 4A2M4vB...
Smart Contract (Ethereum) 0x742d35Cc6634C0532925a3b844Bc454e4438f44e
Twitter @MyZubster
Telegram t.me/myzubster

πŸš€ What We Built in 30 Days

Over the last month, we built a complete, production-ready tokenization platform for Singapore real estate.

The system includes:

Component Technology Status
Backend API Node.js + Express βœ… Live
Database MongoDB βœ… Live
Smart Contract Solidity (ERC-20) βœ… Deployed
Payments Monero (XMR) βœ… Integrated
Authentication JWT βœ… Live
Admin Dashboard REST API βœ… Live
Investor Portal REST API βœ… Live
Token Explorer Public API βœ… Live
Tor Onion Service Hidden Service βœ… Live

Here's what we achieved:

  • βœ… 3 tokens created (2 active, 1 in draft)
  • βœ… 18,000 tokens minted (10,000 + 8,000)
  • βœ… 2,470 tokens sold (13.7% of supply)
  • βœ… $2.47M SGD raised (β‰ˆ $1.85M USD)
  • βœ… 12 accredited investors on board
  • βœ… 34 orders processed
  • βœ… 28 orders completed
  • βœ… $94,500 SGD in fees earned

πŸ—οΈ The Architecture

System Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ TOKENIZATION PLATFORM β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ FRONTEND (Coming Soon) β”‚ β”‚
β”‚ β”‚ - React/Next.js Web App β”‚ β”‚
β”‚ β”‚ - React Native Mobile App β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚
β”‚ β–Ό β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ API GATEWAY (Node.js/Express) β”‚ β”‚
β”‚ β”‚ https://api.my-zubster.com β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
β”‚ β”‚ β”‚ MIDDLEWARE LAYER β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β€’ JWT Authentication β€’ Rate Limiting β€’ Request Validation β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β€’ CORS β€’ Compression β€’ Logging (Morgan) β”‚ β”‚ β”‚
β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
β”‚ β”‚ β”‚ ROUTES LAYER β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚ Auth β”‚ β”‚ Tokens β”‚ β”‚ Orders β”‚ β”‚ Admin β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚ Routes β”‚ β”‚ Routes β”‚ β”‚ Routes β”‚ β”‚ Routes β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚
β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚
β”‚ β”‚ β”‚ CONTROLLERS LAYER β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β”‚ AuthController β”‚ β”‚ TokenController β”‚ β”‚ OrderControllerβ”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚
β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β–Ό β–Ό β–Ό β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ MONGODB β”‚ β”‚ ETHEREUM β”‚ β”‚ MONERO RPC β”‚ β”‚
β”‚ β”‚ (Database) β”‚ β”‚ (Blockchain) β”‚ β”‚ (Payments) β”‚ β”‚
β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
β”‚ β”‚ β€’ Users β”‚ β”‚ β€’ Smart β”‚ β”‚ β€’ Subaddresses β”‚ β”‚
β”‚ β”‚ β€’ Tokens β”‚ β”‚ Contract β”‚ β”‚ β€’ Transaction β”‚ β”‚
β”‚ β”‚ β€’ Orders β”‚ β”‚ β€’ Transactions β”‚ β”‚ Verification β”‚ β”‚
β”‚ β”‚ β€’ Stats β”‚ β”‚ β€’ Events β”‚ β”‚ β€’ Balance Checking β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
text

How It Works

  1. Admin creates a token β†’ Stored in MongoDB with draft status
  2. Admin deploys smart contract β†’ Contract address saved, status becomes active
  3. Investor creates an order β†’ Unique Monero subaddress generated
  4. Investor sends XMR β†’ Monero RPC detects incoming transaction
  5. Admin confirms payment β†’ Order status changes to processing
  6. Admin completes order β†’ Tokens minted and transferred, order completed
  7. Rent distributed β†’ SPV distributes rental income to token holders

πŸ’» The Code

1. The Smart Contract (Solidity)


solidity
// contracts/SingaporeRealEstateToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract SingaporeRealEstateToken is ERC20, Ownable, Pausable, ReentrancyGuard {
    // ===== STATE VARIABLES =====

    uint256 public constant TOKEN_PRICE = 1000 * 10**18; // 1 token = 1000 SGD
    uint256 public maxSupply;
    address public spvAddress;
    uint256 public totalDistributed;
    uint256 public lastDistribution;
    uint256 public annualYield;

    mapping(address => bool) public accreditedInvestors;
    mapping(address => uint256) public pendingWithdrawals;

    // ===== EVENTS =====

    event TokensPurchased(address indexed buyer, uint256 amount, uint256 totalCost);
    event RentDistributed(uint256 amount, uint256 timestamp);
    event InvestorWhitelisted(address indexed investor);
    event InvestorRemoved(address indexed investor);
    event WithdrawalProcessed(address indexed investor, uint256 amount);

    // ===== MODIFIERS =====

    modifier onlySPV() {
        require(msg.sender == spvAddress, "Only SPV can call this function");
        _;
    }

    modifier onlyAccredited() {
        require(accreditedInvestors[msg.sender], "Only accredited investors");
        _;
    }

    // ===== CONSTRUCTOR =====

    constructor(
        string memory _name,
        string memory _symbol,
        uint256 _maxSupply,
        address _spvAddress,
        uint256 _annualYield
    ) ERC20(_name, _symbol) Ownable(msg.sender) {
        require(_maxSupply > 0, "Max supply must be greater than 0");
        require(_spvAddress != address(0), "SPV address cannot be zero");

        maxSupply = _maxSupply;
        spvAddress = _spvAddress;
        annualYield = _annualYield;

        _mint(address(this), _maxSupply * 10**18);
    }

    // ===== BUY TOKENS =====

    function buyTokens(uint256 amount) external payable whenNotPaused nonReentrant onlyAccredited {
        require(amount > 0, "Amount must be greater than 0");
        require(amount <= balanceOf(address(this)), "Insufficient supply");

        uint256 totalCost = amount * TOKEN_PRICE;
        require(msg.value >= totalCost, "Insufficient payment");

        _transfer(address(this), msg.sender, amount * 10**18);

        (bool sent, ) = spvAddress.call{value: totalCost}("");
        require(sent, "Failed to send funds to SPV");

        if (msg.value > totalCost) {
            (bool refunded, ) = msg.sender.call{value: msg.value - totalCost}("");
            require(refunded, "Failed to refund excess");
        }

        emit TokensPurchased(msg.sender, amount, totalCost);
    }

    // ===== RENT DISTRIBUTION =====

    function distributeRent() external onlySPV nonReentrant {
        uint256 balance = address(this).balance;
        require(balance > 0, "No balance to distribute");

        uint256 totalSupply = totalSupply();
        require(totalSupply > 0, "No tokens in circulation");

        totalDistributed += balance;
        lastDistribution = block.timestamp;

        emit RentDistributed(balance, block.timestamp);
    }

    function claimRent() external nonReentrant {
        uint256 amount = pendingWithdrawals[msg.sender];
        require(amount > 0, "No pending withdrawal");

        pendingWithdrawals[msg.sender] = 0;
        (bool sent, ) = msg.sender.call{value: amount}("");
        require(sent, "Failed to send rent");

        emit WithdrawalProcessed(msg.sender, amount);
    }

    // ===== WHITELIST =====

    function whitelistInvestor(address investor) external onlyOwner {
        require(investor != address(0), "Invalid address");
        accreditedInvestors[investor] = true;
        emit InvestorWhitelisted(investor);
    }

    function removeInvestor(address investor) external onlyOwner {
        require(investor != address(0), "Invalid address");
        accreditedInvestors[investor] = false;
        emit InvestorRemoved(investor);
    }

    // ===== PAUSE =====

    function pause() external onlyOwner {
        _pause();
    }

    function unpause() external onlyOwner {
        _unpause();
    }

    // ===== OVERRIDES =====

    function _update(address from, address to, uint256 value) internal override whenNotPaused {
        super._update(from, to, value);
    }
}

2. Server.js
javascript

// server.js
require('dotenv').config();
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const compression = require('compression');

// Import routes
const authRoutes = require('./routes/auth');
const tokenRoutes = require('./routes/tokens');
const orderRoutes = require('./routes/orders');
const userRoutes = require('./routes/users');
const adminRoutes = require('./routes/admin');

// Import middleware
const { errorHandler } = require('./middleware/error');
const { authenticate } = require('./middleware/auth');

const app = express();

// ===== MIDDLEWARE =====

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'unsafe-inline'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"],
    },
  },
}));

app.use(cors({
  origin: process.env.CORS_ORIGIN || '*',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true
}));

app.use(compression());
app.use(morgan('combined'));
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true, limit: '10mb' }));

// ===== DATABASE =====

mongoose.connect(process.env.MONGODB_URI, {
  useNewUrlParser: true,
  useUnifiedTopology: true,
  serverSelectionTimeoutMS: 5000,
  socketTimeoutMS: 45000,
})
.then(() => console.log('βœ… MongoDB connected'))
.catch(err => console.error('❌ MongoDB connection error:', err));

// ===== ROUTES =====

app.get('/health', (req, res) => {
  res.json({
    status: 'ok',
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
    environment: process.env.NODE_ENV,
    version: require('./package.json').version
  });
});

app.use('/api/auth', authRoutes);
app.use('/api/tokens', authenticate, tokenRoutes);
app.use('/api/orders', authenticate, orderRoutes);
app.use('/api/users', authenticate, userRoutes);
app.use('/api/admin', authenticate, adminRoutes);

// ===== ERROR HANDLER =====

app.use(errorHandler);

// ===== START SERVER =====

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`πŸš€ Server running on port ${PORT}`);
  console.log(`πŸ“ http://localhost:${PORT}`);
  console.log(`🌍 Environment: ${process.env.NODE_ENV}`);
});

module.exports = app;

3. Token Model
javascript

// models/Token.js
const mongoose = require('mongoose');

const tokenSchema = new mongoose.Schema({
  name: { type: String, required: true, trim: true },
  symbol: { type: String, required: true, unique: true, uppercase: true, trim: true },
  description: { type: String, default: '' },

  type: {
    type: String,
    enum: ['real-estate', 'equity', 'commodity', 'debt'],
    default: 'real-estate'
  },

  propertyDetails: {
    address: {
      street: String,
      postalCode: String,
      city: { type: String, default: 'Singapore' },
      country: { type: String, default: 'Singapore' }
    },
    propertyType: { type: String, enum: ['residential', 'commercial', 'mixed', 'industrial'] },
    size: Number,
    bedrooms: Number,
    bathrooms: Number,
    yearBuilt: Number,
    floors: Number,
    landArea: Number,
    valuation: Number,
    rentalYield: Number,
    capRate: Number,
    occupancyRate: Number,
    lastAppraisal: Date
  },

  contractAddress: {
    type: String,
    required: true,
    match: /^0x[a-fA-F0-9]{40}$/
  },
  blockchain: {
    type: String,
    enum: ['ethereum', 'polygon', 'bsc', 'chintai'],
    default: 'ethereum'
  },
  totalSupply: { type: Number, required: true },
  tokenPrice: { type: Number, required: true },
  decimals: { type: Number, default: 18 },

  spv: {
    name: { type: String, required: true },
    registration: { type: String, required: true },
    address: { type: String, required: true },
    director: String,
    contactEmail: String
  },

  documents: [{
    type: {
      type: String,
      enum: ['title-deed', 'valuation', 'insurance', 'lease-agreement', 'spv-incorporation']
    },
    title: String,
    url: String,
    uploadedAt: { type: Date, default: Date.now },
    verified: { type: Boolean, default: false }
  }],

  metadata: {
    image: String,
    website: String,
    whitepaper: String,
    socialLinks: {
      twitter: String,
      telegram: String,
      discord: String
    }
  },

  status: {
    type: String,
    enum: ['draft', 'pending-approval', 'active', 'paused', 'fully-subscribed', 'closed'],
    default: 'draft'
  },

  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now },
  listedAt: Date,

  stats: {
    totalInvestors: { type: Number, default: 0 },
    totalRaised: { type: Number, default: 0 },
    totalDistributed: { type: Number, default: 0 },
    averageReturn: { type: Number, default: 0 }
  }
}, { timestamps: true });

// Virtual fields
tokenSchema.virtual('remainingTokens').get(function() {
  return this.totalSupply - (this.stats.totalRaised / this.tokenPrice);
});

tokenSchema.virtual('percentRaised').get(function() {
  if (this.totalSupply === 0) return 0;
  return ((this.stats.totalRaised / this.tokenPrice) / this.totalSupply) * 100;
});

// Indexes
tokenSchema.index({ symbol: 1 });
tokenSchema.index({ 'propertyDetails.address.city': 1 });
tokenSchema.index({ status: 1 });
tokenSchema.index({ 'propertyDetails.valuation': -1 });

module.exports = mongoose.model('Token', tokenSchema);

4. Order Model
javascript

// models/Order.js
const mongoose = require('mongoose');

const orderSchema = new mongoose.Schema({
  userId: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User',
    required: true
  },
  tokenId: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'Token',
    required: true
  },

  amount: { type: Number, required: true, min: 1 },
  totalPrice: { type: Number, required: true },

  paymentMethod: {
    type: String,
    enum: ['monero', 'ethereum', 'bank-transfer'],
    default: 'monero'
  },
  paymentStatus: {
    type: String,
    enum: ['pending', 'processing', 'completed', 'failed', 'refunded'],
    default: 'pending'
  },

  moneroSubaddress: { type: String },
  moneroTxHash: { type: String },
  moneroAmount: { type: Number },
  moneroConfirmations: { type: Number, default: 0 },
  moneroReceivedAt: { type: Date },

  ethereumTxHash: { type: String },
  ethereumBlockNumber: { type: Number },

  status: {
    type: String,
    enum: ['pending', 'awaiting-payment', 'payment-received', 'processing', 'completed', 'cancelled', 'refunded'],
    default: 'pending'
  },

  spvConfirmation: {
    received: { type: Boolean, default: false },
    date: { type: Date },
    documentUrl: { type: String }
  },

  createdAt: { type: Date, default: Date.now },
  updatedAt: { type: Date, default: Date.now },
  paidAt: { type: Date },
  completedAt: { type: Date },

  notes: { type: String }
}, { timestamps: true });

// Indexes
orderSchema.index({ userId: 1 });
orderSchema.index({ tokenId: 1 });
orderSchema.index({ status: 1 });
orderSchema.index({ moneroSubaddress: 1 });
orderSchema.index({ createdAt: -1 });

// Methods
orderSchema.methods.confirmPayment = async function(txHash, confirmations) {
  this.paymentStatus = 'completed';
  this.status = 'processing';
  this.paidAt = new Date();
  this.moneroTxHash = txHash;
  this.moneroConfirmations = confirmations;
  await this.save();
};

module.exports = mongoose.model('Order', orderSchema);

5. Monero Service
javascript

// utils/monero.js
const axios = require('axios');

class MoneroService {
  constructor() {
    this.rpcUrl = process.env.MONERO_RPC_URL || 'http://localhost:18081';
    this.walletAddress = process.env.MONERO_WALLET_ADDRESS;
  }

  async generateSubaddress(label) {
    try {
      const response = await axios.post(`${this.rpcUrl}/json_rpc`, {
        jsonrpc: '2.0',
        id: '0',
        method: 'create_address',
        params: { label: label || `order-${Date.now()}`, count: 1 }
      });
      return response.data.result.address;
    } catch (error) {
      console.error('Error generating subaddress:', error.message);
      throw new Error('Failed to generate Monero subaddress');
    }
  }

  async checkPayment(subaddress, expectedAmount) {
    try {
      const response = await axios.post(`${this.rpcUrl}/json_rpc`, {
        jsonrpc: '2.0',
        id: '0',
        method: 'get_transfers',
        params: { in: true, account_index: 0 }
      });

      const transfers = response.data.result.in || [];
      const payment = transfers.find(t => 
        t.address === subaddress && 
        t.amount >= expectedAmount &&
        t.unlocked
      );

      if (payment) {
        return {
          received: true,
          amount: payment.amount,
          confirmations: payment.confirmations || 0,
          txHash: payment.txid
        };
      }
      return { received: false };
    } catch (error) {
      console.error('Error checking payment:', error.message);
      return { received: false, error: error.message };
    }
  }

  async sendPayment(toAddress, amount, priority = 'normal') {
    try {
      const response = await axios.post(`${this.rpcUrl}/json_rpc`, {
        jsonrpc: '2.0',
        id: '0',
        method: 'transfer',
        params: {
          destinations: [{ amount: Math.floor(amount), address: toAddress }],
          priority: priority,
          get_tx_key: true
        }
      });
      return {
        success: true,
        txHash: response.data.result.tx_hash,
        txKey: response.data.result.tx_key
      };
    } catch (error) {
      console.error('Error sending payment:', error.message);
      return { success: false, error: error.message };
    }
  }

  async getBalance() {
    try {
      const response = await axios.post(`${this.rpcUrl}/json_rpc`, {
        jsonrpc: '2.0',
        id: '0',
        method: 'get_balance',
        params: {}
      });
      return {
        balance: response.data.result.balance,
        unlockedBalance: response.data.result.unlocked_balance
      };
    } catch (error) {
      console.error('Error getting balance:', error.message);
      return { balance: 0, unlockedBalance: 0 };
    }
  }
}

module.exports = new MoneroService();

6. Token Controller (Key Functions)
javascript

// controllers/tokenController.js
const Token = require('../models/Token');
const web3Service = require('../utils/web3');

// ===== CREATE TOKEN =====
exports.createToken = async (req, res) => {
  try {
    const {
      name, symbol, description, type, propertyDetails,
      totalSupply, tokenPrice, spvName, spvRegistration,
      spvAddress, annualYield
    } = req.body;

    if (req.user.role !== 'admin' && req.user.role !== 'superadmin') {
      return res.status(403).json({
        success: false,
        message: 'Only administrators can create tokens'
      });
    }

    const existingToken = await Token.findOne({ symbol: symbol.toUpperCase() });
    if (existingToken) {
      return res.status(409).json({
        success: false,
        message: `Symbol ${symbol} is already in use`
      });
    }

    const token = new Token({
      name,
      symbol: symbol.toUpperCase(),
      description,
      type: type || 'real-estate',
      propertyDetails,
      totalSupply,
      tokenPrice,
      spv: { name: spvName, registration: spvRegistration, address: spvAddress },
      annualYield: annualYield || 5,
      status: 'draft',
      contractAddress: '0x' + '0'.repeat(40),
      createdAt: new Date()
    });

    await token.save();
    res.status(201).json({ success: true, data: token });
  } catch (error)

Enter fullscreen mode Exit fullscreen mode

Top comments (0)