UNPKG

@coinbase/agentkit

Version:

Coinbase AgentKit core primitives

508 lines (507 loc) 19.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getSwapAmountsFromReceipt = exports.getSwapAmountsFromLog = exports.memecoinToEthWithPermit2 = exports.ethToMemecoin = exports.getAmountWithSlippage = exports.generateTokenUri = void 0; const viem_1 = require("viem"); const constants_1 = require("./constants"); /** * Uploads a base64 image to IPFS using Pinata * * @param params - Configuration and base64 image data * @param params.pinataConfig - Pinata configuration including JWT * @param params.base64Image - Base64 encoded image data * @param params.name - Optional name for the uploaded file * @param params.metadata - Optional metadata key-value pairs * @returns Upload response with CID and other details */ const uploadImageToIPFS = async (params) => { try { const formData = new FormData(); // Convert base64 to Blob and then to File // Remove data URL prefix if present (e.g., "data:image/jpeg;base64,") const base64Data = params.base64Image.split(",")[1] || params.base64Image; const byteCharacters = atob(base64Data); const byteArrays = []; for (let offset = 0; offset < byteCharacters.length; offset += 1024) { const slice = byteCharacters.slice(offset, offset + 1024); const byteNumbers = new Array(slice.length); for (let i = 0; i < slice.length; i++) { byteNumbers[i] = slice.charCodeAt(i); } const byteArray = new Uint8Array(byteNumbers); byteArrays.push(byteArray); } // Detect mime type from base64 string let mimeType = "image/png"; // default if (params.base64Image.startsWith("data:")) { mimeType = params.base64Image.split(";")[0].split(":")[1]; } const blob = new Blob(byteArrays, { type: mimeType }); const fileName = params.name || `image.${mimeType.split("/")[1]}`; const file = new File([blob], fileName, { type: mimeType }); formData.append("file", file); const pinataMetadata = { name: params.name || null, keyvalues: params.metadata || {}, }; formData.append("pinataMetadata", JSON.stringify(pinataMetadata)); const pinataOptions = { cidVersion: 1, }; formData.append("pinataOptions", JSON.stringify(pinataOptions)); const response = await fetch("https://api.pinata.cloud/pinning/pinFileToIPFS", { method: "POST", headers: { Authorization: `Bearer ${params.pinataConfig.jwt}`, }, body: formData, }); if (!response.ok) { const error = await response.json(); throw new Error(`Failed to upload image to IPFS: ${error.message || response.statusText}`); } const data = await response.json(); return { IpfsHash: data.IpfsHash, PinSize: data.PinSize, Timestamp: data.Timestamp, isDuplicate: data.isDuplicate || false, }; } catch (error) { if (error instanceof Error) { throw new Error(`Failed to upload image to IPFS: ${error.message}`); } throw error; } }; /** * Uploads JSON data to IPFS using Pinata * * @param params - Configuration and JSON data * @param params.pinataConfig - Pinata configuration including JWT * @param params.json - JSON data to upload * @param params.name - Optional name for the uploaded file * @param params.metadata - Optional metadata key-value pairs * @returns Upload response with CID and other details */ const uploadJsonToIPFS = async (params) => { try { const requestBody = { pinataOptions: { cidVersion: 1, }, pinataMetadata: { name: params.name || null, keyvalues: params.metadata || {}, }, pinataContent: params.json, }; const response = await fetch("https://api.pinata.cloud/pinning/pinJSONToIPFS", { method: "POST", headers: { Authorization: `Bearer ${params.pinataConfig.jwt}`, "Content-Type": "application/json", }, body: JSON.stringify(requestBody), }); if (!response.ok) { const error = await response.json(); throw new Error(`Failed to upload JSON to IPFS: ${error.message || response.statusText}`); } const data = await response.json(); return { IpfsHash: data.IpfsHash, PinSize: data.PinSize, Timestamp: data.Timestamp, isDuplicate: data.isDuplicate || false, }; } catch (error) { if (error instanceof Error) { throw new Error(`Failed to upload JSON to IPFS: ${error.message}`); } throw error; } }; const generateTokenUriBase64Image = async (name, params) => { // 1. upload image to IPFS const imageRes = await uploadImageToIPFS({ pinataConfig: params.pinataConfig, base64Image: params.metadata.base64Image, }); // 2. upload metadata to IPFS const coinMetadata = { name, description: params.metadata.description, image: `ipfs://${imageRes.IpfsHash}`, external_link: params.metadata.websiteUrl || "", collaborators: [], discordUrl: params.metadata.discordUrl || "", twitterUrl: params.metadata.twitterUrl || "", telegramUrl: params.metadata.telegramUrl || "", }; const metadataRes = await uploadJsonToIPFS({ pinataConfig: params.pinataConfig, json: coinMetadata, }); return `ipfs://${metadataRes.IpfsHash}`; }; const generateTokenUri = async (name, params) => { // 1. get base64Image from imageUrl const response = await fetch(params.metadata.imageUrl); if (!response.ok) { throw new Error(`Failed to fetch image: ${response.statusText}`); } const arrayBuffer = await response.arrayBuffer(); const base64Image = Buffer.from(arrayBuffer).toString("base64"); // 2. generate token uri const tokenUri = await generateTokenUriBase64Image(name, { pinataConfig: params.pinataConfig, metadata: { base64Image, description: params.metadata.description, websiteUrl: params.metadata.websiteUrl, discordUrl: params.metadata.discordUrl, twitterUrl: params.metadata.twitterUrl, telegramUrl: params.metadata.telegramUrl, }, }); return tokenUri; }; exports.generateTokenUri = generateTokenUri; const getAmountWithSlippage = (amount, slippage, swapType) => { if (amount == null) { return 0n; } const absAmount = amount < 0n ? -amount : amount; const slippageMultiplier = swapType === "EXACT_IN" ? BigInt(1e18) - (0, viem_1.parseEther)(slippage) : BigInt(1e18) + (0, viem_1.parseEther)(slippage); return (absAmount * slippageMultiplier) / BigInt(1e18); }; exports.getAmountWithSlippage = getAmountWithSlippage; const ETH = viem_1.zeroAddress; const ethToMemecoin = (params) => { const flETH = constants_1.FLETHAddress[params.chainId]; const flETHHooks = constants_1.FLETHHooksAddress[params.chainId]; const flaunchHooks = constants_1.FlaunchPositionManagerAddress[params.chainId]; // Determine actions based on swapType const v4Actions = ("0x" + (params.swapType === "EXACT_IN" ? constants_1.V4Actions.SWAP_EXACT_IN : constants_1.V4Actions.SWAP_EXACT_OUT) + constants_1.V4Actions.SETTLE_ALL + constants_1.V4Actions.TAKE_ALL); // Initialize variables for path and v4Params let path; let v4Params; // Configure path and parameters based on swapType if (params.swapType === "EXACT_IN") { if (params.amountIn == null || params.amountOutMin == null) { throw new Error("amountIn and amountOutMin are required for EXACT_IN swap"); } // Path for 'EXACT_IN' swap path = [ { intermediateCurrency: flETH, fee: 0, tickSpacing: 60, hooks: flETHHooks, hookData: "0x", }, { intermediateCurrency: params.memecoin, fee: 0, tickSpacing: 60, hooks: flaunchHooks, hookData: (0, viem_1.encodeAbiParameters)([{ type: "address", name: "referrer" }], [params.referrer ?? viem_1.zeroAddress]), }, ]; // Parameters for 'EXACT_IN' swap v4Params = (0, viem_1.encodeAbiParameters)(constants_1.IV4RouterAbiExactInput, [ { currencyIn: ETH, path: path, amountIn: params.amountIn, amountOutMinimum: params.amountOutMin, }, ]); } else { if (params.amountOut == null || params.amountInMax == null) { throw new Error("amountOut and amountInMax are required for EXACT_OUT swap"); } // Path for 'EXACT_OUT' swap path = [ { fee: 0, tickSpacing: 60, hookData: "0x", hooks: flETHHooks, intermediateCurrency: ETH, }, { fee: 0, tickSpacing: 60, hooks: flaunchHooks, intermediateCurrency: flETH, hookData: (0, viem_1.encodeAbiParameters)([{ type: "address", name: "referrer" }], [params.referrer ?? viem_1.zeroAddress]), }, ]; // Parameters for 'EXACT_OUT' swap v4Params = (0, viem_1.encodeAbiParameters)(constants_1.IV4RouterAbiExactOutput, [ { currencyOut: params.memecoin, path: path, amountOut: params.amountOut, amountInMaximum: params.amountInMax, }, ]); } // Common parameters for both swap types const settleParams = (0, viem_1.encodeAbiParameters)([ { type: "address", name: "currency", }, { type: "uint256", name: "maxAmount", }, ], [ ETH, params.swapType === "EXACT_IN" ? (params.amountIn ?? viem_1.maxUint256) : (params.amountInMax ?? viem_1.maxUint256), ]); const takeParams = (0, viem_1.encodeAbiParameters)([ { type: "address", name: "currency", }, { type: "uint256", name: "minAmount", }, ], [ params.memecoin, params.swapType === "EXACT_IN" ? (params.amountOutMin ?? viem_1.maxUint256) : (params.amountOut ?? viem_1.maxUint256), ]); // Encode router data const v4RouterData = (0, viem_1.encodeAbiParameters)([ { type: "bytes", name: "actions" }, { type: "bytes[]", name: "params" }, ], [v4Actions, [v4Params, settleParams, takeParams]]); // Commands for Universal Router const urCommands = ("0x" + constants_1.URCommands.V4_SWAP + constants_1.URCommands.SWEEP); const sweepInput = (0, viem_1.encodeAbiParameters)([ { type: "address", name: "token" }, { type: "address", name: "recipient" }, { type: "uint160", name: "amountIn" }, ], [ETH, params.sender, 0n]); // Encode calldata for Universal Router const inputs = [v4RouterData, sweepInput]; const urExecuteCalldata = (0, viem_1.encodeFunctionData)({ abi: constants_1.UNIVERSAL_ROUTER_ABI, functionName: "execute", args: [urCommands, inputs], }); return { calldata: urExecuteCalldata, commands: urCommands, inputs, }; }; exports.ethToMemecoin = ethToMemecoin; // @notice Before calling the UniversalRouter the user must have: // 1. Given the Permit2 contract allowance to spend the memecoin const memecoinToEthWithPermit2 = (params) => { const flETH = constants_1.FLETHAddress[params.chainId]; const flETHHooks = constants_1.FLETHHooksAddress[params.chainId]; const flaunchHooks = constants_1.FlaunchPositionManagerAddress[params.chainId]; const v4Actions = ("0x" + constants_1.V4Actions.SWAP_EXACT_IN + constants_1.V4Actions.SETTLE_ALL + constants_1.V4Actions.TAKE_ALL); const v4ExactInputParams = (0, viem_1.encodeAbiParameters)(constants_1.IV4RouterAbiExactInput, [ { currencyIn: params.memecoin, path: [ { intermediateCurrency: flETH, fee: 0, tickSpacing: 60, hooks: flaunchHooks, hookData: (0, viem_1.encodeAbiParameters)([ { type: "address", name: "referrer", }, ], [params.referrer ?? viem_1.zeroAddress]), }, { intermediateCurrency: ETH, fee: 0, tickSpacing: 60, hooks: flETHHooks, hookData: "0x", }, ], amountIn: params.amountIn, amountOutMinimum: params.ethOutMin, }, ]); const settleParams = (0, viem_1.encodeAbiParameters)([ { type: "address", name: "currency", }, { type: "uint256", name: "maxAmount", }, ], [params.memecoin, params.amountIn]); const takeParams = (0, viem_1.encodeAbiParameters)([ { type: "address", name: "currency", }, { type: "uint256", name: "minAmount", }, ], [ETH, params.ethOutMin]); const v4RouterData = (0, viem_1.encodeAbiParameters)([ { type: "bytes", name: "actions" }, { type: "bytes[]", name: "params" }, ], [v4Actions, [v4ExactInputParams, settleParams, takeParams]]); if (params.signature && params.permitSingle) { const urCommands = ("0x" + constants_1.URCommands.PERMIT2_PERMIT + constants_1.URCommands.V4_SWAP); const permit2PermitInput = (0, viem_1.encodeAbiParameters)([ { type: "tuple", components: [ { type: "tuple", components: [ { type: "address", name: "token" }, { type: "uint160", name: "amount" }, { type: "uint48", name: "expiration" }, { type: "uint48", name: "nonce" }, ], name: "details", }, { type: "address", name: "spender" }, { type: "uint256", name: "sigDeadline" }, ], name: "PermitSingle", }, { type: "bytes", name: "signature" }, ], [params.permitSingle, params.signature]); const inputs = [permit2PermitInput, v4RouterData]; const urExecuteCalldata = (0, viem_1.encodeFunctionData)({ abi: constants_1.UNIVERSAL_ROUTER_ABI, functionName: "execute", args: [urCommands, inputs], }); return { calldata: urExecuteCalldata, commands: urCommands, inputs, }; } else { const urCommands = ("0x" + constants_1.URCommands.V4_SWAP); const inputs = [v4RouterData]; const urExecuteCalldata = (0, viem_1.encodeFunctionData)({ abi: constants_1.UNIVERSAL_ROUTER_ABI, functionName: "execute", args: [urCommands, inputs], }); return { calldata: urExecuteCalldata, commands: urCommands, inputs, }; } }; exports.memecoinToEthWithPermit2 = memecoinToEthWithPermit2; const getSwapAmountsFromLog = ({ filteredPoolSwapEvent, coinAddress, chainId, }) => { const { flAmount0, flAmount1, flFee0, flFee1, ispAmount0, ispAmount1, ispFee0, ispFee1, uniAmount0, uniAmount1, uniFee0, uniFee1, } = filteredPoolSwapEvent; const currency0Delta = flAmount0 + ispAmount0 + uniAmount0; const currency1Delta = flAmount1 + ispAmount1 + uniAmount1; const currency0Fees = flFee0 + ispFee0 + uniFee0; const currency1Fees = flFee1 + ispFee1 + uniFee1; let feesIsInFLETH; let swapType; const flETHIsCurrencyZero = coinAddress > constants_1.FLETHAddress[chainId]; if (flETHIsCurrencyZero) { swapType = currency0Delta < 0 ? "BUY" : "SELL"; feesIsInFLETH = currency0Fees < 0; } else { swapType = currency1Delta < 0 ? "BUY" : "SELL"; feesIsInFLETH = currency1Fees < 0; } const absCurrency0Delta = currency0Delta < 0 ? -currency0Delta : currency0Delta; const absCurrency1Delta = currency1Delta < 0 ? -currency1Delta : currency1Delta; const absCurrency0Fees = currency0Fees < 0 ? -currency0Fees : currency0Fees; const absCurrency1Fees = currency1Fees < 0 ? -currency1Fees : currency1Fees; const fees = { isInFLETH: feesIsInFLETH, amount: flETHIsCurrencyZero ? feesIsInFLETH ? absCurrency0Fees : absCurrency1Fees : feesIsInFLETH ? absCurrency1Fees : absCurrency0Fees, }; if (swapType === "BUY") { return { coinsBought: flETHIsCurrencyZero ? absCurrency1Delta - (!fees.isInFLETH ? fees.amount : 0n) : absCurrency0Delta - (!fees.isInFLETH ? fees.amount : 0n), ethSold: flETHIsCurrencyZero ? absCurrency0Delta - (fees.isInFLETH ? fees.amount : 0n) : absCurrency1Delta - (fees.isInFLETH ? fees.amount : 0n), }; } else { return { coinsSold: flETHIsCurrencyZero ? absCurrency1Delta - (!fees.isInFLETH ? fees.amount : 0n) : absCurrency0Delta - (!fees.isInFLETH ? fees.amount : 0n), ethBought: flETHIsCurrencyZero ? absCurrency0Delta - (fees.isInFLETH ? fees.amount : 0n) : absCurrency1Delta - (fees.isInFLETH ? fees.amount : 0n), }; } }; exports.getSwapAmountsFromLog = getSwapAmountsFromLog; const getSwapAmountsFromReceipt = ({ receipt, coinAddress, chainId, }) => { const filteredPoolSwapEvent = receipt.logs .map(log => { try { if (log.address.toLowerCase() !== constants_1.FlaunchPositionManagerAddress[chainId].toLowerCase()) { return null; } const event = (0, viem_1.decodeEventLog)({ abi: constants_1.POSITION_MANAGER_ABI, data: log.data, topics: log.topics, }); return event.eventName === "PoolSwap" ? event.args : null; } catch { return null; } }) .filter((event) => event !== null)[0]; return (0, exports.getSwapAmountsFromLog)({ filteredPoolSwapEvent, coinAddress, chainId, }); }; exports.getSwapAmountsFromReceipt = getSwapAmountsFromReceipt;