UNPKG

@faktoryfun/core-sdk

Version:

The official SDK for interacting with Faktory tokens and DEX contracts

399 lines (398 loc) 19.8 kB
"use strict"; 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;