How I migrated my Stacks Exchange AMM from a monolithic structure to clean separation with automated contract synchronization

Introduction

Building decentralized applications often starts with everything bundled together. As projects grow and integrate into larger ecosystems, you need clean separation of concerns.

This guide covers how I separated my Stacks Exchange AMM frontend from its backend, integrated it into the Pasifika Web3 Tech Hub ecosystem, and created automated contract address synchronization.

Note: This project was originally forked from LearnWeb3DAO/stacks-amm to be extended for Pacific Island communities.

Key Topics:


Architecture Overview

Before: Monolithic structure with everything bundled together

# Backend (Contract Development)
stacks-amm/
├── contracts/
├── deployments/
├── frontend/ (Integrated Ecosystem)
├── settings 
└── tests 

After: Clean separation with automated synchronization

# Backend (Contract Development)
pasifika-stacks-exchange/
├── contracts/
├── deployments/
├── settings/ 
└── tests/ 

# Frontend (Integrated Ecosystem)
pasifika-web3-fe/
├── app/
├── deployed_contracts/
├── lib/
├── public/
├── scripts/
└── src/config/

Key Migration Steps

1. Analysis & Planning

2. Dependencies Setup

npm install @stacks/connect @stacks/network @stacks/transactions

3. Directory Structure

pasifika-web3-fe/app/stacks-exchange/
├── page.tsx
├── components/
├── hooks/
└── lib/

Core Implementation

Main AMM Page Integration

// app/stacks-exchange/page.tsx
export default function StacksExchange() {
  const { isDarkMode } = useDarkMode();
  const [pools, setPools] = useState([]);
  const [activeTab, setActiveTab] = useState("swap");

  return (
    <div className={`container ${isDarkMode ? 'dark' : 'light'}`}>
      {/* Pasifika Header */}
      <div className="header">
        <div className="logo">
          <Image src="/pasifika.png" alt="Pasifika" />
          <span>Pasifika</span>
        </div>
      </div>

      {/* AMM Interface */}
      <div className="amm-container">
        <div className="tab-navigation">
          {["swap", "add-liquidity", "pools"].map((tab) => (
            <button onClick={() => setActiveTab(tab)}>
              {tab.toUpperCase()}
            </button>
          ))}
        </div>
        
        {/* Tab Content */}
        {activeTab === "swap" && <Swap pools={pools} />}
        {activeTab === "add-liquidity" && <AddLiquidity pools={pools} />}
        {activeTab === "pools" && <PoolsList pools={pools} />}
      </div>
    </div>
  );
}

Stacks Wallet Integration

// hooks/use-stacks.ts
export function useStacks() {
  const [userData, setUserData] = useState(null);
  const appConfig = useMemo(() => new AppConfig(["store_write"]), []);
  const userSession = useMemo(() => new UserSession({ appConfig }), [appConfig]);

  const connectWallet = useCallback(() => {
    showConnect({ appDetails, userSession });
  }, [userSession]);

  return { userData, connectWallet, handleCreatePool, handleSwap };
}

Contract Address Synchronization

The Key Innovation: Automated script to sync contract addresses from backend deployments to frontend.

How It Works

  1. Reads Clarinet deployment YAML files
  2. Extracts contract addresses and metadata
  3. Generates TypeScript definitions for frontend
  4. Saves JSON files for runtime use

Core Sync Script

// scripts/save-contract-addresses.js
const fs = require('fs');
const yaml = require('js-yaml');

// Parse Clarinet deployment files
function extractContractInfo(deploymentPlan, network) {
  const contracts = {};
  
  deploymentPlan.genesis.plan.batches.forEach(batch => {
    batch.transactions?.forEach(transaction => {
      if (transaction['contract-publish']) {
        const contract = transaction['contract-publish'];
        contracts[contract['contract-name']] = {
          address: contract['expected-sender'],
          network: network,
          deployedAt: new Date().toISOString()
        };
      }
    });
  });
  return contracts;
}

// Generate TypeScript definitions
function generateTypeScriptDefinitions(contracts) {
  const contractNames = Object.keys(contracts);
  return `
export const DEPLOYED_CONTRACTS = ${JSON.stringify(contracts, null, 2)};

// Contract addresses
${contractNames.map(name => 
  `export const ${name.toUpperCase()}_CONTRACT = "${contracts[name].address}.${name}";`
).join('\n')}
`;
}

// Main sync function
async function main() {
  const deploymentFile = 'deployments/default.testnet-plan.yaml';
  const deploymentPlan = yaml.load(fs.readFileSync(deploymentFile, 'utf8'));
  
  const contracts = extractContractInfo(deploymentPlan, 'testnet');
  const tsContent = generateTypeScriptDefinitions(contracts);
  
  // Save to frontend
  fs.writeFileSync('deployed_contracts/contract-addresses.ts', tsContent);
  fs.writeFileSync('deployed_contracts/contracts.json', JSON.stringify(contracts, null, 2));
  
  console.log('Contract addresses synchronized!');
}

if (require.main === module) main();

Package.json Integration

{
  "scripts": {
    "sync-contracts": "node scripts/save-contract-addresses.js",
    "dev": "npm run sync-contracts && next dev",
    "build": "npm run sync-contracts && next build"
  },
  "devDependencies": {
    "js-yaml": "^4.1.0"
  }
}

Testing & Verification

# Run sync script
npm run sync-contracts

# Verify generated files
ls deployed_contracts/
# - contract-addresses.ts
# - contracts.json

# Test frontend integration
npm run dev

Results and Benefits

What I Achieved


Conclusion

Separating your DApp frontend from the backend is crucial for scalability, maintainability, and team collaboration. By implementing automated contract address synchronization, you ensure that your frontend always stays in sync with your latest contract deployments.

The approach I've outlined here provides:


Resources


This tutorial is part of the Pasifika Web3 Tech Hub's commitment to sharing knowledge and empowering Pacific Island developers in Stacks blockchain technology.