import express from "express";
import { AutocratClient, PriceMath } from "@metadaoproject/futarchy/v0.4";
import { Connection, Keypair, PublicKey, Transaction } from "@solana/web3.js";
import * as anchor from "@coral-xyz/anchor";
import { getAccount, getAssociatedTokenAddressSync, getMint, getOrCreateAssociatedTokenAccount, } from "@solana/spl-token";
import { AmmClient } from "@metadaoproject/futarchy/v0.4";
import pkg from "@coral-xyz/anchor";
const { BN } = pkg;
import fs from "fs";
import { ConditionalVaultClient } from "@metadaoproject/futarchy/v0.4";
const app = express();
const port = process.env.PORT || 9000;
// Load the wallet
const walletSecretKey = new Uint8Array(JSON.parse(fs.readFileSync("wallet.json", "utf-8")));
const wallet = Keypair.fromSecretKey(walletSecretKey);
// Initialize Futarchy clients
const connection = new Connection("https://mainnet.helius-rpc.com/?api-key=ea9c561f-0680-4ae5-835c-5c0e463fa5e4");
const provider = new anchor.AnchorProvider(connection, {
publicKey: wallet.publicKey,
signTransaction: async (tx) => {
if (tx instanceof Transaction) {
tx.partialSign(wallet);
}
else {
tx.sign([wallet]);
}
return tx;
},
signAllTransactions: async (txs) => {
return txs.map((tx) => {
if (tx instanceof Transaction) {
tx.partialSign(wallet);
}
else {
tx.sign([wallet]);
}
return tx;
});
},
}, { commitment: "confirmed" });
const autocratProgram = AutocratClient.createClient({ provider });
// Middleware
app.use(express.json());
// Routes
app.get("/", (req, res) => {
res.json({ message: "Welcome to the Express TypeScript Server!" });
});
// Get all DAOs
app.get("/daos", async (req, res) => {
try {
const daos = await autocratProgram.autocrat.account.dao.all();
res.json({ success: true, daos });
}
catch (error) {
console.error("Error fetching DAOs:", error);
res.status(500).json({ error: "Failed to fetch DAOs" });
}
});
// Get DAO by ID
app.get("/daos/:id", async (req, res) => {
try {
const daoAddress = new PublicKey(req.params.id);
const dao = await autocratProgram.getDao(daoAddress);
res.json({ dao });
}
catch (error) {
console.error("Error fetching DAO:", error);
res.status(500).json({ error: "Failed to fetch DAO" });
}
});
// Get all proposals for a DAO
app.get("/daos/:id/proposals", async (req, res) => {
try {
const daoAddress = new PublicKey(req.params.id);
const proposals = await autocratProgram.autocrat.account.proposal.all();
const filteredProposals = proposals.filter((prop) => prop.account.dao.toString() === daoAddress.toString());
res.json({ success: true, proposals: filteredProposals });
}
catch (error) {
res.status(500).json({ error: "Failed to fetch proposals" });
}
});
// Get proposal by ID
app.get("/proposals/:id", async (req, res) => {
try {
const proposalAddress = new PublicKey(req.params.id);
const proposal = await autocratProgram.getProposal(proposalAddress);
res.json({ success: true, proposal });
}
catch (error) {
res.status(500).json({ error: "Failed to fetch proposal" });
}
});
// Create a new proposal
app.post("/daos/:id/proposals", (async (req, res) => {
try {
const { descriptionUrl, baseTokensToLP, quoteTokensToLP } = req.body;
if (!descriptionUrl || !baseTokensToLP || !quoteTokensToLP) {
return res.status(400).json({ error: "Missing required fields" });
}
const daoAddress = new PublicKey(req.params.id);
const dao = await autocratProgram.getDao(daoAddress);
const tokenMint = await getMint(connection, dao.tokenMint);
const usdcMint = await getMint(connection, dao.usdcMint);
const tokenDecimals = tokenMint.decimals;
const usdcDecimals = usdcMint.decimals;
// Get or create token accounts for the payer
const metaAccount = await getOrCreateAssociatedTokenAccount(connection, provider.wallet, // Type assertion for wallet
dao.tokenMint, provider.wallet.publicKey);
const usdcAccount = await getOrCreateAssociatedTokenAccount(connection, provider.wallet, // Type assertion for wallet
dao.usdcMint, provider.wallet.publicKey);
// Check balances
const metaBalance = metaAccount.amount;
const usdcBalance = usdcAccount.amount;
// Convert input amounts to chain amounts
const requiredMeta = PriceMath.getChainAmount(baseTokensToLP, tokenDecimals);
const requiredUsdc = PriceMath.getChainAmount(quoteTokensToLP, usdcDecimals);
if (metaBalance < BigInt(requiredMeta.toString()) ||
usdcBalance < BigInt(requiredUsdc.toString())) {
return res.status(400).json({
error: "Insufficient balance for proposal creation",
requiredMeta: requiredMeta.toString(),
requiredUsdc: requiredUsdc.toString(),
});
}
// Create the proposal instruction
const accounts = [
{
pubkey: daoAddress,
isSigner: true,
isWritable: true,
},
];
const data = autocratProgram.autocrat.coder.instruction.encode("update_dao", {
daoParams: {
passThresholdBps: 500,
baseBurnLamports: null,
burnDecayPerSlotLamports: null,
slotsPerProposal: null,
marketTakerFee: null,
},
});
const ix = {
programId: autocratProgram.getProgramId(),
accounts,
data,
};
// Initialize the proposal
const proposalAddress = await autocratProgram.initializeProposal(daoAddress, descriptionUrl, ix, requiredMeta, requiredUsdc);
res.json({
success: true,
proposalAddress: proposalAddress.toString(),
message: "Proposal created successfully",
});
}
catch (error) {
console.error("Error creating proposal:", error);
res.status(500).json({ error: "Failed to create proposal" });
}
}));
// Buy in pass market
app.post("/proposals/:id/buy-pass", (async (req, res) => {
try {
// Get the amount and user public key from the request body
const { amount, user } = req.body;
if (!amount) {
return res.status(400).json({ error: "Amount is required" });
}
if (!user) {
return res.status(400).json({ error: "User public key is required" });
}
const userPublicKey = new PublicKey(user);
// Get the proposal for which trade has to be made
const proposalAddress = new PublicKey(req.params.id);
const proposal = await autocratProgram.getProposal(proposalAddress);
if (!proposal) {
return res.status(404).json({ error: "Proposal not found" });
}
// Initialize vault client for token splitting
const vaultClient = ConditionalVaultClient.createClient({ provider });
const quoteTokenVault = await vaultClient.fetchVault(proposal.quoteVault);
if (!quoteTokenVault) {
return res.status(404).json({ error: "Quote vault not found" });
}
// Fetch user's quote token (usually USDC) account so that we can check the balance
const quoteTokenAddress = getAssociatedTokenAddressSync(quoteTokenVault.underlyingTokenMint, userPublicKey);
const quoteTokenAccount = await getAccount(connection, quoteTokenAddress);
// Initialize AMM client to get the pass market AMM
const ammClient = AmmClient.createClient({ provider });
const passAmm = await ammClient.getAmm(proposal.passAmm);
// Get the user's pass market conditional quote token account, so that we can check the balance
const passQuoteTokenAddress = getAssociatedTokenAddressSync(passAmm.quoteMint, userPublicKey);
const passQuoteTokenAccount = await getAccount(connection, passQuoteTokenAddress);
// Get the mint of the quote token and then decimals
const quoteMint = await getMint(connection, quoteTokenVault.underlyingTokenMint);
const quoteDecimals = quoteMint.decimals;
// Get the mint of the pass market conditional quote token (usually pUSDC) and then decimals
const passQuoteMint = await getMint(connection, passAmm.quoteMint);
const passQuoteDecimals = passQuoteMint.decimals;
// Convert balances to human-readable format
const usdcBalance = Number(quoteTokenAccount.amount) / Math.pow(10, quoteDecimals);
const passQuoteBalance = Number(passQuoteTokenAccount.amount) / Math.pow(10, passQuoteDecimals);
// Calculate the maximum amount that can be bought
const maxBuyAmount = usdcBalance + passQuoteBalance;
// Check if requested amount is within limits
if (amount > maxBuyAmount) {
return res.status(400).json({
error: "Insufficient balance",
requested: amount,
available: maxBuyAmount,
usdcBalance,
passQuoteBalance,
});
}
let splitTx = null;
// If entered amount is greater than user's conditional pass quote balance,
// we need to split quote tokens into pass quote and fail quote first
if (amount > passQuoteBalance) {
const amountToSplit = amount - passQuoteBalance;
const amountToSplitChain = new BN(Math.floor(amountToSplit * Math.pow(10, quoteDecimals)));
// Split tokens before swap
const splitIx = vaultClient.splitTokensIx(proposal.question, proposal.quoteVault, quoteTokenVault.underlyingTokenMint, amountToSplitChain, 2, // numOutcomes (PASS/FAIL)
provider.wallet.publicKey);
// Build the split transaction
splitTx = new Transaction().add(await splitIx.instruction());
}
// Calculate expected output amount and minimum output amount with assumed 1% slippage
const expectedOutput = new BN(passAmm.baseAmount)
.mul(new BN(amount))
.div(new BN(passAmm.quoteAmount).add(new BN(amount)));
const slippageTolerance = 0.01; // 1% slippage tolerance
const slippageFactorBN = new BN(Math.floor((1 - slippageTolerance) * 100));
const outputAmountMin = expectedOutput
.mul(slippageFactorBN)
.div(new BN(100));
// Get the swap instruction
const swapIx = ammClient.swapIx(proposal.passAmm, passAmm.baseMint, passAmm.quoteMint, { buy: {} }, new BN(amount), outputAmountMin, userPublicKey);
const swapTx = new Transaction().add(await swapIx.instruction());
res.json({
success: true,
withSplit: splitTx ? true : false,
splitTx,
swapTx,
expectedOutput: expectedOutput.toString(),
minOutput: outputAmountMin.toString(),
message: "Buy in pass market transactions created successfully",
});
}
catch (error) {
console.error("Error executing buy in pass market:", error);
res.status(500).json({ error: "Failed to execute buy in pass market" });
}
}));
// Sell in pass market
app.post("/proposals/:id/sell-pass", (async (req, res) => {
try {
const { amount, user } = req.body;
if (!amount) {
return res.status(400).json({ error: "Amount is required" });
}
if (!user) {
return res.status(400).json({ error: "User public key is required" });
}
const userPublicKey = new PublicKey(user);
// Get the proposal
const proposalAddress = new PublicKey(req.params.id);
const proposal = await autocratProgram.getProposal(proposalAddress);
if (!proposal) {
return res.status(404).json({ error: "Proposal not found" });
}
// Initialize AMM client to get the pass market AMM
const ammClient = AmmClient.createClient({ provider });
const passAmm = await ammClient.getAmm(proposal.passAmm);
// Get the user's balance of the pass market AMM's underlying token (the base token)
const passMarketBaseTokenAddress = getAssociatedTokenAddressSync(passAmm.baseMint, userPublicKey);
const passMarketBaseTokenAccount = await getAccount(connection, passMarketBaseTokenAddress);
// Get the token decimals
const passMarketBaseTokenMint = await getMint(connection, passAmm.baseMint);
const passMarketBaseTokenDecimals = passMarketBaseTokenMint.decimals;
// Convert user's balance to human-readable format
const userBaseBalance = Number(passMarketBaseTokenAccount.amount) /
Math.pow(10, passMarketBaseTokenDecimals);
// Check if user has sufficient balance
if (amount > userBaseBalance) {
return res.status(400).json({
error: "Insufficient balance",
requested: amount,
available: userBaseBalance,
});
}
// Calculate expected output and minimum output with 1% slippage
const expectedOutput = new BN(passAmm.quoteAmount)
.mul(new BN(amount))
.div(new BN(passAmm.baseAmount).add(new BN(amount)));
const slippageTolerance = 0.01; // 1% slippage tolerance
const slippageFactorBN = new BN(Math.floor((1 - slippageTolerance) * 100));
const outputAmountMin = expectedOutput
.mul(slippageFactorBN)
.div(new BN(100));
// Execute the swap - no need to adjust for decimals here as swap function handles it
const swapIx = ammClient.swapIx(proposal.passAmm, passAmm.baseMint, passAmm.quoteMint, { sell: {} }, new BN(amount), outputAmountMin, userPublicKey);
const swapTx = new Transaction().add(await swapIx.instruction());
res.json({
success: true,
swapTx,
expectedOutput: Number(expectedOutput),
minOutput: Number(outputAmountMin),
message: "Sell in pass market transaction created successfully",
});
}
catch (error) {
console.error("Error executing sell in pass market:", error);
res.status(500).json({ error: "Failed to execute sell in pass market" });
}
}));
// Buy in fail market
app.post("/proposals/:id/buy-fail", (async (req, res) => {
try {
const { amount, user } = req.body;
if (!amount) {
return res.status(400).json({ error: "Amount is required" });
}
if (!user) {
return res.status(400).json({ error: "User public key is required" });
}
const userPublicKey = new PublicKey(user);
// Get the proposal
const proposalAddress = new PublicKey(req.params.id);
const proposal = await autocratProgram.getProposal(proposalAddress);
if (!proposal) {
return res.status(404).json({ error: "Proposal not found" });
}
const vaultClient = ConditionalVaultClient.createClient({ provider });
const quoteVault = await vaultClient.fetchVault(proposal.quoteVault);
if (!quoteVault) {
return res.status(404).json({ error: "Quote vault not found" });
}
// Check user's quote token account
const quoteTokenAddress = getAssociatedTokenAddressSync(quoteVault.underlyingTokenMint, userPublicKey);
const quoteTokenAccount = await getAccount(connection, quoteTokenAddress);
// Initialize AMM client to get the fail market AMM
const ammClient = AmmClient.createClient({ provider });
const amm = await ammClient.getAmm(proposal.failAmm);
// Get the user's fail market quote token balance
const failQuoteTokenAddress = getAssociatedTokenAddressSync(amm.quoteMint, userPublicKey);
const failQuoteTokenAccount = await getAccount(connection, failQuoteTokenAddress);
// Get the decimals for the quote token (usually USDC) from the quote vault
const quoteMint = await getMint(connection, quoteVault.underlyingTokenMint);
const quoteDecimals = quoteMint.decimals;
// Get the mint of the fail market conditional quote token (usually fUSDC) and then decimals
const failQuoteMint = await getMint(connection, amm.quoteMint);
const failMarketQuoteDecimals = failQuoteMint.decimals;
// Convert balances to human-readable format
const quoteTokenBalance = Number(quoteTokenAccount.amount) / Math.pow(10, quoteDecimals);
const failQuoteTokenBalance = Number(failQuoteTokenAccount.amount) /
Math.pow(10, failMarketQuoteDecimals);
// Calculate the maximum amount that can be bought
const maxBuyAmount = quoteTokenBalance + failQuoteTokenBalance;
// Check if requested amount is within limits
if (amount > maxBuyAmount) {
return res.status(400).json({
error: "Insufficient balance",
requested: amount,
available: maxBuyAmount,
quoteTokenBalance,
failQuoteTokenBalance,
});
}
let splitTx = null;
// If amount is greater than user's fail quote token balance, we need to split USDC first
if (amount > failQuoteTokenBalance) {
const amountToSplit = amount - failQuoteTokenBalance;
const amountToSplitChain = new BN(Math.floor(amountToSplit * Math.pow(10, quoteDecimals)).toString());
// Split tokens before swap
const splitIx = vaultClient.splitTokensIx(proposal.question, proposal.quoteVault, quoteVault.underlyingTokenMint, amountToSplitChain, 2, // numOutcomes (PASS/FAIL)
provider.wallet.publicKey);
// Send the split transaction
splitTx = new Transaction().add(await splitIx.instruction());
}
// Calculate expected output and minimum output with 1% slippage
const expectedOutput = new BN(amm.baseAmount)
.mul(new BN(amount))
.div(new BN(amm.quoteAmount).add(new BN(amount)));
const slippageTolerance = 0.01; // 1% slippage tolerance
const slippageFactorBN = new BN(Math.floor((1 - slippageTolerance) * 100));
const outputAmountMin = expectedOutput
.mul(slippageFactorBN)
.div(new BN(100));
// Execute the swap
const swapIx = ammClient.swapIx(proposal.failAmm, amm.quoteMint, amm.baseMint, { buy: {} }, new BN(amount), outputAmountMin, userPublicKey);
const swapTx = new Transaction().add(await swapIx.instruction());
res.json({
success: true,
withSplit: splitTx ? true : false,
splitTx,
swapTx,
expectedOutput: Number(expectedOutput),
minOutput: Number(outputAmountMin),
message: "Buy in fail market transaction created successfully",
});
}
catch (error) {
console.error("Error executing buy in fail market:", error);
res.status(500).json({ error: "Failed to execute buy in fail market" });
}
}));
// Sell in fail market
app.post("/proposals/:id/sell-fail", (async (req, res) => {
try {
const { amount, user } = req.body;
if (!amount) {
return res.status(400).json({ error: "Amount is required" });
}
if (!user) {
return res.status(400).json({ error: "User public key is required" });
}
const userPublicKey = new PublicKey(user);
// Get the proposal
const proposalAddress = new PublicKey(req.params.id);
const proposal = await autocratProgram.getProposal(proposalAddress);
if (!proposal) {
return res.status(404).json({ error: "Proposal not found" });
}
// Initialize AMM client to get the fail market AMM
const ammClient = AmmClient.createClient({ provider });
const amm = await ammClient.getAmm(proposal.failAmm);
// Get the user's balance of the fail market AMM's underlying token (the base token)
const failMarketBaseTokenAddress = getAssociatedTokenAddressSync(amm.baseMint, userPublicKey);
const failMarketBaseTokenAccount = await getAccount(connection, failMarketBaseTokenAddress);
// Get the token decimals
const failMarketBaseTokenMint = await getMint(connection, amm.baseMint);
const failMarketBaseTokenDecimals = failMarketBaseTokenMint.decimals;
// Convert user's balance to human-readable format
const userBaseBalance = Number(failMarketBaseTokenAccount.amount) /
Math.pow(10, failMarketBaseTokenDecimals);
// Check if user has sufficient balance
if (amount > userBaseBalance) {
return res.status(400).json({
error: "Insufficient balance",
requested: amount,
available: userBaseBalance,
});
}
// Calculate expected output and minimum output with 1% slippage
const expectedOutput = new BN(amm.quoteAmount)
.mul(new BN(amount))
.div(new BN(amm.baseAmount).add(new BN(amount)));
const slippageTolerance = 0.01; // 1% slippage tolerance
const slippageFactorBN = new BN(Math.floor((1 - slippageTolerance) * 100));
const outputAmountMin = expectedOutput
.mul(slippageFactorBN)
.div(new BN(100));
// Execute the swap - no need to adjust for decimals here as swap function handles it
const swapIx = ammClient.swapIx(proposal.failAmm, amm.quoteMint, amm.baseMint, { sell: {} }, new BN(amount), outputAmountMin, userPublicKey);
const swapTx = new Transaction().add(await swapIx.instruction());
res.json({
success: true,
swapTx,
expectedOutput: Number(expectedOutput),
minOutput: Number(outputAmountMin),
message: "Sell in fail market transaction created successfully",
});
}
catch (error) {
console.error("Error executing sell in fail market:", error);
res.status(500).json({ error: "Failed to execute sell in fail market" });
}
}));
// Start server
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});