permamind
Version:
An MCP server that provides an immortal memory layer for AI agents and clients
724 lines (658 loc) • 23.9 kB
JavaScript
/**
* Configurable Token Lua Templates
* Based on AOS_tools minting strategies with full configurability
* Supports: Basic, Cascade, Double Mint, and Custom minting strategies
*/
/**
* Generate complete token Lua script with selected minting strategy
*/
export function generateTokenLua(config) {
const baseTemplate = getBaseTokenTemplate(config);
const mintingModule = getMintingModule(config);
return combineTemplates(baseTemplate, mintingModule);
}
/**
* Validate token configuration
*/
export function validateTokenConfig(config) {
const errors = [];
// Basic validation
if (!config.name || config.name.trim().length === 0) {
errors.push("Token name is required");
}
if (!config.ticker || config.ticker.trim().length === 0) {
errors.push("Token ticker is required");
}
if (config.ticker && config.ticker.length > 10) {
errors.push("Token ticker must be 10 characters or less");
}
// Minting strategy validation
if (config.mintingStrategy !== "none" && !config.mintingConfig) {
errors.push("Minting configuration is required for selected strategy");
}
// Initial allocations validation
if (config.initialAllocations) {
let totalAllocated = 0;
Object.entries(config.initialAllocations).forEach(([address, balance]) => {
if (!address || address.trim().length === 0) {
errors.push("Initial allocation address cannot be empty");
}
if (address.length < 20) {
errors.push(`Initial allocation address "${address}" appears to be too short (should be 43 characters)`);
}
const balanceNum = parseFloat(balance);
if (isNaN(balanceNum) || balanceNum < 0) {
errors.push(`Invalid balance "${balance}" for address "${address}"`);
}
else {
totalAllocated += balanceNum;
}
});
// Warn if initial allocations exceed initial supply
if (config.initialSupply) {
const initialSupplyNum = parseFloat(config.initialSupply);
if (!isNaN(initialSupplyNum) && totalAllocated > initialSupplyNum) {
errors.push(`Total initial allocations (${totalAllocated}) exceed initial supply (${initialSupplyNum}). Supply will be adjusted automatically.`);
}
}
}
if (config.mintingConfig) {
switch (config.mintingStrategy) {
case "basic": {
const basicConfig = config.mintingConfig;
if (!basicConfig.buyToken)
errors.push("Buy token address is required for basic minting");
if (!basicConfig.multiplier || basicConfig.multiplier <= 0)
errors.push("Valid multiplier is required");
if (!basicConfig.maxMint)
errors.push("Max mint limit is required");
break;
}
case "cascade": {
const cascadeConfig = config.mintingConfig;
if (!cascadeConfig.buyToken)
errors.push("Buy token address is required for cascade minting");
if (!cascadeConfig.multiplier || cascadeConfig.multiplier <= 0)
errors.push("Valid multiplier is required");
if (!cascadeConfig.baseMintLimit)
errors.push("Base mint limit is required");
if (!cascadeConfig.incrementBlocks ||
cascadeConfig.incrementBlocks <= 0)
errors.push("Valid increment blocks is required");
if (!cascadeConfig.maxCascadeLimit)
errors.push("Max cascade limit is required");
break;
}
case "double_mint": {
const doubleMintConfig = config.mintingConfig;
if (!doubleMintConfig.buyTokens ||
Object.keys(doubleMintConfig.buyTokens).length === 0) {
errors.push("At least one buy token is required for double mint strategy");
}
if (!doubleMintConfig.maxMint)
errors.push("Max mint limit is required");
// Validate each buy token
Object.entries(doubleMintConfig.buyTokens).forEach(([address, tokenConfig]) => {
if (!address || address.trim().length === 0) {
errors.push("Buy token address cannot be empty");
}
if (!tokenConfig.multiplier || tokenConfig.multiplier <= 0) {
errors.push(`Invalid multiplier for buy token ${address}`);
}
});
break;
}
}
}
return {
errors,
valid: errors.length === 0,
};
}
/**
* Combine templates into final Lua script
*/
function combineTemplates(baseTemplate, mintingModule) {
return baseTemplate + mintingModule;
}
/**
* Base token template with standard ERC-20-like functionality
*/
function getBaseTokenTemplate(config) {
const { adminAddress = "", burnable = true, denomination = 12, description = "", initialAllocations = {}, initialSupply = "0", logo = "", name, ticker, transferable = true, } = config;
return `
-- Token: ${name} (${ticker})
-- Generated by Permamind Configurable Token System
local json = require('json')
local bint = require('.bint')(256)
-- Token Configuration
Name = Name or "${name}"
Ticker = Ticker or "${ticker}"
Logo = Logo or "${logo}"
Description = Description or "${description}"
Denomination = Denomination or ${denomination}
-- Token State
Balances = Balances or {}
TotalSupply = TotalSupply or "${initialSupply}"
Minted = Minted or "0"
${initialAllocations && Object.entries(initialAllocations).length > 0
? `-- Initial Balance Allocations (Explicit)
${Object.entries(initialAllocations)
.map(([address, balance]) => `Balances["${address}"] = "${balance}"`)
.join("")}
-- Update total supply to match initial allocations
local function updateSupplyForAllocations()
local totalAllocated = bint("0")
for address, balance in pairs(Balances) do
if balance and balance ~= "0" then
totalAllocated = bint.__add(totalAllocated, bint(balance))
end
end
local currentSupply = bint(TotalSupply or "0")
if bint.__gt(totalAllocated, currentSupply) then
TotalSupply = tostring(totalAllocated)
end
end
-- Call the function to update supply
updateSupplyForAllocations()`
: config.mintingStrategy === "none" &&
initialSupply &&
parseInt(initialSupply) > 0
? `-- Auto-allocate initial supply to owner for 'none' strategy
Balances[ao.id] = "${initialSupply}"
print("Initial supply of ${initialSupply} allocated to owner: " .. ao.id)`
: "-- No initial allocations"}
-- Admin Configuration
Owner = Owner or ao.id
${adminAddress ? `AdminAddress = AdminAddress or "${adminAddress}"` : ""}
-- Features Configuration
Transferable = ${transferable}
Burnable = ${burnable}
-- Utility Functions
local function isOwner(address)
return address == Owner${adminAddress ? ` or address == AdminAddress` : ""}
end
local function hasBalance(address, amount)
local balance = bint(Balances[address] or "0")
return bint.__lt(bint(amount), balance) or bint.__eq(bint(amount), balance)
end
local function addBalance(address, amount)
local currentBalance = bint(Balances[address] or "0")
Balances[address] = tostring(bint.__add(currentBalance, bint(amount)))
end
local function subtractBalance(address, amount)
local currentBalance = bint(Balances[address] or "0")
Balances[address] = tostring(bint.__sub(currentBalance, bint(amount)))
end
-- Core Token Handlers
-- Get token information
Handlers.add('Info', Handlers.utils.hasMatchingTag('Action', 'Info'), function(msg)
ao.send({
Target = msg.From,
Data = json.encode({
Name = Name,
Ticker = Ticker,
Logo = Logo,
Description = Description,
Denomination = tostring(Denomination),
TotalSupply = TotalSupply,
Owner = Owner,
Transferable = Transferable,
Burnable = Burnable,
MintingStrategy = "${config.mintingStrategy}",
ProcessId = ao.id
})
})
end)
-- Get balance for an address
Handlers.add('Balance', Handlers.utils.hasMatchingTag('Action', 'Balance'), function(msg)
local target = msg.Tags.Target or msg.From
local balance = Balances[target] or "0"
ao.send({
Target = msg.From,
Data = json.encode({
Account = target,
Balance = balance,
Ticker = Ticker,
Data = balance
})
})
end)
-- Get all balances
Handlers.add('Balances', Handlers.utils.hasMatchingTag('Action', 'Balances'), function(msg)
ao.send({
Target = msg.From,
Data = json.encode(Balances)
})
end)
${transferable
? `
-- Transfer tokens
Handlers.add('Transfer', Handlers.utils.hasMatchingTag('Action', 'Transfer'), function(msg)
local recipient = msg.Tags.Recipient
local amount = msg.Tags.Quantity or msg.Tags.Amount
assert(recipient, 'Recipient is required')
assert(amount, 'Quantity is required')
assert(hasBalance(msg.From, amount), 'Insufficient balance')
subtractBalance(msg.From, amount)
addBalance(recipient, amount)
-- Send debit notice to sender
ao.send({
Target = msg.From,
Action = 'Debit-Notice',
Recipient = recipient,
Quantity = amount,
Data = "Transfer successful"
})
-- Send credit notice to recipient
ao.send({
Target = recipient,
Action = 'Credit-Notice',
Sender = msg.From,
Quantity = amount,
Data = "Transfer received"
})
end)
`
: ""}
${burnable
? `
-- Burn tokens
Handlers.add('Burn', Handlers.utils.hasMatchingTag('Action', 'Burn'), function(msg)
local amount = msg.Tags.Quantity or msg.Tags.Amount
assert(amount, 'Quantity is required')
assert(hasBalance(msg.From, amount), 'Insufficient balance')
subtractBalance(msg.From, amount)
TotalSupply = tostring(bint.__sub(bint(TotalSupply), bint(amount)))
ao.send({
Target = msg.From,
Action = 'Burn-Notice',
Quantity = amount,
Data = "Tokens burned successfully"
})
end)
`
: ""}
-- Mint tokens (owner only, if no specific minting strategy)
${config.mintingStrategy === "none"
? `
Handlers.add('Mint', Handlers.utils.hasMatchingTag('Action', 'Mint'), function(msg)
assert(isOwner(msg.From), 'Only owner can mint tokens')
local recipient = msg.Tags.Recipient or msg.From
local amount = msg.Tags.Quantity or msg.Tags.Amount
assert(amount, 'Quantity is required')
addBalance(recipient, amount)
TotalSupply = tostring(bint.__add(bint(TotalSupply), bint(amount)))
Minted = tostring(bint.__add(bint(Minted), bint(amount)))
ao.send({
Target = recipient,
Action = 'Credit-Notice',
Quantity = amount,
Data = "Tokens minted"
})
end)
`
: ""}
-- Transfer ownership (current owner only)
Handlers.add('Transfer-Ownership', Handlers.utils.hasMatchingTag('Action', 'Transfer-Ownership'), function(msg)
assert(isOwner(msg.From), 'Only owner can transfer ownership')
local newOwner = msg.Tags.NewOwner
assert(newOwner, 'NewOwner is required')
Owner = newOwner
ao.send({
Target = msg.From,
Data = "Ownership transferred to " .. newOwner
})
end)`;
}
/**
* Basic minting strategy - simple multiplier-based minting
*/
function getBasicMintTemplate(config) {
const { buyToken, maxMint, multiplier } = config;
return `
-- Basic Minting Strategy
-- Basic Minting Configuration
BuyToken = "${buyToken}"
Multiplier = ${multiplier}
MaxMint = "${maxMint}"
-- Basic Minting Handler
Handlers.prepend('BasicMint',
function(msg)
return msg.From == BuyToken and msg.Action == "Credit-Notice"
end,
function(msg)
local amountToMint = tostring(bint(msg.Quantity) * bint(Multiplier))
local totalAfterMint = bint.__add(bint(Minted), bint(amountToMint))
if bint.__le(totalAfterMint, bint(MaxMint)) then
-- Mint full amount
addBalance(msg.Sender, amountToMint)
TotalSupply = tostring(bint.__add(bint(TotalSupply), bint(amountToMint)))
Minted = tostring(totalAfterMint)
ao.send({
Target = msg.Sender,
Action = 'Mint-Success',
Quantity = amountToMint,
Data = "Tokens minted successfully"
})
else
-- Partial mint or refund
local remainingMintable = bint.__sub(bint(MaxMint), bint(Minted))
if bint.__gt(remainingMintable, bint("0")) then
-- Partial mint
addBalance(msg.Sender, tostring(remainingMintable))
TotalSupply = tostring(bint.__add(bint(TotalSupply), remainingMintable))
Minted = MaxMint
-- Calculate refund
local tokensUsedForMinting = bint.__div(remainingMintable, bint(Multiplier))
local refundAmount = bint.__sub(bint(msg.Quantity), tokensUsedForMinting)
ao.send({
Target = msg.Sender,
Action = 'Mint-Partial',
Quantity = tostring(remainingMintable),
Data = "Partial mint completed"
})
if bint.__gt(refundAmount, bint("0")) then
ao.send({
Target = BuyToken,
Action = 'Transfer',
Recipient = msg.Sender,
Quantity = tostring(refundAmount)
})
end
else
-- Full refund
ao.send({
Target = BuyToken,
Action = 'Transfer',
Recipient = msg.Sender,
Quantity = msg.Quantity
})
ao.send({
Target = msg.Sender,
Action = 'Mint-Failed',
Data = "Minting limit reached. Full refund issued."
})
end
end
end
)`;
}
/**
* Cascade minting strategy - progressive limit increases over time
*/
function getCascadeMintTemplate(config) {
const { baseMintLimit, buyToken, incrementBlocks, maxCascadeLimit, maxMint, multiplier, } = config;
return `
-- Cascade Minting Strategy
-- Cascade Minting Configuration
BuyToken = "${buyToken}"
Multiplier = ${multiplier}
MaxMint = "${maxMint}"
BaseMintLimit = "${baseMintLimit}"
IncrementBlocks = ${incrementBlocks}
MaxCascadeLimit = "${maxCascadeLimit}"
-- Calculate current mint limit based on block height
local function getCurrentMintLimit()
local currentHeight = bint(ao.block or "0")
local increments = bint.__div(currentHeight, bint(IncrementBlocks))
local additionalLimit = bint.__mul(increments, bint(BaseMintLimit))
local currentLimit = bint.__add(bint(BaseMintLimit), additionalLimit)
-- Cap at maximum cascade limit
if bint.__gt(currentLimit, bint(MaxCascadeLimit)) then
return MaxCascadeLimit
end
return tostring(currentLimit)
end
-- Cascade Minting Handler
Handlers.prepend('CascadeMint',
function(msg)
return msg.From == BuyToken and msg.Action == "Credit-Notice"
end,
function(msg)
local amountToMint = tostring(bint(msg.Quantity) * bint(Multiplier))
local currentMintLimit = getCurrentMintLimit()
local totalAfterMint = bint.__add(bint(Minted), bint(amountToMint))
if bint.__le(totalAfterMint, bint(currentMintLimit)) then
-- Mint full amount
addBalance(msg.Sender, amountToMint)
TotalSupply = tostring(bint.__add(bint(TotalSupply), bint(amountToMint)))
Minted = tostring(totalAfterMint)
ao.send({
Target = msg.Sender,
Action = 'Cascade-Mint-Success',
Quantity = amountToMint,
CurrentLimit = currentMintLimit,
Data = "Tokens minted successfully"
})
else
-- Partial mint or refund
local remainingMintable = bint.__sub(bint(currentMintLimit), bint(Minted))
if bint.__gt(remainingMintable, bint("0")) then
-- Partial mint
addBalance(msg.Sender, tostring(remainingMintable))
TotalSupply = tostring(bint.__add(bint(TotalSupply), remainingMintable))
Minted = currentMintLimit
-- Calculate refund
local tokensUsedForMinting = bint.__div(remainingMintable, bint(Multiplier))
local refundAmount = bint.__sub(bint(msg.Quantity), tokensUsedForMinting)
ao.send({
Target = msg.Sender,
Action = 'Cascade-Mint-Partial',
Quantity = tostring(remainingMintable),
CurrentLimit = currentMintLimit,
Data = "Partial mint completed"
})
if bint.__gt(refundAmount, bint("0")) then
ao.send({
Target = BuyToken,
Action = 'Transfer',
Recipient = msg.Sender,
Quantity = tostring(refundAmount)
})
end
else
-- Full refund
ao.send({
Target = BuyToken,
Action = 'Transfer',
Recipient = msg.Sender,
Quantity = msg.Quantity
})
ao.send({
Target = msg.Sender,
Action = 'Cascade-Mint-Failed',
CurrentLimit = currentMintLimit,
Data = "Current minting limit reached. Full refund issued."
})
end
end
end
)
-- Handler to check current mint limit
Handlers.add('MintLimit', Handlers.utils.hasMatchingTag('Action', 'MintLimit'), function(msg)
ao.send({
Target = msg.From,
Data = json.encode({
CurrentLimit = getCurrentMintLimit(),
Minted = Minted,
BaseMintLimit = BaseMintLimit,
IncrementBlocks = IncrementBlocks,
MaxCascadeLimit = MaxCascadeLimit,
CurrentBlock = ao.block or "0"
})
})
end)`;
}
/**
* Double mint strategy - multiple buy tokens with different multipliers
*/
function getDoubleMintTemplate(config) {
const { buyTokens, maxMint } = config;
const buyTokenConfigs = Object.entries(buyTokens)
.filter(([, config]) => config.enabled)
.map(([address, config]) => ` ["${address}"] = ${config.multiplier}`)
.join(",");
return `
-- Double Mint Strategy
-- Double Mint Configuration
MaxMint = "${maxMint}"
BuyTokenMultipliers = {
${buyTokenConfigs}
}
-- Double Minting Handler
Handlers.prepend('DoubleMint',
function(msg)
return BuyTokenMultipliers[msg.From] ~= nil and msg.Action == "Credit-Notice"
end,
function(msg)
local multiplier = BuyTokenMultipliers[msg.From]
local amountToMint = tostring(bint(msg.Quantity) * bint(multiplier))
local totalAfterMint = bint.__add(bint(Minted), bint(amountToMint))
if bint.__le(totalAfterMint, bint(MaxMint)) then
-- Mint full amount
addBalance(msg.Sender, amountToMint)
TotalSupply = tostring(bint.__add(bint(TotalSupply), bint(amountToMint)))
Minted = tostring(totalAfterMint)
ao.send({
Target = msg.Sender,
Action = 'Double-Mint-Success',
Quantity = amountToMint,
BuyToken = msg.From,
Multiplier = tostring(multiplier),
Data = "Tokens minted successfully"
})
else
-- Partial mint or refund
local remainingMintable = bint.__sub(bint(MaxMint), bint(Minted))
if bint.__gt(remainingMintable, bint("0")) then
-- Partial mint
addBalance(msg.Sender, tostring(remainingMintable))
TotalSupply = tostring(bint.__add(bint(TotalSupply), remainingMintable))
Minted = MaxMint
-- Calculate refund
local tokensUsedForMinting = bint.__div(remainingMintable, bint(multiplier))
local refundAmount = bint.__sub(bint(msg.Quantity), tokensUsedForMinting)
ao.send({
Target = msg.Sender,
Action = 'Double-Mint-Partial',
Quantity = tostring(remainingMintable),
BuyToken = msg.From,
Data = "Partial mint completed"
})
if bint.__gt(refundAmount, bint("0")) then
ao.send({
Target = msg.From,
Action = 'Transfer',
Recipient = msg.Sender,
Quantity = tostring(refundAmount)
})
end
else
-- Full refund
ao.send({
Target = msg.From,
Action = 'Transfer',
Recipient = msg.Sender,
Quantity = msg.Quantity
})
ao.send({
Target = msg.Sender,
Action = 'Double-Mint-Failed',
Data = "Minting limit reached. Full refund issued."
})
end
end
end
)
-- Handler to check buy token configurations
Handlers.add('BuyTokens', Handlers.utils.hasMatchingTag('Action', 'BuyTokens'), function(msg)
ao.send({
Target = msg.From,
Data = json.encode(BuyTokenMultipliers)
})
end)`;
}
/**
* Get minting module based on strategy
*/
function getMintingModule(config) {
switch (config.mintingStrategy) {
case "basic":
return getBasicMintTemplate(config.mintingConfig);
case "cascade":
return getCascadeMintTemplate(config.mintingConfig);
case "double_mint":
return getDoubleMintTemplate(config.mintingConfig);
case "none":
return "";
default:
throw new Error(`Unknown minting strategy: ${config.mintingStrategy}`);
}
}
/**
* Generate example configurations for different strategies
*/
export const exampleConfigs = {
basic: {
mintingConfig: {
buyToken: "xU9zFkq3X2ZQ6olwNVvr1vUWIjc3kXTWr7xKQD6dh10", // wAR
maxMint: "1000000000000000000",
multiplier: 1000,
},
mintingStrategy: "basic",
name: "Basic Mint Token",
ticker: "BMT",
},
cascade: {
mintingConfig: {
baseMintLimit: "100000000000000000",
buyToken: "xU9zFkq3X2ZQ6olwNVvr1vUWIjc3kXTWr7xKQD6dh10", // wAR
incrementBlocks: 670,
maxCascadeLimit: "1000000000000000000000",
maxMint: "1000000000000000000",
multiplier: 1000,
},
mintingStrategy: "cascade",
name: "Cascade Mint Token",
ticker: "CMT",
},
doubleMint: {
mintingConfig: {
buyTokens: {
OT9qTE2467gcozb2g8R6D6N3nQS94ENcaAIJfUzHCww: {
enabled: true,
// TRUNK
multiplier: 500,
},
xU9zFkq3X2ZQ6olwNVvr1vUWIjc3kXTWr7xKQD6dh10: {
enabled: true,
// wAR
multiplier: 1000,
},
},
maxMint: "1000000000000000000",
},
mintingStrategy: "double_mint",
name: "Double Mint Token",
ticker: "DMT",
},
simple: {
initialSupply: "1000000000000000000",
mintingStrategy: "none",
name: "Simple Token",
ticker: "SIMP",
},
withAllocations: {
initialAllocations: {
alice_address_example_123456789012345678901: "300000000000000000", // 30% to early investor
bob_address_example_123456789012345678901234: "200000000000000000", // 20% to team
Y3EMIurCZKqO8Dm_86dsbdHNdwM86Yswk7v4hsGp45I: "500000000000000000", // 50% to founder
},
initialSupply: "1000000000000000000",
mintingStrategy: "none",
name: "Pre-allocated Token",
ticker: "ALLOC",
},
};
export const luaModule = generateTokenLua;