@faktoryfun/core-sdk
Version:
The official SDK for interacting with Faktory tokens and DEX contracts
399 lines (398 loc) • 19.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.FaktorySDK = void 0;
const transactions_1 = require("@stacks/transactions");
const network_1 = require("./network");
class FaktorySDK {
constructor(config) {
this.SBTC_CONTRACT = {
mainnet: {
address: "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4",
name: "sbtc-token",
assetName: "sbtc-token",
},
testnet: {
address: "STV9K21TBFAK4KNRJXF5DFP8N7W46G4V9RJ5XDY2",
name: "sbtc-token",
assetName: "sbtc-token",
},
devnet: {
address: "STV9K21TBFAK4KNRJXF5DFP8N7W46G4V9RJ5XDY2",
name: "sbtc-token",
assetName: "sbtc-token",
},
mocknet: {
address: "STV9K21TBFAK4KNRJXF5DFP8N7W46G4V9RJ5XDY2",
name: "sbtc-token",
assetName: "sbtc-token",
},
};
this.network = (0, network_1.validateNetwork)(config.network);
this.apiHost =
config.apiHost ||
(this.network === "testnet"
? "https://faktory-testnet-be.vercel.app/api"
: "https://faktory-be.vercel.app/api");
this.apiKey =
config.apiKey ||
"jc_403bb168f0490084db3b3f6b24e9462e72ecf722ff944269b21d5278d4cf1461";
this.hiroApiKey = config.hiroApiKey;
}
async fetch(endpoint, options = {}) {
const url = `${this.apiHost}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
...options.headers,
Authorization: `Bearer ${this.apiKey}`,
},
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
}
// New method - user handles deployment
async getTokenDeployParams(input) {
// Validate required fields
const requiredFields = [
"symbol",
"name",
"description",
"supply",
"targetStx",
"creatorAddress",
"initialBuyAmount",
"targetAmm",
];
for (const field of requiredFields) {
if (input[field] === undefined || input[field] === null) {
throw new Error(`Missing required field: ${field}`);
}
}
if (input.supply > 1000000000) {
throw new Error("Token supply cannot exceed 1 billion tokens for better relatability");
}
return this.fetch("/tokens/create", {
method: "POST",
body: JSON.stringify(input),
headers: {
"Content-Type": "application/json",
},
});
}
// Buy params builder
async getBuyParams({ dexContract, inAmount, // amount is in STX or BTC units
senderAddress, slippage = 15, }) {
const networkObj = (0, network_1.getNetwork)(this.network);
const tokenInfo = await this.getTokenInfo(dexContract);
// Check if token is BTC denominated
const isBtcDenominated = tokenInfo.denomination === "btc";
// Convert based on denomination
let ustx = isBtcDenominated
? inAmount * 100000000 // Convert to satoshis (10^8)
: inAmount * 1000000; // Convert to microSTX (10^6)
const [contractAddress, contractName] = dexContract.split(".");
const [tokenAddress, tokenName] = tokenInfo.tokenContract.split(".");
const isExternal = this.isExternalDex(contractName);
const sbtcContract = this.SBTC_CONTRACT[this.network];
if (isBtcDenominated) {
// For BTC-denominated tokens, we handle differently
// Get buy quote
const buyQuoteCV = await (0, transactions_1.callReadOnlyFunction)({
contractAddress,
contractName,
functionName: "get-in", // Keep same function name
functionArgs: [(0, transactions_1.uintCV)(ustx)],
network: networkObj,
senderAddress,
});
const buyQuoteJSON = (0, transactions_1.cvToJSON)(buyQuoteCV);
const stxToGrad = buyQuoteJSON.value.value["stx-to-grad"]?.value;
if (stxToGrad) {
const maxAllowed = Math.floor(Number(stxToGrad) * 1.15); // 15% leeway
if (Number(ustx) > maxAllowed) {
console.log(`Buy amount (${ustx}) exceeds the recommended amount needed to graduate plus 15% leeway. Adjusting to ${maxAllowed}.`);
ustx = maxAllowed;
}
}
// Internal DEX format
const quoteAmount = buyQuoteJSON.value.value["tokens-out"].value;
const newStx = Number(buyQuoteJSON.value.value["new-stx"].value);
const tokenBalance = buyQuoteJSON.value.value["ft-balance"]?.value;
if (!tokenBalance) {
throw new Error("Missing token balance from quote response");
}
// const minTokensOutInitial = Math.floor(
// Number(quoteAmount) * slippageFactor
// );
const slippagePercent = BigInt(100 - slippage);
const minTokensOut = (BigInt(quoteAmount) * slippagePercent) / BigInt(100);
const currentStxBalance = Number(buyQuoteJSON.value.value["total-stx"].value);
const isLastBuy = tokenInfo.targetStx &&
currentStxBalance + ustx >=
tokenInfo.targetStx * Math.pow(10, tokenInfo.decimals);
if (!tokenInfo.targetStx) {
throw new Error("Missing target STX from quote response");
}
try {
const postConditions = isLastBuy
? [
(0, transactions_1.makeStandardFungiblePostCondition)(senderAddress, transactions_1.FungibleConditionCode.LessEqual, ustx, (0, transactions_1.createAssetInfo)(sbtcContract.address, sbtcContract.name, sbtcContract.assetName)),
(0, transactions_1.makeContractFungiblePostCondition)(contractAddress, contractName, transactions_1.FungibleConditionCode.GreaterEqual, tokenBalance, (0, transactions_1.createAssetInfo)(tokenAddress, tokenName, tokenInfo.symbol)),
(0, transactions_1.makeContractFungiblePostCondition)(contractAddress, contractName, transactions_1.FungibleConditionCode.GreaterEqual, tokenInfo.targetStx * Math.pow(10, tokenInfo.decimals), (0, transactions_1.createAssetInfo)(sbtcContract.address, sbtcContract.name, sbtcContract.assetName)),
]
: [
(0, transactions_1.makeStandardFungiblePostCondition)(senderAddress, transactions_1.FungibleConditionCode.LessEqual, ustx, (0, transactions_1.createAssetInfo)(sbtcContract.address, sbtcContract.name, sbtcContract.assetName)),
(0, transactions_1.makeContractFungiblePostCondition)(contractAddress, contractName, transactions_1.FungibleConditionCode.GreaterEqual, minTokensOut, (0, transactions_1.createAssetInfo)(tokenAddress, tokenName, tokenInfo.symbol)),
];
return {
contractAddress,
contractName,
functionName: "buy",
functionArgs: [
(0, transactions_1.contractPrincipalCV)(tokenAddress, tokenName),
(0, transactions_1.uintCV)(ustx),
],
network: networkObj,
anchorMode: transactions_1.AnchorMode.Any,
postConditionMode: transactions_1.PostConditionMode.Deny,
postConditions,
};
}
catch (error) {
console.error(`Error creating post conditions:`, error);
throw error;
}
}
else {
// Original STX-denominated logic with similar logs
// Get buy quote
const buyQuoteCV = await (0, transactions_1.callReadOnlyFunction)({
contractAddress,
contractName,
functionName: isExternal ? "get-buyable-tokens" : "get-in",
functionArgs: [(0, transactions_1.uintCV)(ustx)],
network: networkObj,
senderAddress,
});
const buyQuoteJSON = (0, transactions_1.cvToJSON)(buyQuoteCV);
let quoteAmount;
let newStx;
let tokenBalance;
if (isExternal) {
// External DEX format
quoteAmount = buyQuoteJSON.value.value["buyable-token"].value;
const stxBalance = buyQuoteJSON.value.value["stx-balance"].value;
const stxBuy = buyQuoteJSON.value.value["stx-buy"].value;
newStx = Number(stxBalance) - Number(stxBuy);
tokenBalance = buyQuoteJSON.value.value["token-balance"]?.value;
// Check recommend-stx-amount limit for external DEX with 15% leeway
const recommendedStxAmount = buyQuoteJSON.value.value["recommend-stx-amount"]?.value;
if (recommendedStxAmount) {
const maxAllowed = Math.floor(Number(recommendedStxAmount) * 1.15); // 15% leeway
if (Number(ustx) > maxAllowed) {
console.log(`Buy amount (${ustx}) exceeds the recommended amount plus 15% leeway. Adjusting to ${maxAllowed}.`);
ustx = maxAllowed;
}
}
}
else {
// Internal DEX format
quoteAmount = buyQuoteJSON.value.value["tokens-out"].value;
newStx = Number(buyQuoteJSON.value.value["new-stx"].value);
tokenBalance = buyQuoteJSON.value.value["ft-balance"]?.value;
const stxToGrad = buyQuoteJSON.value.value["stx-to-grad"]?.value;
if (stxToGrad) {
const maxAllowed = Math.floor(Number(stxToGrad) * 1.15); // 15% leeway
if (Number(ustx) > maxAllowed) {
console.log(`Buy amount (${ustx}) exceeds the remaining amount needed to graduate plus 15% leeway. Adjusting to ${maxAllowed}.`);
ustx = maxAllowed;
}
}
}
if (!tokenBalance) {
throw new Error("Missing token balance from quote response");
}
const slippagePercent = BigInt(100 - slippage);
const minTokensOut = (BigInt(quoteAmount) * slippagePercent) / BigInt(100);
// const minTokensOut = Math.floor(Number(quoteAmount) * slippageFactor);
let currentStxBalance;
if (isExternal) {
currentStxBalance = Number(buyQuoteJSON.value.value["stx-balance"].value);
}
else {
currentStxBalance = Number(buyQuoteJSON.value.value["total-stx"].value);
}
const isLastBuy = tokenInfo.targetStx &&
currentStxBalance + ustx >=
tokenInfo.targetStx * Math.pow(10, tokenInfo.decimals);
if (!tokenInfo.targetStx) {
throw new Error("Missing target STX from quote response");
}
const postConditions = isLastBuy
? [
(0, transactions_1.makeStandardSTXPostCondition)(senderAddress, transactions_1.FungibleConditionCode.LessEqual, ustx),
(0, transactions_1.makeContractFungiblePostCondition)(contractAddress, contractName, transactions_1.FungibleConditionCode.GreaterEqual, tokenBalance, (0, transactions_1.createAssetInfo)(tokenAddress, tokenName, tokenInfo.symbol)),
(0, transactions_1.makeContractSTXPostCondition)(contractAddress, contractName, transactions_1.FungibleConditionCode.GreaterEqual, tokenInfo.targetStx * Math.pow(10, tokenInfo.decimals)),
]
: [
(0, transactions_1.makeStandardSTXPostCondition)(senderAddress, transactions_1.FungibleConditionCode.LessEqual, ustx),
(0, transactions_1.makeContractFungiblePostCondition)(contractAddress, contractName, transactions_1.FungibleConditionCode.GreaterEqual, minTokensOut, (0, transactions_1.createAssetInfo)(tokenAddress, tokenName, tokenInfo.symbol)),
];
return {
contractAddress,
contractName,
functionName: "buy",
functionArgs: [
(0, transactions_1.contractPrincipalCV)(tokenAddress, tokenName),
(0, transactions_1.uintCV)(ustx),
],
network: networkObj,
anchorMode: transactions_1.AnchorMode.Any,
postConditionMode: transactions_1.PostConditionMode.Deny,
postConditions,
};
}
}
// Sell params builder
async getSellParams({ dexContract, amount, // amount is in TOKEN units
senderAddress, slippage = 15, }) {
const networkObj = (0, network_1.getNetwork)(this.network);
const tokenInfo = await this.getTokenInfo(dexContract);
const amountWithDecimals = BigInt(amount) * BigInt(Math.pow(10, tokenInfo.decimals));
const isBtcDenominated = tokenInfo.denomination === "btc";
const sbtcContract = this.SBTC_CONTRACT[this.network];
const [contractAddress, contractName] = dexContract.split(".");
const [tokenAddress, tokenName] = tokenInfo.tokenContract.split(".");
const isExternal = this.isExternalDex(contractName);
const sellQuoteCV = await (0, transactions_1.callReadOnlyFunction)({
contractAddress,
contractName,
functionName: isExternal ? "get-sellable-stx" : "get-out",
functionArgs: [(0, transactions_1.uintCV)(amountWithDecimals)],
network: networkObj,
senderAddress,
});
const sellQuoteJSON = (0, transactions_1.cvToJSON)(sellQuoteCV);
const quoteAmount = sellQuoteJSON.value.value["stx-out"].value;
const slippagePercent = BigInt(100 - slippage);
const minStxOut = (BigInt(quoteAmount) * slippagePercent) / BigInt(100);
return {
contractAddress,
contractName,
functionName: "sell",
functionArgs: [
(0, transactions_1.contractPrincipalCV)(tokenAddress, tokenName),
(0, transactions_1.uintCV)(amountWithDecimals),
],
network: networkObj,
anchorMode: transactions_1.AnchorMode.Any,
postConditionMode: transactions_1.PostConditionMode.Deny,
postConditions: [
(0, transactions_1.makeStandardFungiblePostCondition)(senderAddress, transactions_1.FungibleConditionCode.LessEqual, amountWithDecimals, (0, transactions_1.createAssetInfo)(tokenAddress, tokenName, tokenInfo.symbol)),
isBtcDenominated
? (0, transactions_1.makeContractFungiblePostCondition)(contractAddress, contractName, transactions_1.FungibleConditionCode.GreaterEqual, minStxOut, (0, transactions_1.createAssetInfo)(sbtcContract.address, sbtcContract.name, sbtcContract.assetName))
: (0, transactions_1.makeContractSTXPostCondition)(contractAddress, contractName, transactions_1.FungibleConditionCode.GreaterEqual, minStxOut),
],
};
}
isExternalDex(contractName) {
return !contractName.endsWith("faktory-dex");
}
// Read-only functions
async getIn(dexContract, senderAddress, stx // amount in STX units
) {
const networkObj = (0, network_1.getNetwork)(this.network);
const [contractAddress, contractName] = dexContract.split(".");
const isExternal = this.isExternalDex(contractName);
const ustx = stx * 1000000; // Convert to microSTX
const result = await (0, transactions_1.callReadOnlyFunction)({
contractAddress,
contractName,
functionName: isExternal ? "get-buyable-tokens" : "get-in",
functionArgs: [(0, transactions_1.uintCV)(ustx)],
network: networkObj,
senderAddress,
});
return (0, transactions_1.cvToJSON)(result);
}
async getOut(dexContract, senderAddress, amount // amount in TOKEN units
) {
const networkObj = (0, network_1.getNetwork)(this.network);
const [contractAddress, contractName] = dexContract.split(".");
const isExternal = this.isExternalDex(contractName);
const tokenInfo = await this.getTokenInfo(dexContract);
const amountWithDecimals = amount * Math.pow(10, tokenInfo.decimals);
const result = await (0, transactions_1.callReadOnlyFunction)({
contractAddress,
contractName,
functionName: isExternal ? "get-sellable-stx" : "get-out",
functionArgs: [(0, transactions_1.uintCV)(amountWithDecimals)],
network: networkObj,
senderAddress,
});
return (0, transactions_1.cvToJSON)(result);
}
async getOpen(dexContract, senderAddress) {
const networkObj = (0, network_1.getNetwork)(this.network);
const [contractAddress, contractName] = dexContract.split(".");
const result = await (0, transactions_1.callReadOnlyFunction)({
contractAddress,
contractName,
functionName: "get-open",
functionArgs: [],
network: networkObj,
senderAddress,
});
return (0, transactions_1.cvToJSON)(result);
}
async getVerifiedTokens(options) {
const url = new URL("/tokens", this.apiHost);
const params = new URLSearchParams({ verified: "true" });
if (options?.search)
params.append("search", options.search);
if (options?.sortOrder)
params.append("sortOrder", options.sortOrder);
if (options?.page)
params.append("page", options.page.toString());
if (options?.limit)
params.append("limit", options.limit.toString());
if (options?.daoOnly)
params.append("daoOnly", "true");
return this.fetch(url.pathname + "?" + params.toString(), { method: "GET" });
}
async getDaoTokens(options) {
return this.getVerifiedTokens({
...options,
daoOnly: true,
});
}
async getTokenInfo(dexContract) {
const response = await this.fetch(`/tokens/${dexContract}`);
return {
symbol: response.data.symbol,
decimals: response.data.decimals,
targetStx: response.data.targetStx,
tokenContract: response.data.tokenContract,
denomination: response.data.denomination || "btc",
};
}
async verifyTransfer(tokenAddress) {
return this.fetch(`/tokens/verify-transfer?token_address=${tokenAddress}`, { method: "GET" });
}
async getToken(dexContract) {
return this.fetch(`/tokens/${dexContract}`, {
method: "GET",
});
}
async getTokenTrades(tokenContract) {
if (!tokenContract) {
throw new Error("Token contract is required");
}
return this.fetch(`/tokens/trades/${tokenContract}`, {
method: "GET",
});
}
}
exports.FaktorySDK = FaktorySDK;