type WalletAddress = string;
interface LaunchConfig {
symbol: string;
issuer: WalletAddress; // XRPL token issuer
treasury: WalletAddress; // XRPL treasury receiving XRP
maxSupply: number; // total token supply available to curve
virtualXrp: number; // smooths initial price
virtualToken: number; // smooths initial price
feeBps: number; // platform fee in basis points
graduationTargetXrp: number; // once reached, migrate to XRPL AMM / DEX liquidity
}
interface LaunchState {
sold: number; // tokens sold from curve
reserveXrp: number; // XRP accumulated in reserve
feeXrp: number; // XRP collected as fees
graduated: boolean;
}
interface QuoteResult {
direction: "buy" | "sell";
xrpInOrOut: number;
tokenInOrOut: number;
feeXrp: number;
priceBefore: number;
priceAfter: number;
newReserveXrp: number;
newSold: number;
}
class BondingCurveLaunchpad {
private config: LaunchConfig;
private state: LaunchState;
constructor(config: LaunchConfig) {
this.config = config;
this.state = {
sold: 0,
reserveXrp: 0,
feeXrp: 0,
graduated: false,
};
}
/**
* Simple constant-product style curve using "virtual reserves".
* This is just a clean demo model:
* effectiveX = reserveXrp virtualXrp
* effectiveY = unsoldTokens virtualToken
* k = X * Y
*/
private getEffectiveReserves() {
const unsold = this.config.maxSupply - this.state.sold;
return {
x: this.state.reserveXrp this.config.virtualXrp,
y: unsold this.config.virtualToken,
};
}
public currentPrice(): number {
const { x, y } = this.getEffectiveReserves();
return x / y; // XRP per token
}
public quoteBuy(xrpInGross: number): QuoteResult {
if (this.state.graduated) throw new Error("Launch already graduated");
if (xrpInGross <= 0) throw new Error("xrpInGross must be > 0");
const feeXrp = (xrpInGross * this.config.feeBps) / 10_000;
const xrpInNet = xrpInGross - feeXrp;
const { x, y } = this.getEffectiveReserves();
const k = x * y;
const priceBefore = x / y;
const newX = x xrpInNet;
const newY = k / newX;
const tokensOut = y - newY;
const available = this.config.maxSupply - this.state.sold;
if (tokensOut > available) throw new Error("Not enough tokens remaining on curve");
const projectedReserve = this.state.reserveXrp xrpInNet;
const projectedSold = this.state.sold tokensOut;
const priceAfter = newX / newY;
return {
direction: "buy",
xrpInOrOut: xrpInGross,
tokenInOrOut: tokensOut,
feeXrp,
priceBefore,
priceAfter,
newReserveXrp: projectedReserve,
newSold: projectedSold,
};
}
public executeBuy(user: WalletAddress, xrpInGross: number): QuoteResult {
const quote = this.quoteBuy(xrpInGross);
// In a real XRPL app, this is where you'd:
// 1. verify inbound XRP payment to treasury / escrow
// 2. deliver issued token from issuer/distributor account
// 3. record trade in DB
// 4. optionally emit websocket event to UI
this.state.reserveXrp = quote.newReserveXrp;
this.state.sold = quote.newSold;
this.state.feeXrp = quote.feeXrp;
console.log(`[BUY] ${user} paid ${xrpInGross.toFixed(6)} XRP and receives ${quote.tokenInOrOut.toFixed(6)} ${this.config.symbol}`);
this.checkGraduation();
return quote;
}
public quoteSell(tokensIn: number): QuoteResult {
if (this.state.graduated) throw new Error("After graduation, sell via AMM / DEX");
if (tokensIn <= 0) throw new Error("tokensIn must be > 0");
if (tokensIn > this.state.sold) throw new Error("Cannot sell more than circulating curve supply");
const { x, y } = this.getEffectiveReserves();
const k = x * y;
const priceBefore = x / y;
const newY = y tokensIn;
const newX = k / newY;
const grossXrpOut = x - newX;
const feeXrp = (grossXrpOut * this.config.feeBps) / 10_000;
const netXrpOut = grossXrpOut - feeXrp;
if (netXrpOut > this.state.reserveXrp) {
throw new Error("Insufficient reserve XRP");
}
const projectedReserve = this.state.reserveXrp - netXrpOut;
const projectedSold = this.state.sold - tokensIn;
const priceAfter = newX / newY;
return {
direction: "sell",
xrpInOrOut: netXrpOut,
tokenInOrOut: tokensIn,
feeXrp,
priceBefore,
priceAfter,
newReserveXrp: projectedReserve,
newSold: projectedSold,
};
}
public executeSell(user: WalletAddress, tokensIn: number): QuoteResult {
const quote = this.quoteSell(tokensIn);
// In a real XRPL app, this is where you'd:
// 1. verify token payment from user to distributor / issuer account
// 2. send XRP back from treasury
// 3. record trade
// 4. emit UI update
this.state.reserveXrp = quote.newReserveXrp;
this.state.sold = quote.newSold;
this.state.feeXrp = quote.feeXrp;
console.log(`[SELL] ${user} sold ${tokensIn.toFixed(6)} ${this.config.symbol} and receives ${quote.xrpInOrOut.toFixed(6)} XRP`);
return quote;
}
private checkGraduation() {
if (
!this.state.graduated &&
this.state.reserveXrp >= this.config.graduationTargetXrp
) {
this.state.graduated = true;
console.log(`\n[GRADUATION]
Curve target reached.
Next step:
- stop curve trading
- seed XRPL AMM / DEX liquidity
- route future trading to the public market
`);
}
}
public snapshot() {
return {
config: this.config,
state: this.state,
currentPriceXrpPerToken: this.currentPrice(),
remainingSupply: this.config.maxSupply - this.state.sold,
};
}
}
// -------------------------
// Example usage
// -------------------------
const launch = new BondingCurveLaunchpad({
symbol: "TROLL",
issuer: "rIssuerAddressExample",
treasury: "rTreasuryAddressExample",
maxSupply: 1_000_000,
virtualXrp: 250,
virtualToken: 500_000,
feeBps: 300, // 3%
graduationTargetXrp: 10_000,
});
console.log("Initial snapshot:", launch.snapshot());
const q1 = launch.executeBuy("rUserOne", 100);
console.log("Buy quote/result:", q1);
console.log("Snapshot:", launch.snapshot());
const q2 = launch.executeBuy("rUserTwo", 250);
console.log("Buy quote/result:", q2);
console.log("Snapshot:", launch.snapshot());
const q3 = launch.executeSell("rUserOne", 5000);
console.log("Sell quote/result:", q3);
console.log("Snapshot:", launch.snapshot());