diff --git a/cdp-agentkit-core/typescript/src/action_providers/wow/README.md b/cdp-agentkit-core/typescript/src/action_providers/wow/README.md new file mode 100644 index 000000000..32b59d4f9 --- /dev/null +++ b/cdp-agentkit-core/typescript/src/action_providers/wow/README.md @@ -0,0 +1 @@ +# WOW Action Provider \ No newline at end of file diff --git a/cdp-agentkit-core/typescript/src/action_providers/wow/constants.ts b/cdp-agentkit-core/typescript/src/action_providers/wow/constants.ts new file mode 100644 index 000000000..a623f3af6 --- /dev/null +++ b/cdp-agentkit-core/typescript/src/action_providers/wow/constants.ts @@ -0,0 +1,852 @@ +import type { Abi } from "abitype"; + +export const SUPPORTED_NETWORKS = ["base-mainnet"]; + +export const WOW_FACTORY_ABI: Abi = [ + { + type: "constructor", + inputs: [ + { name: "_tokenImplementation", type: "address", internalType: "address" }, + { name: "_bondingCurve", type: "address", internalType: "address" }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "UPGRADE_INTERFACE_VERSION", + inputs: [], + outputs: [{ name: "", type: "string", internalType: "string" }], + stateMutability: "view", + }, + { + type: "function", + name: "bondingCurve", + inputs: [], + outputs: [{ name: "", type: "address", internalType: "address" }], + stateMutability: "view", + }, + { + type: "function", + name: "deploy", + inputs: [ + { name: "_tokenCreator", type: "address", internalType: "address" }, + { name: "_platformReferrer", type: "address", internalType: "address" }, + { name: "_tokenURI", type: "string", internalType: "string" }, + { name: "_name", type: "string", internalType: "string" }, + { name: "_symbol", type: "string", internalType: "string" }, + ], + outputs: [{ name: "", type: "address", internalType: "address" }], + stateMutability: "payable", + }, + { + type: "function", + name: "implementation", + inputs: [], + outputs: [{ name: "", type: "address", internalType: "address" }], + stateMutability: "view", + }, + { + type: "function", + name: "initialize", + inputs: [{ name: "_owner", type: "address", internalType: "address" }], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "owner", + inputs: [], + outputs: [{ name: "", type: "address", internalType: "address" }], + stateMutability: "view", + }, + { + type: "function", + name: "proxiableUUID", + inputs: [], + outputs: [{ name: "", type: "bytes32", internalType: "bytes32" }], + stateMutability: "view", + }, + { + type: "function", + name: "renounceOwnership", + inputs: [], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "tokenImplementation", + inputs: [], + outputs: [{ name: "", type: "address", internalType: "address" }], + stateMutability: "view", + }, + { + type: "function", + name: "transferOwnership", + inputs: [{ name: "newOwner", type: "address", internalType: "address" }], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "upgradeToAndCall", + inputs: [ + { name: "newImplementation", type: "address", internalType: "address" }, + { name: "data", type: "bytes", internalType: "bytes" }, + ], + outputs: [], + stateMutability: "payable", + }, + { + type: "event", + name: "Initialized", + inputs: [{ name: "version", type: "uint64", indexed: false, internalType: "uint64" }], + anonymous: false, + }, + { + type: "event", + name: "OwnershipTransferred", + inputs: [ + { name: "previousOwner", type: "address", indexed: true, internalType: "address" }, + { name: "newOwner", type: "address", indexed: true, internalType: "address" }, + ], + anonymous: false, + }, + { + type: "event", + name: "Upgraded", + inputs: [{ name: "implementation", type: "address", indexed: true, internalType: "address" }], + anonymous: false, + }, + { + type: "error", + name: "AddressEmptyCode", + inputs: [{ name: "target", type: "address", internalType: "address" }], + }, + { type: "error", name: "ERC1167FailedCreateClone", inputs: [] }, + { + type: "error", + name: "ERC1967InvalidImplementation", + inputs: [{ name: "implementation", type: "address", internalType: "address" }], + }, + { type: "error", name: "ERC1967NonPayable", inputs: [] }, + { type: "error", name: "FailedInnerCall", inputs: [] }, + { type: "error", name: "InvalidInitialization", inputs: [] }, + { type: "error", name: "NotInitializing", inputs: [] }, + { + type: "error", + name: "OwnableInvalidOwner", + inputs: [{ name: "owner", type: "address", internalType: "address" }], + }, + { + type: "error", + name: "OwnableUnauthorizedAccount", + inputs: [{ name: "account", type: "address", internalType: "address" }], + }, + { type: "error", name: "ReentrancyGuardReentrantCall", inputs: [] }, + { type: "error", name: "UUPSUnauthorizedCallContext", inputs: [] }, + { + type: "error", + name: "UUPSUnsupportedProxiableUUID", + inputs: [{ name: "slot", type: "bytes32", internalType: "bytes32" }], + }, +]; + +export const WOW_ABI: Abi = [ + { + inputs: [ + { internalType: "address", name: "_protocolFeeRecipient", type: "address" }, + { internalType: "address", name: "_protocolRewards", type: "address" }, + { internalType: "address", name: "_weth", type: "address" }, + { internalType: "address", name: "_nonfungiblePositionManager", type: "address" }, + { internalType: "address", name: "_swapRouter", type: "address" }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { + inputs: [{ internalType: "address", name: "target", type: "address" }], + name: "AddressEmptyCode", + type: "error", + }, + { + inputs: [{ internalType: "address", name: "account", type: "address" }], + name: "AddressInsufficientBalance", + type: "error", + }, + { inputs: [], name: "AddressZero", type: "error" }, + { + inputs: [ + { internalType: "address", name: "spender", type: "address" }, + { internalType: "uint256", name: "allowance", type: "uint256" }, + { internalType: "uint256", name: "needed", type: "uint256" }, + ], + name: "ERC20InsufficientAllowance", + type: "error", + }, + { + inputs: [ + { internalType: "address", name: "sender", type: "address" }, + { internalType: "uint256", name: "balance", type: "uint256" }, + { internalType: "uint256", name: "needed", type: "uint256" }, + ], + name: "ERC20InsufficientBalance", + type: "error", + }, + { + inputs: [{ internalType: "address", name: "approver", type: "address" }], + name: "ERC20InvalidApprover", + type: "error", + }, + { + inputs: [{ internalType: "address", name: "receiver", type: "address" }], + name: "ERC20InvalidReceiver", + type: "error", + }, + { + inputs: [{ internalType: "address", name: "sender", type: "address" }], + name: "ERC20InvalidSender", + type: "error", + }, + { + inputs: [{ internalType: "address", name: "spender", type: "address" }], + name: "ERC20InvalidSpender", + type: "error", + }, + { inputs: [], name: "EthAmountTooSmall", type: "error" }, + { inputs: [], name: "EthTransferFailed", type: "error" }, + { inputs: [], name: "FailedInnerCall", type: "error" }, + { inputs: [], name: "InitialOrderSizeTooLarge", type: "error" }, + { inputs: [], name: "InsufficientFunds", type: "error" }, + { inputs: [], name: "InsufficientLiquidity", type: "error" }, + { inputs: [], name: "InvalidInitialization", type: "error" }, + { inputs: [], name: "InvalidMarketType", type: "error" }, + { inputs: [], name: "MarketAlreadyGraduated", type: "error" }, + { inputs: [], name: "MarketNotGraduated", type: "error" }, + { inputs: [], name: "NotInitializing", type: "error" }, + { inputs: [], name: "OnlyPool", type: "error" }, + { inputs: [], name: "OnlyWeth", type: "error" }, + { inputs: [], name: "ReentrancyGuardReentrantCall", type: "error" }, + { + inputs: [{ internalType: "address", name: "token", type: "address" }], + name: "SafeERC20FailedOperation", + type: "error", + }, + { inputs: [], name: "SlippageBoundsExceeded", type: "error" }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "owner", type: "address" }, + { indexed: true, internalType: "address", name: "spender", type: "address" }, + { indexed: false, internalType: "uint256", name: "value", type: "uint256" }, + ], + name: "Approval", + type: "event", + }, + { + anonymous: false, + inputs: [{ indexed: false, internalType: "uint64", name: "version", type: "uint64" }], + name: "Initialized", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "from", type: "address" }, + { indexed: true, internalType: "address", name: "to", type: "address" }, + { indexed: false, internalType: "uint256", name: "value", type: "uint256" }, + ], + name: "Transfer", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "tokenAddress", type: "address" }, + { indexed: true, internalType: "address", name: "poolAddress", type: "address" }, + { + indexed: false, + internalType: "uint256", + name: "totalEthLiquidity", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "totalTokenLiquidity", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "lpPositionId", + type: "uint256", + }, + { + indexed: false, + internalType: "enum IWow.MarketType", + name: "marketType", + type: "uint8", + }, + ], + name: "WowMarketGraduated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "buyer", type: "address" }, + { indexed: true, internalType: "address", name: "recipient", type: "address" }, + { + indexed: true, + internalType: "address", + name: "orderReferrer", + type: "address", + }, + { indexed: false, internalType: "uint256", name: "totalEth", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "ethFee", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "ethSold", type: "uint256" }, + { + indexed: false, + internalType: "uint256", + name: "tokensBought", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "buyerTokenBalance", + type: "uint256", + }, + { indexed: false, internalType: "string", name: "comment", type: "string" }, + { + indexed: false, + internalType: "enum IWow.MarketType", + name: "marketType", + type: "uint8", + }, + ], + name: "WowTokenBuy", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "factoryAddress", + type: "address", + }, + { indexed: true, internalType: "address", name: "tokenCreator", type: "address" }, + { + indexed: false, + internalType: "address", + name: "platformReferrer", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "protocolFeeRecipient", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "bondingCurve", + type: "address", + }, + { indexed: false, internalType: "string", name: "tokenURI", type: "string" }, + { indexed: false, internalType: "string", name: "name", type: "string" }, + { indexed: false, internalType: "string", name: "symbol", type: "string" }, + { + indexed: false, + internalType: "address", + name: "tokenAddress", + type: "address", + }, + { indexed: false, internalType: "address", name: "poolAddress", type: "address" }, + ], + name: "WowTokenCreated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "tokenCreator", type: "address" }, + { + indexed: true, + internalType: "address", + name: "platformReferrer", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "orderReferrer", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "protocolFeeRecipient", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "tokenCreatorFee", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "platformReferrerFee", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "orderReferrerFee", + type: "uint256", + }, + { indexed: false, internalType: "uint256", name: "protocolFee", type: "uint256" }, + ], + name: "WowTokenFees", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "seller", type: "address" }, + { indexed: true, internalType: "address", name: "recipient", type: "address" }, + { + indexed: true, + internalType: "address", + name: "orderReferrer", + type: "address", + }, + { indexed: false, internalType: "uint256", name: "totalEth", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "ethFee", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "ethBought", type: "uint256" }, + { indexed: false, internalType: "uint256", name: "tokensSold", type: "uint256" }, + { + indexed: false, + internalType: "uint256", + name: "sellerTokenBalance", + type: "uint256", + }, + { indexed: false, internalType: "string", name: "comment", type: "string" }, + { + indexed: false, + internalType: "enum IWow.MarketType", + name: "marketType", + type: "uint8", + }, + ], + name: "WowTokenSell", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "from", type: "address" }, + { indexed: true, internalType: "address", name: "to", type: "address" }, + { indexed: false, internalType: "uint256", name: "amount", type: "uint256" }, + { + indexed: false, + internalType: "uint256", + name: "fromTokenBalance", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "toTokenBalance", + type: "uint256", + }, + { indexed: false, internalType: "uint256", name: "totalSupply", type: "uint256" }, + ], + name: "WowTokenTransfer", + type: "event", + }, + { + inputs: [], + name: "MAX_TOTAL_SUPPLY", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "MIN_ORDER_SIZE", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "ORDER_REFERRER_FEE_BPS", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "PLATFORM_REFERRER_FEE_BPS", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "PROTOCOL_FEE_BPS", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "TOKEN_CREATOR_FEE_BPS", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "TOTAL_FEE_BPS", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "WETH", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "owner", type: "address" }, + { internalType: "address", name: "spender", type: "address" }, + ], + name: "allowance", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "spender", type: "address" }, + { internalType: "uint256", name: "value", type: "uint256" }, + ], + name: "approve", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "account", type: "address" }], + name: "balanceOf", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "bondingCurve", + outputs: [{ internalType: "contract BondingCurve", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "tokensToBurn", type: "uint256" }], + name: "burn", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "recipient", type: "address" }, + { internalType: "address", name: "refundRecipient", type: "address" }, + { internalType: "address", name: "orderReferrer", type: "address" }, + { internalType: "string", name: "comment", type: "string" }, + { internalType: "enum IWow.MarketType", name: "expectedMarketType", type: "uint8" }, + { internalType: "uint256", name: "minOrderSize", type: "uint256" }, + { internalType: "uint160", name: "sqrtPriceLimitX96", type: "uint160" }, + ], + name: "buy", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "payable", + type: "function", + }, + { + inputs: [], + name: "currentExchangeRate", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "decimals", + outputs: [{ internalType: "uint8", name: "", type: "uint8" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "ethOrderSize", type: "uint256" }], + name: "getEthBuyQuote", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "ethOrderSize", type: "uint256" }], + name: "getEthSellQuote", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "tokenOrderSize", type: "uint256" }], + name: "getTokenBuyQuote", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "tokenOrderSize", type: "uint256" }], + name: "getTokenSellQuote", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "_tokenCreator", type: "address" }, + { internalType: "address", name: "_platformReferrer", type: "address" }, + { internalType: "address", name: "_bondingCurve", type: "address" }, + { internalType: "string", name: "_tokenURI", type: "string" }, + { internalType: "string", name: "_name", type: "string" }, + { internalType: "string", name: "_symbol", type: "string" }, + ], + name: "initialize", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [], + name: "marketType", + outputs: [{ internalType: "enum IWow.MarketType", name: "", type: "uint8" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "name", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "nonfungiblePositionManager", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "", type: "address" }, + { internalType: "address", name: "", type: "address" }, + { internalType: "uint256", name: "", type: "uint256" }, + { internalType: "bytes", name: "", type: "bytes" }, + ], + name: "onERC721Received", + outputs: [{ internalType: "bytes4", name: "", type: "bytes4" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "platformReferrer", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "poolAddress", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "protocolFeeRecipient", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "protocolRewards", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "tokensToSell", type: "uint256" }, + { internalType: "address", name: "recipient", type: "address" }, + { internalType: "address", name: "orderReferrer", type: "address" }, + { internalType: "string", name: "comment", type: "string" }, + { internalType: "enum IWow.MarketType", name: "expectedMarketType", type: "uint8" }, + { internalType: "uint256", name: "minPayoutSize", type: "uint256" }, + { internalType: "uint160", name: "sqrtPriceLimitX96", type: "uint160" }, + ], + name: "sell", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "state", + outputs: [ + { + components: [ + { internalType: "enum IWow.MarketType", name: "marketType", type: "uint8" }, + { internalType: "address", name: "marketAddress", type: "address" }, + ], + internalType: "struct IWow.MarketState", + name: "", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "swapRouter", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "symbol", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "tokenCreator", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "tokenURI", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "totalSupply", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "value", type: "uint256" }, + ], + name: "transfer", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "from", type: "address" }, + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "value", type: "uint256" }, + ], + name: "transferFrom", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "int256", name: "amount0Delta", type: "int256" }, + { internalType: "int256", name: "amount1Delta", type: "int256" }, + { internalType: "bytes", name: "", type: "bytes" }, + ], + name: "uniswapV3SwapCallback", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { stateMutability: "payable", type: "receive" }, +] as const; + +export const WOW_FACTORY_CONTRACT_ADDRESSES: Record = { + "base-sepolia": "0x04870e22fa217Cb16aa00501D7D5253B8838C1eA", + "base-mainnet": "0x997020E5F59cCB79C74D527Be492Cc610CB9fA2B", +}; + +export const ADDRESSES: Record> = { + "base-sepolia": { + WowFactory: "0xB09c0b1b18369Ef62e896D5a49Af8d65EFa0A404", + WowFactoryImpl: "0xB522291f22FE7FA45D56797F7A685D5c637Edc32", + Wow: "0x15ba66e376856F3F6FE53dE9eeAb10dEF10E8C92", + BondingCurve: "0xCE00c75B9807A2aA87B2297cA7Dc1C0190137D6F", + NonfungiblePositionManager: "0x27F971cb582BF9E50F397e4d29a5C7A34f11faA2", + SwapRouter02: "0x94cC0AaC535CCDB3C01d6787D6413C739ae12bc4", + WETH: "0x4200000000000000000000000000000000000006", + UniswapQuoter: "0xC5290058841028F1614F3A6F0F5816cAd0df5E27", + }, + "base-mainnet": { + WowFactory: "0xA06262157905913f855573f53AD48DE2D4ba1F4A", + WowFactoryImpl: "0xe4c17055048aEe01D0d122804816fEe5E6ac4A67", + Wow: "0x293997C6a1f2A1cA3aB971f548c4D95585E46282", + BondingCurve: "0x264ece5D58A576cc775B719bf182F2946076bE78", + NonfungiblePositionManager: "0x03a520b32C04BF3bEEf7BEb72E919cf822Ed34f1", + SwapRouter02: "0x2626664c2603336E57B271c5C0b26F421741e481", + WETH: "0x4200000000000000000000000000000000000006", + UniswapQuoter: "0x3d4e44Eb1374240CE5F1B871ab261CD16335B76a", + }, +}; + +export const GENERIC_TOKEN_METADATA_URI = "ipfs://QmY1GqprFYvojCcUEKgqHeDj9uhZD9jmYGrQTfA9vAE78J"; + +/** + * Gets the Zora Wow ERC20 Factory contract address for the specified network. + * + * @param network - The network ID to get the contract address for + * @returns The contract address for the specified network + * @throws Error if the specified network is not supported + */ +export function getFactoryAddress(network: string): string { + const normalizedNetwork = network.toLowerCase(); + if (!(normalizedNetwork in WOW_FACTORY_CONTRACT_ADDRESSES)) { + throw new Error( + `Invalid network: ${network}. Valid networks are: ${Object.keys( + WOW_FACTORY_CONTRACT_ADDRESSES, + ).join(", ")}`, + ); + } + return WOW_FACTORY_CONTRACT_ADDRESSES[normalizedNetwork]; +} diff --git a/cdp-agentkit-core/typescript/src/action_providers/wow/index.ts b/cdp-agentkit-core/typescript/src/action_providers/wow/index.ts new file mode 100644 index 000000000..796932b35 --- /dev/null +++ b/cdp-agentkit-core/typescript/src/action_providers/wow/index.ts @@ -0,0 +1,2 @@ +export * from "./schemas"; +export * from "./wowActionProvider"; diff --git a/cdp-agentkit-core/typescript/src/action_providers/wow/schemas.ts b/cdp-agentkit-core/typescript/src/action_providers/wow/schemas.ts new file mode 100644 index 000000000..4a0196c07 --- /dev/null +++ b/cdp-agentkit-core/typescript/src/action_providers/wow/schemas.ts @@ -0,0 +1,57 @@ +import { z } from "zod"; +import { isAddress } from "viem"; + +const ethereumAddress = z.custom<`0x${string}`>( + val => typeof val === "string" && isAddress(val), + "Invalid address", +); + +/** + * Input schema for buying WOW tokens. + */ +export const WowBuyTokenInput = z + .object({ + contractAddress: ethereumAddress.describe("The WOW token contract address"), + amountEthInWei: z + .string() + .regex(/^\d+$/, "Must be a valid wei amount") + .describe("Amount of ETH to spend (in wei)"), + }) + .strip() + .describe("Instructions for buying WOW tokens"); + +/** + * Input schema for creating WOW tokens. + */ +export const WowCreateTokenInput = z + .object({ + name: z.string().min(1).describe("The name of the token to create, e.g. WowCoin"), + symbol: z.string().min(1).describe("The symbol of the token to create, e.g. WOW"), + tokenUri: z + .string() + .url() + .optional() + .describe( + "The URI of the token metadata to store on IPFS, e.g. ipfs://QmY1GqprFYvojCcUEKgqHeDj9uhZD9jmYGrQTfA9vAE78J", + ), + }) + .strip() + .describe("Instructions for creating a WOW token"); + +/** + * Input schema for selling WOW tokens. + */ +export const WowSellTokenInput = z + .object({ + contractAddress: ethereumAddress.describe( + "The WOW token contract address, such as `0x036CbD53842c5426634e7929541eC2318f3dCF7e`", + ), + amountTokensInWei: z + .string() + .regex(/^\d+$/, "Must be a valid wei amount") + .describe( + "Amount of tokens to sell (in wei), meaning 1 is 1 wei or 0.000000000000000001 of the token", + ), + }) + .strip() + .describe("Instructions for selling WOW tokens"); diff --git a/cdp-agentkit-core/typescript/src/action_providers/wow/uniswap/constants.ts b/cdp-agentkit-core/typescript/src/action_providers/wow/uniswap/constants.ts new file mode 100644 index 000000000..9c9eb0ff9 --- /dev/null +++ b/cdp-agentkit-core/typescript/src/action_providers/wow/uniswap/constants.ts @@ -0,0 +1,100 @@ +import type { Abi } from "abitype"; + +export const UNISWAP_QUOTER_ABI: Abi = [ + { + inputs: [ + { + components: [ + { internalType: "address", name: "tokenIn", type: "address" }, + { internalType: "address", name: "tokenOut", type: "address" }, + { internalType: "uint256", name: "amountIn", type: "uint256" }, + { internalType: "uint24", name: "fee", type: "uint24" }, + { internalType: "uint160", name: "sqrtPriceLimitX96", type: "uint160" }, + ], + internalType: "struct IQuoterV2.QuoteExactInputSingleParams", + name: "params", + type: "tuple", + }, + ], + name: "quoteExactInputSingle", + outputs: [ + { internalType: "uint256", name: "amountOut", type: "uint256" }, + { internalType: "uint160", name: "sqrtPriceX96After", type: "uint160" }, + { internalType: "uint32", name: "initializedTicksCrossed", type: "uint32" }, + { internalType: "uint256", name: "gasEstimate", type: "uint256" }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { internalType: "address", name: "tokenIn", type: "address" }, + { internalType: "address", name: "tokenOut", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + { internalType: "uint24", name: "fee", type: "uint24" }, + { internalType: "uint160", name: "sqrtPriceLimitX96", type: "uint160" }, + ], + internalType: "struct IQuoterV2.QuoteExactOutputSingleParams", + name: "params", + type: "tuple", + }, + ], + name: "quoteExactOutputSingle", + outputs: [ + { internalType: "uint256", name: "amountIn", type: "uint256" }, + { internalType: "uint160", name: "sqrtPriceX96After", type: "uint160" }, + { internalType: "uint32", name: "initializedTicksCrossed", type: "uint32" }, + { internalType: "uint256", name: "gasEstimate", type: "uint256" }, + ], + stateMutability: "nonpayable", + type: "function", + }, +] as const; + +export const UNISWAP_V3_ABI: Abi = [ + { + inputs: [], + name: "fee", + outputs: [{ internalType: "uint24", name: "", type: "uint24" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "liquidity", + outputs: [{ internalType: "uint128", name: "", type: "uint128" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "slot0", + outputs: [ + { internalType: "uint160", name: "sqrtPriceX96", type: "uint160" }, + { internalType: "int24", name: "tick", type: "int24" }, + { internalType: "uint16", name: "observationIndex", type: "uint16" }, + { internalType: "uint16", name: "observationCardinality", type: "uint16" }, + { internalType: "uint16", name: "observationCardinalityNext", type: "uint16" }, + { internalType: "uint8", name: "feeProtocol", type: "uint8" }, + { internalType: "bool", name: "unlocked", type: "bool" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "token0", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "token1", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, +] as const; diff --git a/cdp-agentkit-core/typescript/src/action_providers/wow/uniswap/utils.ts b/cdp-agentkit-core/typescript/src/action_providers/wow/uniswap/utils.ts new file mode 100644 index 000000000..3966c0962 --- /dev/null +++ b/cdp-agentkit-core/typescript/src/action_providers/wow/uniswap/utils.ts @@ -0,0 +1,291 @@ +import { formatEther, getAddress } from "viem"; +import { EvmWalletProvider } from "../../../wallet_providers"; +import { ADDRESSES, WOW_ABI } from "../constants"; +import { UNISWAP_QUOTER_ABI, UNISWAP_V3_ABI } from "./constants"; + +export interface PriceInfo { + eth: string; + usd: number; +} + +export interface Balance { + erc20z: string; + weth: string; +} + +export interface Price { + perToken: PriceInfo; + total: PriceInfo; +} + +export interface Quote { + amountIn: number; + amountOut: number; + balance: Balance | null; + fee: number | null; + error: string | null; +} + +export interface PoolInfo { + token0: string; + balance0: number; + token1: string; + balance1: number; + fee: number; + liquidity: number; + sqrtPriceX96: number; +} + +/** + * Creates a PriceInfo object from wei amount and ETH price. + * + * @param weiAmount - Amount in wei + * @param ethPriceInUsd - Current ETH price in USD + * @returns A PriceInfo object containing the amount in ETH and USD + */ +export function createPriceInfo(weiAmount: string, ethPriceInUsd: number): PriceInfo { + const amountInEth = formatEther(BigInt(weiAmount)); + const usd = Number(amountInEth) * ethPriceInUsd; + return { + eth: weiAmount, + usd, + }; +} + +/** + * Gets pool info for a given uniswap v3 pool address. + * + * @param wallet - The wallet provider to use for contract calls + * @param poolAddress - Uniswap v3 pool address + * @returns A PoolInfo object containing pool details + */ +export async function getPoolInfo( + wallet: EvmWalletProvider, + poolAddress: string, +): Promise { + try { + const results = await Promise.all([ + wallet.readContract({ + address: poolAddress as `0x${string}`, + functionName: "token0", + args: [], + abi: UNISWAP_V3_ABI, + }), + wallet.readContract({ + address: poolAddress as `0x${string}`, + functionName: "token1", + args: [], + abi: UNISWAP_V3_ABI, + }), + wallet.readContract({ + address: poolAddress as `0x${string}`, + functionName: "fee", + args: [], + abi: UNISWAP_V3_ABI, + }), + wallet.readContract({ + address: poolAddress as `0x${string}`, + functionName: "liquidity", + args: [], + abi: UNISWAP_V3_ABI, + }), + wallet.readContract({ + address: poolAddress as `0x${string}`, + functionName: "slot0", + args: [], + abi: UNISWAP_V3_ABI, + }), + ]); + + const [token0Result, token1Result, fee, liquidity, slot0] = results; + + const [balance0, balance1] = await Promise.all([ + wallet.readContract({ + address: token0Result as `0x${string}`, + functionName: "balanceOf", + args: [poolAddress], + abi: WOW_ABI, + }), + wallet.readContract({ + address: token1Result as `0x${string}`, + functionName: "balanceOf", + args: [poolAddress], + abi: WOW_ABI, + }), + ]); + + return { + token0: token0Result as string, + balance0: Number(balance0), + token1: token1Result as string, + balance1: Number(balance1), + fee: Number(fee), + liquidity: Number(liquidity), + sqrtPriceX96: Number((slot0 as unknown[])[0]), + }; + } catch (error) { + throw new Error(`Failed to fetch pool information: ${error}`); + } +} + +/** + * Gets exact input quote from Uniswap. + * + * @param wallet - The wallet provider to use for contract calls + * @param tokenIn - Token address to swap from + * @param tokenOut - Token address to swap to + * @param amountIn - Amount of tokens to swap (in Wei) + * @param fee - Fee for the swap + * @returns Amount of tokens to receive (in Wei) + */ +export async function exactInputSingle( + wallet: EvmWalletProvider, + tokenIn: string, + tokenOut: string, + amountIn: string, + fee: string, +): Promise { + try { + const networkId = wallet.getNetwork().networkId!; + const amount = await wallet.readContract({ + address: ADDRESSES[networkId].UniswapQuoter as `0x${string}`, + functionName: "quoteExactInputSingle", + args: [ + { + tokenIn: getAddress(tokenIn), + tokenOut: getAddress(tokenOut), + fee, + amountIn, + sqrtPriceLimitX96: 0, + }, + ], + abi: UNISWAP_QUOTER_ABI, + }); + + return Number(amount); + } catch (error) { + console.error("Quoter error:", error); + return 0; + } +} + +/** + * Gets Uniswap quote for buying or selling tokens. + * + * @param wallet - The wallet provider to use for contract calls + * @param tokenAddress - Token address + * @param amount - Amount of tokens (in Wei) + * @param quoteType - 'buy' or 'sell' + * @returns A Quote object containing quote details + */ +export async function getUniswapQuote( + wallet: EvmWalletProvider, + tokenAddress: string, + amount: number, + quoteType: "buy" | "sell", +): Promise { + let pool: PoolInfo | null = null; + let tokens: [string, string] | null = null; + let balances: [number, number] | null = null; + let quoteResult: number | null = null; + const utilization = 0; + const networkId = wallet.getNetwork().networkId!; + + const poolAddress = await getPoolAddress(wallet, tokenAddress); + const invalidPoolError = !poolAddress ? "Invalid pool address" : null; + + try { + pool = await getPoolInfo(wallet, poolAddress); + const { token0, token1, balance0, balance1, fee } = pool; + tokens = [token0, token1]; + balances = [balance0, balance1]; + + const isToken0Weth = token0.toLowerCase() === ADDRESSES[networkId].WETH.toLowerCase(); + const tokenIn = + (quoteType === "buy" && isToken0Weth) || (quoteType === "sell" && !isToken0Weth) + ? token0 + : token1; + + const [tokenOut, balanceOut] = tokenIn === token0 ? [token1, balance1] : [token0, balance0]; + const isInsufficientLiquidity = quoteType === "buy" && amount > balanceOut; + + if (!isInsufficientLiquidity) { + quoteResult = await exactInputSingle(wallet, tokenIn, tokenOut, String(amount), String(fee)); + } + } catch (error) { + console.error("Error fetching quote:", error); + } + + const insufficientLiquidity = (quoteType === "sell" && pool && !quoteResult) || false; + + let errorMsg: string | null = null; + if (!pool) { + errorMsg = "Failed fetching pool"; + } else if (insufficientLiquidity) { + errorMsg = "Insufficient liquidity"; + } else if (!quoteResult && utilization >= 0.9) { + errorMsg = "Price impact too high"; + } else if (!quoteResult) { + errorMsg = "Failed fetching quote"; + } + + const balanceResult = + tokens && balances + ? { + erc20z: String( + balances[tokens[0].toLowerCase() === ADDRESSES[networkId].WETH.toLowerCase() ? 1 : 0], + ), + weth: String( + balances[tokens[0].toLowerCase() === ADDRESSES[networkId].WETH.toLowerCase() ? 0 : 1], + ), + } + : null; + + return { + amountIn: Number(amount), + amountOut: quoteResult || 0, + balance: balanceResult, + fee: pool?.fee ? pool.fee / 1000000 : null, + error: invalidPoolError || errorMsg, + }; +} + +/** + * Checks if a token has graduated from the Zora Wow protocol. + * + * @param wallet - The wallet provider to use for contract calls + * @param tokenAddress - Token address + * @returns True if the token has graduated, false otherwise + */ +export async function getHasGraduated( + wallet: EvmWalletProvider, + tokenAddress: string, +): Promise { + const marketType = await wallet.readContract({ + address: tokenAddress as `0x${string}`, + functionName: "marketType", + args: [], + abi: WOW_ABI, + }); + return marketType === 1; +} + +/** + * Fetches the uniswap v3 pool address for a given token. + * + * @param wallet - The wallet provider to use for contract calls + * @param tokenAddress - The address of the token contract + * @returns The uniswap v3 pool address associated with the token + */ +export async function getPoolAddress( + wallet: EvmWalletProvider, + tokenAddress: string, +): Promise { + const poolAddress = await wallet.readContract({ + address: tokenAddress as `0x${string}`, + functionName: "poolAddress", + args: [], + abi: WOW_ABI, + }); + return poolAddress as string; +} diff --git a/cdp-agentkit-core/typescript/src/action_providers/wow/utils.ts b/cdp-agentkit-core/typescript/src/action_providers/wow/utils.ts new file mode 100644 index 000000000..3fd282515 --- /dev/null +++ b/cdp-agentkit-core/typescript/src/action_providers/wow/utils.ts @@ -0,0 +1,82 @@ +import { EvmWalletProvider } from "../../wallet_providers"; +import { WOW_ABI } from "./constants"; +import { getHasGraduated, getUniswapQuote } from "./uniswap/utils"; + +/** + * Gets the current supply of a token. + * + * @param wallet - The wallet provider to use for contract calls + * @param tokenAddress - Address of the token contract + * @returns The current token supply + */ +export async function getCurrentSupply( + wallet: EvmWalletProvider, + tokenAddress: string, +): Promise { + const supply = await wallet.readContract({ + address: tokenAddress as `0x${string}`, + abi: WOW_ABI, + functionName: "totalSupply", + args: [], + }); + + return supply as string; +} + +/** + * Gets quote for buying tokens. + * + * @param wallet - The wallet provider to use for contract calls + * @param tokenAddress - Address of the token contract + * @param amountEthInWei - Amount of ETH to buy (in wei) + * @returns The buy quote amount + */ +export async function getBuyQuote( + wallet: EvmWalletProvider, + tokenAddress: string, + amountEthInWei: string, +): Promise { + const hasGraduated = await getHasGraduated(wallet, tokenAddress); + + const tokenQuote = ( + hasGraduated + ? (await getUniswapQuote(wallet, tokenAddress, Number(amountEthInWei), "buy")).amountOut + : await wallet.readContract({ + address: tokenAddress as `0x${string}`, + abi: WOW_ABI, + functionName: "getEthBuyQuote", + args: [amountEthInWei], + }) + ) as string | number; + + return tokenQuote.toString(); +} + +/** + * Gets quote for selling tokens. + * + * @param wallet - The wallet provider to use for contract calls + * @param tokenAddress - Address of the token contract + * @param amountTokensInWei - Amount of tokens to sell (in wei) + * @returns The sell quote amount + */ +export async function getSellQuote( + wallet: EvmWalletProvider, + tokenAddress: string, + amountTokensInWei: string, +): Promise { + const hasGraduated = await getHasGraduated(wallet, tokenAddress); + + const tokenQuote = ( + hasGraduated + ? (await getUniswapQuote(wallet, tokenAddress, Number(amountTokensInWei), "sell")).amountOut + : await wallet.readContract({ + address: tokenAddress as `0x${string}`, + abi: WOW_ABI, + functionName: "getTokenSellQuote", + args: [amountTokensInWei], + }) + ) as string | number; + + return tokenQuote.toString(); +} diff --git a/cdp-agentkit-core/typescript/src/action_providers/wow/wowActionProvider.test.ts b/cdp-agentkit-core/typescript/src/action_providers/wow/wowActionProvider.test.ts new file mode 100644 index 000000000..ccb9ccaad --- /dev/null +++ b/cdp-agentkit-core/typescript/src/action_providers/wow/wowActionProvider.test.ts @@ -0,0 +1,337 @@ +import { encodeFunctionData } from "viem"; +import { EvmWalletProvider } from "../../wallet_providers"; +import { WowActionProvider } from "./wowActionProvider"; +import { WOW_ABI, WOW_FACTORY_ABI, GENERIC_TOKEN_METADATA_URI } from "./constants"; +import { getBuyQuote, getSellQuote } from "./utils"; +import { getHasGraduated } from "./uniswap/utils"; +import { WowBuyTokenInput, WowCreateTokenInput, WowSellTokenInput } from "./schemas"; + +jest.mock("./utils", () => ({ + getBuyQuote: jest.fn(), + getSellQuote: jest.fn(), +})); + +jest.mock("./uniswap/utils", () => ({ + getHasGraduated: jest.fn(), +})); + +describe("WowActionProvider", () => { + const MOCK_CONTRACT_ADDRESS = "0x1234567890123456789012345678901234567890" as `0x${string}`; + const INVALID_ADDRESS = "0xinvalid" as `0x${string}`; + const MOCK_AMOUNT_ETH_IN_WEI = BigInt("100000000000000000"); + const INVALID_WEI = "1.5"; // Wei amounts can't have decimals + const MOCK_AMOUNT_TOKENS_IN_WEI = BigInt("1000000000000000000"); + const MOCK_NAME = "Test Token"; + const MOCK_SYMBOL = "TEST"; + const MOCK_URI = "ipfs://QmY1GqprFYvojCcUEKgqHeDj9uhZD9jmYGrQTfA9vAE78J"; + const INVALID_URI = "not-a-url"; + const MOCK_TX_HASH = "0xabcdef1234567890"; + const MOCK_ADDRESS = "0x9876543210987654321098765432109876543210" as `0x${string}`; + + let provider: WowActionProvider; + let mockWallet: jest.Mocked; + + beforeEach(() => { + mockWallet = { + getAddress: jest.fn().mockReturnValue(MOCK_ADDRESS), + getNetwork: jest.fn().mockReturnValue({ protocolFamily: "evm", networkId: "base-sepolia" }), + sendTransaction: jest.fn().mockResolvedValue(MOCK_TX_HASH as `0x${string}`), + waitForTransactionReceipt: jest.fn().mockResolvedValue({}), + readContract: jest.fn(), + } as unknown as jest.Mocked; + + provider = new WowActionProvider(); + + (getBuyQuote as jest.Mock).mockResolvedValue("1000000000000000000"); + (getSellQuote as jest.Mock).mockResolvedValue("1000000000000000000"); + (getHasGraduated as jest.Mock).mockResolvedValue(true); + }); + + describe("Input Validation", () => { + describe("buyToken", () => { + it("should validate Ethereum addresses", () => { + const result = WowBuyTokenInput.safeParse({ + contractAddress: INVALID_ADDRESS, + amountEthInWei: MOCK_AMOUNT_ETH_IN_WEI.toString(), + }); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain("Invalid address"); + } + }); + + it("should validate wei amount format", () => { + const result = WowBuyTokenInput.safeParse({ + contractAddress: MOCK_CONTRACT_ADDRESS, + amountEthInWei: INVALID_WEI, + }); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toBe("Must be a valid wei amount"); + } + }); + + it("should accept valid input", () => { + const result = WowBuyTokenInput.safeParse({ + contractAddress: MOCK_CONTRACT_ADDRESS, + amountEthInWei: MOCK_AMOUNT_ETH_IN_WEI.toString(), + }); + expect(result.success).toBe(true); + }); + }); + + describe("createToken", () => { + it("should require non-empty name", () => { + const result = WowCreateTokenInput.safeParse({ + name: "", + symbol: MOCK_SYMBOL, + }); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].code).toBe("too_small"); + } + }); + + it("should require non-empty symbol", () => { + const result = WowCreateTokenInput.safeParse({ + name: MOCK_NAME, + symbol: "", + }); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].code).toBe("too_small"); + } + }); + + it("should validate tokenUri format if provided", () => { + const result = WowCreateTokenInput.safeParse({ + name: MOCK_NAME, + symbol: MOCK_SYMBOL, + tokenUri: INVALID_URI, + }); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].code).toBe("invalid_string"); + } + }); + + it("should accept valid input with proper URL", () => { + const result = WowCreateTokenInput.safeParse({ + name: MOCK_NAME, + symbol: MOCK_SYMBOL, + tokenUri: MOCK_URI, + }); + expect(result.success).toBe(true); + }); + }); + + describe("sellToken", () => { + it("should validate Ethereum addresses", () => { + const result = WowSellTokenInput.safeParse({ + contractAddress: INVALID_ADDRESS, + amountTokensInWei: MOCK_AMOUNT_TOKENS_IN_WEI.toString(), + }); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toContain("Invalid address"); + } + }); + + it("should validate wei amount format", () => { + const result = WowSellTokenInput.safeParse({ + contractAddress: MOCK_CONTRACT_ADDRESS, + amountTokensInWei: INVALID_WEI, + }); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toBe("Must be a valid wei amount"); + } + }); + + it("should accept valid input", () => { + const result = WowSellTokenInput.safeParse({ + contractAddress: MOCK_CONTRACT_ADDRESS, + amountTokensInWei: MOCK_AMOUNT_TOKENS_IN_WEI.toString(), + }); + expect(result.success).toBe(true); + }); + }); + }); + + describe("supportsNetwork", () => { + it("should return true for supported networks", () => { + expect(provider.supportsNetwork({ protocolFamily: "evm", networkId: "base-mainnet" })).toBe( + true, + ); + }); + + it("should return false for unsupported networks", () => { + expect(provider.supportsNetwork({ protocolFamily: "evm", networkId: "base-sepolia" })).toBe( + false, + ); + expect(provider.supportsNetwork({ protocolFamily: "evm", networkId: "ethereum" })).toBe( + false, + ); + expect( + provider.supportsNetwork({ protocolFamily: "bitcoin", networkId: "base-mainnet" }), + ).toBe(false); + }); + }); + + describe("buyToken", () => { + it("should successfully buy tokens", async () => { + const args = { + contractAddress: MOCK_CONTRACT_ADDRESS, + amountEthInWei: MOCK_AMOUNT_ETH_IN_WEI.toString(), + }; + + const response = await provider.buyToken(mockWallet, args); + + expect(mockWallet.sendTransaction).toHaveBeenCalledWith({ + to: MOCK_CONTRACT_ADDRESS, + data: encodeFunctionData({ + abi: WOW_ABI, + functionName: "buy", + args: [ + MOCK_ADDRESS, + MOCK_ADDRESS, + "0x0000000000000000000000000000000000000000", + "", + 1n, + BigInt(Math.floor(Number("1000000000000000000") * 99)) / BigInt(100), + 0n, + ], + }), + value: MOCK_AMOUNT_ETH_IN_WEI, + }); + + expect(response).toContain("Purchased WoW ERC20 memecoin"); + expect(response).toContain(MOCK_TX_HASH); + }); + + it("should handle buy errors", async () => { + const args = { + contractAddress: MOCK_CONTRACT_ADDRESS, + amountEthInWei: MOCK_AMOUNT_ETH_IN_WEI.toString(), + }; + + const error = new Error("Buy failed"); + mockWallet.sendTransaction.mockRejectedValue(error); + + const response = await provider.buyToken(mockWallet, args); + expect(response).toContain(`Error buying Zora Wow ERC20 memecoin: ${error}`); + }); + }); + + describe("createToken", () => { + it("should successfully create a token", async () => { + const args = { + name: MOCK_NAME, + symbol: MOCK_SYMBOL, + tokenUri: MOCK_URI, + }; + + const response = await provider.createToken(mockWallet, args); + + expect(mockWallet.sendTransaction).toHaveBeenCalledWith({ + to: expect.any(String), + data: encodeFunctionData({ + abi: WOW_FACTORY_ABI, + functionName: "deploy", + args: [ + MOCK_ADDRESS, + "0x0000000000000000000000000000000000000000", + MOCK_URI, + MOCK_NAME, + MOCK_SYMBOL, + ], + }), + }); + + expect(response).toContain(`Created WoW ERC20 memecoin ${MOCK_NAME}`); + expect(response).toContain(`with symbol ${MOCK_SYMBOL}`); + expect(response).toContain(MOCK_TX_HASH); + }); + + it("should use default token URI if not provided", async () => { + const args = { + name: MOCK_NAME, + symbol: MOCK_SYMBOL, + }; + + await provider.createToken(mockWallet, args); + + expect(mockWallet.sendTransaction).toHaveBeenCalledWith({ + to: expect.any(String), + data: encodeFunctionData({ + abi: WOW_FACTORY_ABI, + functionName: "deploy", + args: [ + MOCK_ADDRESS, + "0x0000000000000000000000000000000000000000", + GENERIC_TOKEN_METADATA_URI, + MOCK_NAME, + MOCK_SYMBOL, + ], + }), + }); + }); + + it("should handle create errors", async () => { + const args = { + name: MOCK_NAME, + symbol: MOCK_SYMBOL, + }; + + const error = new Error("Create failed"); + mockWallet.sendTransaction.mockRejectedValue(error); + + const response = await provider.createToken(mockWallet, args); + expect(response).toContain(`Error creating Zora Wow ERC20 memecoin: ${error}`); + }); + }); + + describe("sellToken", () => { + it("should successfully sell tokens", async () => { + const args = { + contractAddress: MOCK_CONTRACT_ADDRESS, + amountTokensInWei: MOCK_AMOUNT_TOKENS_IN_WEI.toString(), + }; + + const response = await provider.sellToken(mockWallet, args); + + expect(mockWallet.sendTransaction).toHaveBeenCalledWith({ + to: MOCK_CONTRACT_ADDRESS, + data: encodeFunctionData({ + abi: WOW_ABI, + functionName: "sell", + args: [ + MOCK_AMOUNT_TOKENS_IN_WEI, + MOCK_ADDRESS, + "0x0000000000000000000000000000000000000000", + "", + 1n, + BigInt(Math.floor(Number("1000000000000000000") * 98)) / BigInt(100), + 0n, + ], + }), + }); + + expect(response).toContain("Sold WoW ERC20 memecoin"); + expect(response).toContain(MOCK_TX_HASH); + }); + + it("should handle sell errors", async () => { + const args = { + contractAddress: MOCK_CONTRACT_ADDRESS, + amountTokensInWei: MOCK_AMOUNT_TOKENS_IN_WEI.toString(), + }; + + const error = new Error("Sell failed"); + mockWallet.sendTransaction.mockRejectedValue(error); + + const response = await provider.sellToken(mockWallet, args); + expect(response).toContain(`Error selling Zora Wow ERC20 memecoin: ${error}`); + }); + }); +}); diff --git a/cdp-agentkit-core/typescript/src/action_providers/wow/wowActionProvider.ts b/cdp-agentkit-core/typescript/src/action_providers/wow/wowActionProvider.ts new file mode 100644 index 000000000..3f767ab33 --- /dev/null +++ b/cdp-agentkit-core/typescript/src/action_providers/wow/wowActionProvider.ts @@ -0,0 +1,230 @@ +import { z } from "zod"; +import { ActionProvider } from "../action_provider"; +import { Network, EvmWalletProvider } from "../../wallet_providers"; +import { CreateAction } from "../action_decorator"; +import { + SUPPORTED_NETWORKS, + WOW_ABI, + WOW_FACTORY_ABI, + GENERIC_TOKEN_METADATA_URI, + getFactoryAddress, +} from "./constants"; +import { getBuyQuote, getSellQuote } from "./utils"; +import { getHasGraduated } from "./uniswap/utils"; +import { encodeFunctionData } from "viem"; +import { WowBuyTokenInput, WowCreateTokenInput, WowSellTokenInput } from "./schemas"; + +/** + * WowActionProvider is an action provider for Wow protocol interactions. + */ +export class WowActionProvider extends ActionProvider { + /** + * Constructor for the WowActionProvider class. + */ + constructor() { + super("wow", []); + } + + /** + * Buys a Zora Wow ERC20 memecoin with ETH. + * + * @param wallet - The wallet to create the token from. + * @param args - The input arguments for the action. + * @returns A message containing the token purchase details. + */ + @CreateAction({ + name: "buy_token", + description: ` +This tool can only be used to buy a Zora Wow ERC20 memecoin (also can be referred to as a bonding curve token) with ETH. +Do not use this tool for any other purpose, or trading other assets. + +Inputs: +- WOW token contract address +- Address to receive the tokens +- Amount of ETH to spend (in wei) + +Important notes: +- The amount is a string and cannot have any decimal points, since the unit of measurement is wei. +- Make sure to use the exact amount provided, and if there's any doubt, check by getting more information before continuing with the action. +- 1 wei = 0.000000000000000001 ETH +- Minimum purchase amount is 100000000000000 wei (0.0000001 ETH) +- Only supported on the following networks: + - Base Sepolia (ie, 'base-sepolia') + - Base Mainnet (ie, 'base', 'base-mainnet')`, + schema: WowBuyTokenInput, + }) + async buyToken( + wallet: EvmWalletProvider, + args: z.infer, + ): Promise { + try { + const tokenQuote = await getBuyQuote(wallet, args.contractAddress, args.amountEthInWei); + + // Multiply by 99/100 and floor to get 99% of quote as minimum + const minTokens = BigInt(Math.floor(Number(tokenQuote) * 99)) / BigInt(100); + + const hasGraduated = await getHasGraduated(wallet, args.contractAddress); + + const data = encodeFunctionData({ + abi: WOW_ABI, + functionName: "buy", + args: [ + wallet.getAddress(), + wallet.getAddress(), + "0x0000000000000000000000000000000000000000", + "", + hasGraduated ? 1n : 0n, + minTokens, + 0n, + ], + }); + + const txHash = await wallet.sendTransaction({ + to: args.contractAddress as `0x${string}`, + data, + value: BigInt(args.amountEthInWei), + }); + + const receipt = await wallet.waitForTransactionReceipt(txHash); + + return `Purchased WoW ERC20 memecoin with transaction hash: ${txHash}, and receipt:\n${JSON.stringify(receipt)}`; + } catch (error) { + return `Error buying Zora Wow ERC20 memecoin: ${error}`; + } + } + + /** + * Creates a Zora Wow ERC20 memecoin. + * + * @param wallet - The wallet to create the token from. + * @param args - The input arguments for the action. + * @returns A message containing the token creation details. + */ + @CreateAction({ + name: "create_token", + description: ` +This tool can only be used to create a Zora Wow ERC20 memecoin (also can be referred to as a bonding curve token) using the WoW factory. +Do not use this tool for any other purpose, or for creating other types of tokens. + +Inputs: +- Token name (e.g. WowCoin) +- Token symbol (e.g. WOW) +- Token URI (optional) - Contains metadata about the token + +Important notes: +- Uses a bonding curve - no upfront liquidity needed +- Only supported on the following networks: + - Base Sepolia (ie, 'base-sepolia') + - Base Mainnet (ie, 'base', 'base-mainnet')`, + schema: WowCreateTokenInput, + }) + async createToken( + wallet: EvmWalletProvider, + args: z.infer, + ): Promise { + const factoryAddress = getFactoryAddress(wallet.getNetwork().networkId!); + + try { + const data = encodeFunctionData({ + abi: WOW_FACTORY_ABI, + functionName: "deploy", + args: [ + wallet.getAddress(), + "0x0000000000000000000000000000000000000000", + args.tokenUri || GENERIC_TOKEN_METADATA_URI, + args.name, + args.symbol, + ], + }); + + const txHash = await wallet.sendTransaction({ + to: factoryAddress as `0x${string}`, + data, + }); + + const receipt = await wallet.waitForTransactionReceipt(txHash); + + return `Created WoW ERC20 memecoin ${args.name} with symbol ${ + args.symbol + } on network ${wallet.getNetwork().networkId}.\nTransaction hash for the token creation: ${txHash}, and receipt:\n${JSON.stringify(receipt)}`; + } catch (error) { + return `Error creating Zora Wow ERC20 memecoin: ${error}`; + } + } + + /** + * Sells WOW tokens for ETH. + * + * @param wallet - The wallet to sell the tokens from. + * @param args - The input arguments for the action. + * @returns A message confirming the sale with the transaction hash. + */ + @CreateAction({ + name: "sell_token", + description: ` +This tool can only be used to sell a Zora Wow ERC20 memecoin (also can be referred to as a bonding curve token) for ETH. +Do not use this tool for any other purpose, or trading other assets. + +Inputs: +- WOW token contract address +- Amount of tokens to sell (in wei) + +Important notes: +- The amount is a string and cannot have any decimal points, since the unit of measurement is wei. +- Make sure to use the exact amount provided, and if there's any doubt, check by getting more information before continuing with the action. +- 1 wei = 0.000000000000000001 ETH +- Minimum purchase amount is 100000000000000 wei (0.0000001 ETH) +- Only supported on the following networks: + - Base Sepolia (ie, 'base-sepolia') + - Base Mainnet (ie, 'base', 'base-mainnet')`, + schema: WowSellTokenInput, + }) + async sellToken( + wallet: EvmWalletProvider, + args: z.infer, + ): Promise { + try { + const ethQuote = await getSellQuote(wallet, args.contractAddress, args.amountTokensInWei); + const hasGraduated = await getHasGraduated(wallet, args.contractAddress); + + // Multiply by 98/100 and floor to get 98% of quote as minimum + const minEth = BigInt(Math.floor(Number(ethQuote) * 98)) / BigInt(100); + + const data = encodeFunctionData({ + abi: WOW_ABI, + functionName: "sell", + args: [ + BigInt(args.amountTokensInWei), + wallet.getAddress(), + "0x0000000000000000000000000000000000000000", + "", + hasGraduated ? 1n : 0n, + minEth, + 0n, + ], + }); + + const txHash = await wallet.sendTransaction({ + to: args.contractAddress as `0x${string}`, + data, + }); + + const receipt = await wallet.waitForTransactionReceipt(txHash); + + return `Sold WoW ERC20 memecoin with transaction hash: ${txHash}, and receipt:\n${JSON.stringify(receipt)}`; + } catch (error) { + return `Error selling Zora Wow ERC20 memecoin: ${error}`; + } + } + + /** + * Checks if the Wow action provider supports the given network. + * + * @param network - The network to check. + * @returns True if the Wow action provider supports the network, false otherwise. + */ + supportsNetwork = (network: Network) => + network.protocolFamily === "evm" && SUPPORTED_NETWORKS.includes(network.networkId!); +} + +export const wowActionProvider = () => new WowActionProvider();