diff --git a/assets/_locales/en/messages.json b/assets/_locales/en/messages.json index 4a5b26b3c..492d26baf 100644 --- a/assets/_locales/en/messages.json +++ b/assets/_locales/en/messages.json @@ -4637,5 +4637,25 @@ "not_enough_ao_tokens": { "message": "Not enough $$AO tokens.", "description": "Not enough $AO tokens text" + }, + "agent_update_title": { + "message": "Update Agent", + "description": "Agent update title text" + }, + "agent_update_description": { + "message": "Update agent to latest version.", + "description": "Agent update description text" + }, + "update_agent": { + "message": "Update agent", + "description": "Update agent text" + }, + "success_updating_agent": { + "message": "Agent updated successfully", + "description": "Success updating agent text" + }, + "no_changes_to_save": { + "message": "No changes to save", + "description": "No changes to save text" } } diff --git a/assets/_locales/zh_CN/messages.json b/assets/_locales/zh_CN/messages.json index c74ac6748..943f38c41 100644 --- a/assets/_locales/zh_CN/messages.json +++ b/assets/_locales/zh_CN/messages.json @@ -4597,5 +4597,25 @@ "not_enough_ao_tokens": { "message": "没有足够的 $$AO 代币。", "description": "Not enough $AO tokens text" + }, + "agent_update_title": { + "message": "更新代理", + "description": "Agent update title text" + }, + "agent_update_description": { + "message": "更新代理至最新版本。", + "description": "Agent update description text" + }, + "update_agent": { + "message": "更新代理", + "description": "Update agent text" + }, + "success_updating_agent": { + "message": "更新代理成功", + "description": "Success updating agent text" + }, + "no_changes_to_save": { + "message": "没有需要保存的更改", + "description": "No changes to save text" } } diff --git a/assets/agents/contracts/ao-yield-agent.lua b/assets/agents/contracts/ao-yield-agent.lua index 535842945..947a1c72e 100644 --- a/assets/agents/contracts/ao-yield-agent.lua +++ b/assets/agents/contracts/ao-yield-agent.lua @@ -1 +1 @@ -local function _loaded_mod_src_ao_yield_agent_libs_constants()local mod={PERMASWAP_AO_WAR_POOL_ID="FRF1k0BSv0gRzNA2n-95_Fpz9gADq9BGi5PyXKFp6r8",PERMASWAP_AO_WUSDC_POOL_ID="gjnaCsEd749ZXeG2H8akvf8wzbl7CQ4Ox-KYEBAdONk",BOTEGA_AO_WAR_POOL_ID="B6qAwHi2OjZmyFCEU8hV6FZDSHbAOz8r0yy-fBbuTus",BOTEGA_AO_WUSDC_POOL_ID="TYqlQ2vqkF0H6nC0mCgGe6G12pqq9DsSXpvtHYc6_xY",BOTEGA_AMM_FACTORY_ID="3XBGLrygs11K63F_7mldWz4veNx6Llg6hI2yZs8LKHo",AO_PROCESS_ID="0syT13r0s0tgPmIed95bJnuSqaD29HQNN8D3ElLSrsc",WAR_PROCESS_ID="xU9zFkq3X2ZQ6olwNVvr1vUWIjc3kXTWr7xKQD6dh10",WUSDC_PROCESS_ID="7zH9dlMNoxprab9loshv3Y7WG45DOny_Vrq9KrXObdQ"}mod.PERMASWAP_POOL_IDS={[mod.WAR_PROCESS_ID]=mod.PERMASWAP_AO_WAR_POOL_ID,[mod.WUSDC_PROCESS_ID]=mod.PERMASWAP_AO_WUSDC_POOL_ID}mod.BOTEGA_POOL_IDS={[mod.WAR_PROCESS_ID]=mod.BOTEGA_AO_WAR_POOL_ID,[mod.WUSDC_PROCESS_ID]=mod.BOTEGA_AO_WUSDC_POOL_ID}return mod end;_G.package.loaded["src.ao_yield_agent.libs.constants"]=_loaded_mod_src_ao_yield_agent_libs_constants()local function _loaded_mod_src_commonlibs_aolibs()local mod={}local mod_json;local mod_bint;local jsonstatus,json=pcall(require,"json")if jsonstatus then mod_json=json else if not mod_json then print("Library 'json' does not exist. Using fallback dkjson.")end;local dkjsonstatus,dkjson=pcall(require,"dkjson")mod_json=dkjson end;local bintstatus,bint=pcall(require,".bint")if bintstatus then mod_bint=bint else if not mod_bint then print("Library '.bint' does not exist. Using fallback https://github.com/permaweb/aos/blob/main/process/bint.lua")end;local copiedbintstatus,copiedbint=pcall(require,"src.commonlibs.bint")mod_bint=copiedbint end;mod.bint=function(bits,word_bits)if not bits or type(bits)~="number"then bits=1024 end;local instance=mod_bint(bits,word_bits)return instance end;mod.json=mod_json;return mod end;_G.package.loaded["src.commonlibs.aolibs"]=_loaded_mod_src_commonlibs_aolibs()local function _loaded_mod_src_ao_yield_agent_libs_enums()local DexType={PERMASWAP="Permaswap",BOTEGA="Botega",Auto="Auto"}local AgentStatus={ACTIVE="Active",CANCELLED="Cancelled",COMPLETED="Completed"}local enums={DexType=DexType,AgentStatus=AgentStatus}return enums end;_G.package.loaded["src.ao_yield_agent.libs.enums"]=_loaded_mod_src_ao_yield_agent_libs_enums()local function _loaded_mod_src_ao_yield_agent_utils()local aolibs=require('src.commonlibs.aolibs')local enums=require('src.ao_yield_agent.libs.enums')local bint=aolibs.bint(1024)local utils={add=function(a,b)return tostring(bint(a)+bint(b))end,subtract=function(a,b)return tostring(bint(a)-bint(b))end,mul=function(a,b)return tostring(bint.__mul(bint(a),bint(b)))end,div=function(a,b)return tostring(bint.udiv(bint(a),bint(b)))end,lt=function(a,b)return bint.__lt(bint(a),bint(b))end,lte=function(a,b)return bint.__lt(bint(a),bint(b))or bint.__eq(bint(a),bint(b))end,gt=function(a,b)return bint.__lt(bint(b),bint(a))end,gte=function(a,b)return bint.__lt(bint(b),bint(a))or bint.__eq(bint(b),bint(a))end,isZero=function(a)return bint.__eq(bint(a),bint("0"))end}function utils.isAddress(addr)if type(addr)~="string"then return false end;if string.len(addr)~=43 then return false end;if string.match(addr,"^[A-z0-9_-]+$")==nil then return false end;return true end;function utils.isValidNumber(val)return type(val)=="number"and val==val and val~=math.huge and val~=-math.huge end;function utils.isValidInteger(val)return utils.isValidNumber(val)and val%1==0 end;function utils.isBintRaw(val)local success,result=pcall(function()if type(val)~="number"and type(val)~="string"and not bint.isbint(val)then return false end;if type(val)=="number"and not utils.isValidInteger(val)then return false end;return true end)return success and result end;function utils.isTokenQuantity(qty)local numVal=tonumber(qty)if not numVal or numVal<=0 then return false end;if not utils.isBintRaw(qty)then return false end;if type(qty)=="number"and qty<0 then return false end;if type(qty)=="string"and string.sub(qty,1,1)=="-"then return false end;return true end;function utils.isPercentage(val)if not val or type(val)~="number"then return false end;return val//1==val and val>=0 and val<=100 end;function utils.isValidDex(val)return val==enums.DexType.PERMASWAP or val==enums.DexType.BOTEGA or val==enums.DexType.Auto end;function utils.isValidSlippage(val)if not val or type(val)~="number"then return false end;return val//1==val and val>=0.5 and val<=10 end;function utils.isValidRunningTime(startDate,endDate)if not startDate or not endDate then return false end;return startDate<=endDate end;function utils.isValidBoolean(val)return val=="true"or val=="false"end;function utils.isValidStatus(val)return val==enums.AgentStatus.ACTIVE or val==enums.AgentStatus.CANCELLED or val==enums.AgentStatus.COMPLETED end;function utils.hasReachedEndDate()if not EndDate then return false end;local currentTime=os.time()local swappedOrProcessed=SwappedUpToDate or ProcessedUpToDate or 0;return currentTime>=EndDate and currentTime>=swappedOrProcessed end;function utils.isValidAgentVersion(version)if not version or type(version)~="string"then return false end;local major,minor,patch=version:match("^(%d+)%.(%d+)%.(%d+)$")if not major then return false end;major=tonumber(major)minor=tonumber(minor)patch=tonumber(patch)if not major or not minor or not patch then return false end;if major<0 or minor<0 or patch<0 then return false end;return true end;return utils end;_G.package.loaded["src.ao_yield_agent.utils"]=_loaded_mod_src_ao_yield_agent_utils()local function _loaded_mod_src_ao_yield_agent_libs_token()local constants=require('src.ao_yield_agent.libs.constants')local utils=require('src.ao_yield_agent.utils')local mod={}function mod.getBalance(tokenId)local result=ao.send({Target=tokenId,Action="Balance"}).receive()return result.Tags.Balance end;function mod.getAOBalance()return mod.getBalance(constants.AO_PROCESS_ID)end;function mod.transferToRecipient(tokenId,quantity,recipient)ao.send({Target=tokenId,Action="Transfer",Recipient=recipient,Quantity=quantity})end;function mod.transferToSelf(tokenId,quantity)mod.transferToRecipient(tokenId,quantity,Owner)end;function mod.transferRemainingBalanceToSelf()local aoBalance=mod.getAOBalance()if(not utils.isZero(aoBalance))then mod.transferToSelf(constants.AO_PROCESS_ID,aoBalance)end;local warBalance=mod.getBalance(constants.WAR_PROCESS_ID)if(not utils.isZero(warBalance))then mod.transferToSelf(constants.WAR_PROCESS_ID,warBalance)end;local wusdcBalance=mod.getBalance(constants.WUSDC_PROCESS_ID)if(not utils.isZero(wusdcBalance))then mod.transferToSelf(constants.WUSDC_PROCESS_ID,wusdcBalance)end end;return mod end;_G.package.loaded["src.ao_yield_agent.libs.token"]=_loaded_mod_src_ao_yield_agent_libs_token()local function _loaded_mod_src_ao_yield_agent_libs_permaswap()local utils=require('src.ao_yield_agent.utils')local json=require('json')local mod={}local function isSwapConfirmation(msg,noteSettle)return msg.Tags.Action=='Credit-Notice'and msg.Tags.Sender==noteSettle and msg.Tags["X-FFP-For"]=="Settled"end;local function isSwapRefund(msg,noteSettle)return msg.Tags.Action=='Credit-Notice'and msg.Tags.Sender==noteSettle and msg.Tags["X-FFP-For"]=="Refund"end;function mod._awaitSwap(noteSettle)local response=Receive(function(msg)return isSwapConfirmation(msg,noteSettle)or isSwapRefund(msg,noteSettle)end)if isSwapConfirmation(response,noteSettle)then return true,response else return false,response end end;function mod.getExpectedOutput(poolId,tokenIn,amountIn)local swapOutput=ao.send({Target=poolId,Action="GetAmountOut",AmountIn=amountIn,TokenIn=tokenIn}).receive()local amountOut=(swapOutput and swapOutput.AmountOut)or"0"local adjustedSlippage=math.floor(Slippage*100)local expectedMinOutput=utils.div(utils.mul(amountOut,utils.subtract(10000,adjustedSlippage)),10000)return{amountOut=tostring(amountOut),expectedMinOutput=tostring(expectedMinOutput)}end;function mod.requestOrder(poolId,tokenIn,tokenOut,amountIn,amountOut)local requestOrder=ao.send({Target=poolId,Action="RequestOrder",TokenIn=tokenIn,TokenOut=tokenOut,AmountIn=tostring(amountIn),AmountOut=tostring(amountOut)}).receive()return requestOrder end;function mod.swap(result)ao.send({Target=result.tokenIn,Action="Transfer",Recipient=result.noteSettle,Quantity=result.amountIn,["X-FFP-For"]="Settle",["X-FFP-NoteIDs"]=json.encode({result.noteId})})return mod._awaitSwap(result.noteSettle)end;return mod end;_G.package.loaded["src.ao_yield_agent.libs.permaswap"]=_loaded_mod_src_ao_yield_agent_libs_permaswap()local function _loaded_mod_src_ao_yield_agent_libs_botega()local utils=require("src.ao_yield_agent.utils")local constants=require("src.ao_yield_agent.libs.constants")local mod={}local function isSwapConfirmation(msg,poolId)return msg.From==constants.BOTEGA_AMM_FACTORY_ID and msg.Tags["Relayed-From"]==poolId and msg.Tags["Relay-To"]==ao.id and msg.Tags.Action=='Order-Confirmation'end;local function isSwapRefund(msg,poolId)return msg.Tags.Action=='Credit-Notice'and msg.Tags.Sender==poolId and msg.Tags["X-Refunded-Order"]~=nil end;function mod._awaitSwap(poolId)local response=Receive(function(msg)return isSwapConfirmation(msg,poolId)or isSwapRefund(msg,poolId)end)if isSwapConfirmation(response,poolId)then return true,response else return false,response end end;function mod.getExpectedOutput(poolId,tokenIn,amountIn)local swapOutput=ao.send({Target=poolId,Action="Get-Swap-Output",Tags={Token=tokenIn,Quantity=tostring(amountIn),Swapper=ao.id}}).receive()local amountOut=(swapOutput and swapOutput.Output)or"0"local adjustedSlippage=math.floor(Slippage*100)local expectedMinOutput=utils.div(utils.mul(amountOut,utils.subtract(10000,adjustedSlippage)),10000)return{amountOut=tostring(amountOut),expectedMinOutput=tostring(expectedMinOutput)}end;function mod.getSwapNonce()return os.time().."-"..math.random(100000000,999999999)end;function mod.swap(result)ao.send({Target=result.tokenIn,Action="Transfer",Recipient=result.poolId,Quantity=result.amountIn,["X-Expected-Min-Output"]=result.expectedMinOutput,["X-Swap-Nonce"]=mod.getSwapNonce(),["X-Action"]="Swap"})return mod._awaitSwap(result.poolId)end;return mod end;_G.package.loaded["src.ao_yield_agent.libs.botega"]=_loaded_mod_src_ao_yield_agent_libs_botega()local function _loaded_mod_src_ao_yield_agent_libs_swap()local constants=require('src.ao_yield_agent.libs.constants')local permaswap=require('src.ao_yield_agent.libs.permaswap')local botega=require('src.ao_yield_agent.libs.botega')local utils=require('src.ao_yield_agent.utils')local enums=require('src.ao_yield_agent.libs.enums')local aolibs=require('src.commonlibs.aolibs')local token=require('src.ao_yield_agent.libs.token')local json=require('json')local bint=aolibs.bint(1024)local mod={}function mod.getFeeInfo(quantity)local result=ao.send({Target=FeeProcessId,Action="Get-Fee",Tags={Target=Owner,Quantity=quantity or"0",["Fee-Type"]="defi"}}).receive()local swapFeePercent=tonumber(result.Tags["Final-Fee-Percent"])local adjustedSwapFeePercent=math.floor(swapFeePercent*100)local feeSavings=utils.subtract(result.Tags["Original-Fee"],result.Tags["Final-Fee"])return{SwapFeePercent=tostring(adjustedSwapFeePercent),FeeRecipient=result.Tags["Fee-Recipient"],FeeSavings=feeSavings}end;function mod.retrySwap(msg,params)local dex=params.dex;local feeInfo=params.feeInfo;local pushedFor=params.pushedFor;local balance=params.balance;local retry=params.retry;if retry and Dex==enums.DexType.Auto then if dex==enums.DexType.PERMASWAP then mod.swapAOTokenWithPermaswap(msg,nil,feeInfo,pushedFor,balance,false)elseif dex==enums.DexType.BOTEGA then mod.swapAOTokenWithBotega(msg,nil,feeInfo,pushedFor,balance,false)end end end;function mod.swapAOToken(msg,pushedFor)local tokenIn=constants.AO_PROCESS_ID;local tokenOut=msg.Tags["Token-Out"]or TokenOut;local permaswapPoolId=constants.PERMASWAP_POOL_IDS[tokenOut]local botegaPoolId=constants.BOTEGA_POOL_IDS[tokenOut]local amountIn=token.getAOBalance()or msg.Tags.Quantity;assert(bint.__lt(0,bint(amountIn)),'Quantity must be greater than 0')local dex=(utils.isValidDex(msg.Tags["X-Dex"])and msg.Tags["X-Dex"])or Dex;if dex==enums.DexType.PERMASWAP then mod.swapAOTokenWithPermaswap(msg,nil,nil,pushedFor,amountIn,true)elseif dex==enums.DexType.BOTEGA then mod.swapAOTokenWithBotega(msg,nil,nil,pushedFor,amountIn,true)else local permaswapOutput=permaswap.getExpectedOutput(permaswapPoolId,tokenIn,amountIn)local botegaOutput=botega.getExpectedOutput(botegaPoolId,tokenIn,amountIn)local feeInfo=mod.getFeeInfo(amountIn)if(utils.isZero(permaswapOutput.amountOut)and utils.isZero(botegaOutput.amountOut))then mod.replyWithSwapError(msg,"Amount out is zero")return end;if utils.lt(permaswapOutput.amountOut,botegaOutput.amountOut)then mod.swapAOTokenWithBotega(msg,botegaOutput,feeInfo,pushedFor,amountIn,true)else mod.swapAOTokenWithPermaswap(msg,permaswapOutput,feeInfo,pushedFor,amountIn,true)end end end;function mod.swapAOTokenWithPermaswap(msg,expectedOutput,feeInfo,pushedFor,balance,retry)local tokenIn=constants.AO_PROCESS_ID;local tokenOut=msg.Tags["Token-Out"]or TokenOut;local poolId=constants.PERMASWAP_POOL_IDS[tokenOut]local quantity=balance or token.getAOBalance()assert(bint.__lt(0,bint(quantity)),'Quantity must be greater than 0')feeInfo=feeInfo or mod.getFeeInfo(quantity)local fee=utils.div(utils.mul(quantity,feeInfo.SwapFeePercent),10000)local amountIn=utils.subtract(quantity,fee)local output=expectedOutput or permaswap.getExpectedOutput(poolId,tokenIn,amountIn)local retryParams={feeInfo=feeInfo,pushedFor=pushedFor,balance=quantity,retry=retry,dex=enums.DexType.BOTEGA}if(not utils.lte(output.expectedMinOutput,"0"))then local requestOrder=permaswap.requestOrder(poolId,tokenIn,tokenOut,amountIn,output.expectedMinOutput)local result={noteId=requestOrder.NoteID,noteSettle=requestOrder.NoteSettle,tokenIn=tokenIn,tokenOut=tokenOut,amountIn=tostring(amountIn),amountOut=tostring(output.amountOut),poolId=poolId,expectedMinOutput=tostring(output.expectedMinOutput)}if result.noteId and result.noteSettle then local success,swapResult=permaswap.swap(result)if(success)then token.transferToSelf(tokenOut,swapResult.Tags.Quantity)mod.swapSuccess(msg,{tokenOut=tokenOut,amountIn=quantity,amountOut=swapResult.Tags.Quantity,fee=fee,feeRecipient=feeInfo.FeeRecipient,dex=enums.DexType.PERMASWAP,swapDateFrom=msg.Tags["X-Swap-Date-From"],swapDateTo=msg.Tags["X-Swap-Date-To"],pushedFor=pushedFor,feeSavings=feeInfo.FeeSavings})else SwapInProgress=false;mod.replyWithSwapError(msg,result)mod.retrySwap(msg,retryParams)end else SwapInProgress=false;mod.replyWithSwapError(msg,result)mod.retrySwap(msg,retryParams)end else SwapInProgress=false;mod.replyWithSwapError(msg,"Amount out is zero")mod.retrySwap(msg,retryParams)end end;function mod.swapAOTokenWithBotega(msg,expectedOutput,feeInfo,pushedFor,balance,retry)local tokenIn=constants.AO_PROCESS_ID;local tokenOut=msg.Tags["Token-Out"]or TokenOut;local poolId=constants.BOTEGA_POOL_IDS[tokenOut]local quantity=balance or token.getAOBalance()assert(bint.__lt(0,bint(quantity)),'Quantity must be greater than 0')feeInfo=feeInfo or mod.getFeeInfo(quantity)local fee=utils.div(utils.mul(quantity,feeInfo.SwapFeePercent),10000)local amountIn=utils.subtract(quantity,fee)local output=expectedOutput or botega.getExpectedOutput(poolId,tokenIn,amountIn)local result={tokenIn=tokenIn,tokenOut=tokenOut,amountIn=tostring(amountIn),amountOut=tostring(output.amountOut),expectedMinOutput=tostring(output.expectedMinOutput),poolId=poolId}local retryParams={feeInfo=feeInfo,pushedFor=pushedFor,balance=quantity,retry=retry,dex=enums.DexType.PERMASWAP}if(not utils.lte(result.expectedMinOutput,"0"))then local success,swapResult=botega.swap(result)if(success)then mod.swapSuccess(msg,{tokenOut=tokenOut,amountIn=quantity,amountOut=swapResult.Tags["To-Quantity"],fee=fee,feeRecipient=feeInfo.FeeRecipient,dex=enums.DexType.BOTEGA,swapDateFrom=msg.Tags["X-Swap-Date-From"],swapDateTo=msg.Tags["X-Swap-Date-To"],pushedFor=pushedFor,feeSavings=feeInfo.FeeSavings})else SwapInProgress=false;mod.replyWithSwapError(msg,result)mod.retrySwap(msg,retryParams)end else SwapInProgress=false;mod.replyWithSwapError(msg,"Amount out is zero")mod.retrySwap(msg,retryParams)end end;function mod.swapSuccess(msg,swapInfo)local amountIn=swapInfo.amountIn;local fee=swapInfo.fee;local feeRecipient=swapInfo.feeRecipient;local currentTime=msg.Timestamp;local swapUpToDate=tonumber(swapInfo.swapDateTo)or currentTime;if(not SwappedUpToDate)then SwappedUpToDate=swapUpToDate else SwappedUpToDate=math.max(SwappedUpToDate,swapUpToDate)end;TotalTransactions=TotalTransactions+1;TotalAOSold=utils.add(TotalAOSold,amountIn)TotalWanderFee=utils.add(TotalWanderFee,fee)TotalBought[swapInfo.tokenOut]=utils.add(TotalBought[swapInfo.tokenOut]or"0",swapInfo.amountOut)SwapInProgress=false;if(utils.hasReachedEndDate())then Status=enums.AgentStatus.COMPLETED;ao.send({Target=ao.id,Action="Finalize-Agent"})end;token.transferToRecipient(constants.AO_PROCESS_ID,fee,feeRecipient)mod.replyWithSwapSuccess({amountIn=amountIn,amountOut=swapInfo.amountOut,tokenIn=constants.AO_PROCESS_ID,tokenOut=swapInfo.tokenOut,fee=fee,swapDateFrom=swapInfo.swapDateFrom,swapDateTo=swapInfo.swapDateTo,dex=swapInfo.dex,parentTxId=swapInfo.pushedFor,feeSavings=swapInfo.feeSavings})end;function mod.replyWithSwapSuccess(swapResult)local tags={["Amount-In"]=swapResult.amountIn,["Amount-Out"]=swapResult.amountOut,["Swap-Fee"]=swapResult.fee,["Token-In"]=swapResult.tokenIn,["Token-Out"]=swapResult.tokenOut,["Agent-Version"]=AgentVersion,["Dex"]=swapResult.dex,["Parent-Tx-Id"]=swapResult.parentTxId,["Fee-Savings"]=swapResult.feeSavings,["Agent-Owner"]=Owner}if swapResult.swapDateFrom and swapResult.swapDateTo then tags["Swap-Date-From"]=swapResult.swapDateFrom;tags["Swap-Date-To"]=swapResult.swapDateTo end;ao.send({Target=Owner,Data="Swap request processed successfully",Action="Swap-Success",Tags=tags})end;function mod.replyWithSwapError(msg,result)msg.reply({Data="Failed to process swap: "..(json.encode(result)or"Unknown error occurred"),Action="Swap-Error",Tags={Error="SwapProcessingFailed",Status="Failed",["Agent-Version"]=AgentVersion}})end;return mod end;_G.package.loaded["src.ao_yield_agent.libs.swap"]=_loaded_mod_src_ao_yield_agent_libs_swap()local function _loaded_mod_src_ao_yield_agent_libs_assertions()local utils=require("src.ao_yield_agent.utils")local enums=require("src.ao_yield_agent.libs.enums")local mod={}function mod.isTokenQuantity(name,quantity)local numQuantity=tonumber(quantity)assert(utils.isTokenQuantity(numQuantity),"Invalid quantity `"..name.."`. Must be a valid token quantity.")end;mod.isAddress=function(name,value)assert(utils.isAddress(value),"Invalid address `"..name.."`. Must be a valid Arweave address.")end;mod.isPercentage=function(name,value)assert(utils.isPercentage(value),"Invalid percentage `"..name.."`. Must be a valid percentage.")end;mod.checkWalletForPermission=function(msg,errorMessage)assert(ao.id==msg.From or Owner==msg.From,errorMessage or"Wallet does not have permission to update.")end;mod.isValidDex=function(name,value)assert(utils.isValidDex(value),"Invalid dex `"..name.."`. Must be a valid dex.")end;mod.isValidSlippage=function(name,value)assert(utils.isValidSlippage(value),"Invalid slippage `"..name.."`. Must be a valid slippage.")end;mod.isValidRunningTime=function(name1,name2,startDate,endDate)assert(utils.isValidRunningTime(startDate,endDate),"Invalid running time `"..name1.."` and `"..name2.."`. Must be a valid running time.")end;mod.isAgentActive=function()assert(Status==enums.AgentStatus.ACTIVE,"Agent is not active.")end;return mod end;_G.package.loaded["src.ao_yield_agent.libs.assertions"]=_loaded_mod_src_ao_yield_agent_libs_assertions()local constants=require('src.ao_yield_agent.libs.constants')local token=require('src.ao_yield_agent.libs.token')local swap=require('src.ao_yield_agent.libs.swap')local assertions=require('src.ao_yield_agent.libs.assertions')local utils=require('src.ao_yield_agent.utils')local enums=require('src.ao_yield_agent.libs.enums')local json=require('json')Status=Status or enums.AgentStatus.ACTIVE;Dex=Dex or ao.env.Process.Tags["Dex"]or enums.DexType.Auto;TokenOut=TokenOut or ao.env.Process.Tags["Token-Out"]or constants.WAR_PROCESS_ID;Slippage=Slippage or tonumber(ao.env.Process.Tags["Slippage"])or 0.5;StartDate=StartDate or tonumber(ao.env.Process.Tags["Start-Date"])or os.time()EndDate=EndDate or tonumber(ao.env.Process.Tags["End-Date"])or math.huge;RunIndefinitely=RunIndefinitely or ao.env.Process.Tags["Run-Indefinitely"]=="true"ConversionPercentage=ConversionPercentage or tonumber(ao.env.Process.Tags["Conversion-Percentage"])or 50;TotalTransactions=TotalTransactions or 0;TotalAOSold=TotalAOSold or"0"TotalWanderFee=TotalWanderFee or"0"TotalBought=TotalBought or{}ProcessedUpToDate=ProcessedUpToDate or nil;SwapInProgress=SwapInProgress or false;SwappedUpToDate=SwappedUpToDate or nil;FeeProcessId=FeeProcessId or"rkAezEIgacJZ_dVuZHOKJR8WKpSDqLGfgPJrs_Es7CA"AgentVersion=AgentVersion or ao.env.Process.Tags["Agent-Version"]or"1.0.1"Handlers.add("Info","Info",function(msg)msg.reply({Action="Info-Response",["Start-Date"]=tostring(StartDate),["End-Date"]=tostring(EndDate),Dex=Dex,["Token-Out"]=TokenOut,Slippage=tostring(Slippage),Status=Status,["Run-Indefinitely"]=tostring(RunIndefinitely),["Conversion-Percentage"]=tostring(ConversionPercentage),["Total-Transactions"]=tostring(TotalTransactions),["Total-AO-Sold"]=tostring(TotalAOSold),["Total-Wander-Fee"]=tostring(TotalWanderFee),["Total-Bought"]=json.encode(TotalBought),["Swap-In-Progress"]=tostring(SwapInProgress),["Processed-Up-To-Date"]=tostring(ProcessedUpToDate),["Swapped-Up-To-Date"]=tostring(SwappedUpToDate),["Agent-Version"]=AgentVersion})end)Handlers.add("Update-Agent","Update-Agent",function(msg)assertions.checkWalletForPermission(msg)assertions.isAgentActive()if(utils.isValidDex(msg.Tags.Dex))then Dex=msg.Tags.Dex end;if(utils.isValidSlippage(tonumber(msg.Tags.Slippage)))then Slippage=tonumber(msg.Tags.Slippage)end;if(utils.isValidRunningTime(tonumber(msg.Tags["Start-Date"]),tonumber(msg.Tags["End-Date"])))then StartDate=tonumber(msg.Tags["Start-Date"])EndDate=tonumber(msg.Tags["End-Date"])end;if(utils.isAddress(msg.Tags["Token-Out"]))then TokenOut=msg.Tags["Token-Out"]end;if(utils.isValidBoolean(msg.Tags["Run-Indefinitely"]))then RunIndefinitely=msg.Tags["Run-Indefinitely"]=="true"end;if(utils.isPercentage(tonumber(msg.Tags["Conversion-Percentage"])))then ConversionPercentage=tonumber(msg.Tags["Conversion-Percentage"])end;if(utils.isValidStatus(msg.Tags.Status))then Status=msg.Tags.Status;if(Status==enums.AgentStatus.COMPLETED or Status==enums.AgentStatus.CANCELLED)then ao.send({Target=ao.id,Action="Finalize-Agent"})end end;if(utils.isValidAgentVersion(msg.Tags["Agent-Version"]))then AgentVersion=msg.Tags["Agent-Version"]end end)Handlers.add("Swap-Permaswap","Swap-Permaswap",function(msg)assertions.checkWalletForPermission(msg,"Wallet does not have permission to swap.")SwapInProgress=true;swap.swapAOTokenWithPermaswap(msg,nil,nil,msg.Id)end)Handlers.add("Swap-Botega","Swap-Botega",function(msg)assertions.checkWalletForPermission(msg,"Wallet does not have permission to swap.")SwapInProgress=true;swap.swapAOTokenWithBotega(msg,nil,nil,msg.Id)end)Handlers.add("Withdraw","Withdraw",function(msg)assertions.checkWalletForPermission(msg,"Wallet does not have permission to withdraw.")local tokenId=msg.Tags["Token-Id"]local quantity=msg.Tags["Quantity"]assertions.isAddress("Token-Id",tokenId)assertions.isTokenQuantity("Quantity",quantity)token.transferToSelf(tokenId,quantity)end)Handlers.add("Credit-Notice","Credit-Notice",function(msg)local tokenId=msg.From or msg.Tags["From-Process"]local quantity=msg.Tags.Quantity;local pushedFor=msg.Tags["Pushed-For"]if tokenId==constants.AO_PROCESS_ID then SwapInProgress=true;ProcessedUpToDate=tonumber(msg.Tags["X-Swap-Date-To"])or os.time()swap.swapAOToken(msg,pushedFor)elseif tokenId==constants.WAR_PROCESS_ID or tokenId==constants.WUSDC_PROCESS_ID then token.transferToSelf(tokenId,quantity)end end)Handlers.add("Finalize-Agent","Finalize-Agent",function(msg)assertions.checkWalletForPermission(msg,"Wallet does not have permission to finalize the agent.")SwapInProgress=true;local aoBalance=token.getAOBalance()if(not utils.isZero(aoBalance))then swap.swapAOToken(msg,msg.Id)end;token.transferRemainingBalanceToSelf()end) \ No newline at end of file +local function _loaded_mod_src_ao_yield_agent_libs_constants()local mod={PERMASWAP_AO_WAR_POOL_ID="FRF1k0BSv0gRzNA2n-95_Fpz9gADq9BGi5PyXKFp6r8",PERMASWAP_AO_WUSDC_POOL_ID="gjnaCsEd749ZXeG2H8akvf8wzbl7CQ4Ox-KYEBAdONk",BOTEGA_AO_WAR_POOL_ID="B6qAwHi2OjZmyFCEU8hV6FZDSHbAOz8r0yy-fBbuTus",BOTEGA_AO_WUSDC_POOL_ID="TYqlQ2vqkF0H6nC0mCgGe6G12pqq9DsSXpvtHYc6_xY",BOTEGA_AMM_FACTORY_ID="3XBGLrygs11K63F_7mldWz4veNx6Llg6hI2yZs8LKHo",AO_PROCESS_ID="0syT13r0s0tgPmIed95bJnuSqaD29HQNN8D3ElLSrsc",WAR_PROCESS_ID="xU9zFkq3X2ZQ6olwNVvr1vUWIjc3kXTWr7xKQD6dh10",WUSDC_PROCESS_ID="7zH9dlMNoxprab9loshv3Y7WG45DOny_Vrq9KrXObdQ"}mod.PERMASWAP_POOL_IDS={[mod.WAR_PROCESS_ID]=mod.PERMASWAP_AO_WAR_POOL_ID,[mod.WUSDC_PROCESS_ID]=mod.PERMASWAP_AO_WUSDC_POOL_ID}mod.BOTEGA_POOL_IDS={[mod.WAR_PROCESS_ID]=mod.BOTEGA_AO_WAR_POOL_ID,[mod.WUSDC_PROCESS_ID]=mod.BOTEGA_AO_WUSDC_POOL_ID}return mod end;_G.package.loaded["src.ao_yield_agent.libs.constants"]=_loaded_mod_src_ao_yield_agent_libs_constants()local function _loaded_mod_src_commonlibs_aolibs()local mod={}local mod_json;local mod_bint;local jsonstatus,json=pcall(require,"json")if jsonstatus then mod_json=json else if not mod_json then print("Library 'json' does not exist. Using fallback dkjson.")end;local dkjsonstatus,dkjson=pcall(require,"dkjson")mod_json=dkjson end;local bintstatus,bint=pcall(require,".bint")if bintstatus then mod_bint=bint else if not mod_bint then print("Library '.bint' does not exist. Using fallback https://github.com/permaweb/aos/blob/main/process/bint.lua")end;local copiedbintstatus,copiedbint=pcall(require,"src.commonlibs.bint")mod_bint=copiedbint end;mod.bint=function(bits,word_bits)if not bits or type(bits)~="number"then bits=1024 end;local instance=mod_bint(bits,word_bits)return instance end;mod.json=mod_json;return mod end;_G.package.loaded["src.commonlibs.aolibs"]=_loaded_mod_src_commonlibs_aolibs()local function _loaded_mod_src_ao_yield_agent_libs_enums()local DexType={PERMASWAP="Permaswap",BOTEGA="Botega",Auto="Auto"}local AgentStatus={ACTIVE="Active",CANCELLED="Cancelled",COMPLETED="Completed"}local enums={DexType=DexType,AgentStatus=AgentStatus}return enums end;_G.package.loaded["src.ao_yield_agent.libs.enums"]=_loaded_mod_src_ao_yield_agent_libs_enums()local function _loaded_mod_src_ao_yield_agent_utils()local aolibs=require('src.commonlibs.aolibs')local enums=require('src.ao_yield_agent.libs.enums')local bint=aolibs.bint(1024)local utils={add=function(a,b)return tostring(bint(a)+bint(b))end,subtract=function(a,b)return tostring(bint(a)-bint(b))end,mul=function(a,b)return tostring(bint.__mul(bint(a),bint(b)))end,div=function(a,b)return tostring(bint.udiv(bint(a),bint(b)))end,lt=function(a,b)return bint.__lt(bint(a),bint(b))end,lte=function(a,b)return bint.__lt(bint(a),bint(b))or bint.__eq(bint(a),bint(b))end,gt=function(a,b)return bint.__lt(bint(b),bint(a))end,gte=function(a,b)return bint.__lt(bint(b),bint(a))or bint.__eq(bint(b),bint(a))end,isZero=function(a)return bint.__eq(bint(a),bint("0"))end}function utils.isAddress(addr)if type(addr)~="string"then return false end;if string.len(addr)~=43 then return false end;if string.match(addr,"^[A-z0-9_-]+$")==nil then return false end;return true end;function utils.isValidNumber(val)return type(val)=="number"and val==val and val~=math.huge and val~=-math.huge end;function utils.isValidInteger(val)return utils.isValidNumber(val)and val%1==0 end;function utils.isBintRaw(val)local success,result=pcall(function()if type(val)~="number"and type(val)~="string"and not bint.isbint(val)then return false end;if type(val)=="number"and not utils.isValidInteger(val)then return false end;return true end)return success and result end;function utils.isTokenQuantity(qty)local numVal=tonumber(qty)if not numVal or numVal<=0 then return false end;if not utils.isBintRaw(qty)then return false end;if type(qty)=="number"and qty<0 then return false end;if type(qty)=="string"and string.sub(qty,1,1)=="-"then return false end;return true end;function utils.isPercentage(val)if not val or type(val)~="number"then return false end;return val//1==val and val>=0 and val<=100 end;function utils.isValidDex(val)return val==enums.DexType.PERMASWAP or val==enums.DexType.BOTEGA or val==enums.DexType.Auto end;function utils.isValidSlippage(val)if not val or type(val)~="number"then return false end;return val>=0.5 and val<=10 and(val*10)%1==0 end;function utils.isValidRunningTime(startDate,endDate)if not startDate or not endDate then return false end;return startDate<=endDate end;function utils.isValidBoolean(val)return val=="true"or val=="false"end;function utils.isValidStatus(val)return val==enums.AgentStatus.ACTIVE or val==enums.AgentStatus.CANCELLED or val==enums.AgentStatus.COMPLETED end;function utils.hasReachedEndDate()if not EndDate then return false end;local currentTime=os.time()local swappedOrProcessed=SwappedUpToDate or ProcessedUpToDate or 0;return currentTime>=EndDate and currentTime>=swappedOrProcessed end;function utils.isValidAgentVersion(version)if not version or type(version)~="string"then return false end;local major,minor,patch=version:match("^(%d+)%.(%d+)%.(%d+)$")if not major then return false end;major=tonumber(major)minor=tonumber(minor)patch=tonumber(patch)if not major or not minor or not patch then return false end;if major<0 or minor<0 or patch<0 then return false end;return true end;return utils end;_G.package.loaded["src.ao_yield_agent.utils"]=_loaded_mod_src_ao_yield_agent_utils()local function _loaded_mod_src_ao_yield_agent_libs_token()local constants=require('src.ao_yield_agent.libs.constants')local utils=require('src.ao_yield_agent.utils')local mod={}function mod.getBalance(tokenId)local result=ao.send({Target=tokenId,Action="Balance"}).receive()return result.Tags.Balance end;function mod.getAOBalance()return mod.getBalance(constants.AO_PROCESS_ID)end;function mod.transferToRecipient(tokenId,quantity,recipient)ao.send({Target=tokenId,Action="Transfer",Recipient=recipient,Quantity=quantity})end;function mod.transferToSelf(tokenId,quantity)mod.transferToRecipient(tokenId,quantity,Owner)end;function mod.transferRemainingBalanceToSelf()local aoBalance=mod.getAOBalance()if(not utils.isZero(aoBalance))then mod.transferToSelf(constants.AO_PROCESS_ID,aoBalance)end;local warBalance=mod.getBalance(constants.WAR_PROCESS_ID)if(not utils.isZero(warBalance))then mod.transferToSelf(constants.WAR_PROCESS_ID,warBalance)end;local wusdcBalance=mod.getBalance(constants.WUSDC_PROCESS_ID)if(not utils.isZero(wusdcBalance))then mod.transferToSelf(constants.WUSDC_PROCESS_ID,wusdcBalance)end end;return mod end;_G.package.loaded["src.ao_yield_agent.libs.token"]=_loaded_mod_src_ao_yield_agent_libs_token()local function _loaded_mod_src_ao_yield_agent_libs_permaswap()local utils=require('src.ao_yield_agent.utils')local json=require('json')local mod={}local function isSwapConfirmation(msg,noteSettle)return msg.Tags.Action=='Credit-Notice'and msg.Tags.Sender==noteSettle and msg.Tags["X-FFP-For"]=="Settled"end;local function isSwapRefund(msg,noteSettle)return msg.Tags.Action=='Credit-Notice'and msg.Tags.Sender==noteSettle and msg.Tags["X-FFP-For"]=="Refund"end;function mod._awaitSwap(noteSettle)local response=Receive(function(msg)return isSwapConfirmation(msg,noteSettle)or isSwapRefund(msg,noteSettle)end)if isSwapConfirmation(response,noteSettle)then return true,response else return false,response end end;function mod.getExpectedOutput(poolId,tokenIn,amountIn)local swapOutput=ao.send({Target=poolId,Action="GetAmountOut",AmountIn=amountIn,TokenIn=tokenIn}).receive()local amountOut=(swapOutput and swapOutput.AmountOut)or"0"local adjustedSlippage=math.floor(Slippage*100)local expectedMinOutput=utils.div(utils.mul(amountOut,utils.subtract(10000,adjustedSlippage)),10000)return{amountOut=tostring(amountOut),expectedMinOutput=tostring(expectedMinOutput)}end;function mod.requestOrder(poolId,tokenIn,tokenOut,amountIn,amountOut)local requestOrder=ao.send({Target=poolId,Action="RequestOrder",TokenIn=tokenIn,TokenOut=tokenOut,AmountIn=tostring(amountIn),AmountOut=tostring(amountOut)}).receive()return requestOrder end;function mod.swap(result)ao.send({Target=result.tokenIn,Action="Transfer",Recipient=result.noteSettle,Quantity=result.amountIn,["X-FFP-For"]="Settle",["X-FFP-NoteIDs"]=json.encode({result.noteId})})return mod._awaitSwap(result.noteSettle)end;return mod end;_G.package.loaded["src.ao_yield_agent.libs.permaswap"]=_loaded_mod_src_ao_yield_agent_libs_permaswap()local function _loaded_mod_src_ao_yield_agent_libs_botega()local utils=require("src.ao_yield_agent.utils")local constants=require("src.ao_yield_agent.libs.constants")local mod={}local function isSwapConfirmation(msg,poolId)return msg.From==constants.BOTEGA_AMM_FACTORY_ID and msg.Tags["Relayed-From"]==poolId and msg.Tags["Relay-To"]==ao.id and msg.Tags.Action=='Order-Confirmation'end;local function isSwapRefund(msg,poolId)return msg.Tags.Action=='Credit-Notice'and msg.Tags.Sender==poolId and msg.Tags["X-Refunded-Order"]~=nil end;function mod._awaitSwap(poolId)local response=Receive(function(msg)return isSwapConfirmation(msg,poolId)or isSwapRefund(msg,poolId)end)if isSwapConfirmation(response,poolId)then return true,response else return false,response end end;function mod.getExpectedOutput(poolId,tokenIn,amountIn)local swapOutput=ao.send({Target=poolId,Action="Get-Swap-Output",Tags={Token=tokenIn,Quantity=tostring(amountIn),Swapper=ao.id}}).receive()local amountOut=(swapOutput and swapOutput.Output)or"0"local adjustedSlippage=math.floor(Slippage*100)local expectedMinOutput=utils.div(utils.mul(amountOut,utils.subtract(10000,adjustedSlippage)),10000)return{amountOut=tostring(amountOut),expectedMinOutput=tostring(expectedMinOutput)}end;function mod.getSwapNonce()return os.time().."-"..math.random(100000000,999999999)end;function mod.swap(result)ao.send({Target=result.tokenIn,Action="Transfer",Recipient=result.poolId,Quantity=result.amountIn,["X-Expected-Min-Output"]=result.expectedMinOutput,["X-Swap-Nonce"]=mod.getSwapNonce(),["X-Action"]="Swap"})return mod._awaitSwap(result.poolId)end;return mod end;_G.package.loaded["src.ao_yield_agent.libs.botega"]=_loaded_mod_src_ao_yield_agent_libs_botega()local function _loaded_mod_src_ao_yield_agent_libs_swap()local constants=require('src.ao_yield_agent.libs.constants')local permaswap=require('src.ao_yield_agent.libs.permaswap')local botega=require('src.ao_yield_agent.libs.botega')local utils=require('src.ao_yield_agent.utils')local enums=require('src.ao_yield_agent.libs.enums')local token=require('src.ao_yield_agent.libs.token')local json=require('json')local mod={}function mod.assertQuantityGreaterThanZero(quantity)if utils.lte(quantity,"0")then SwapInProgress=false;Send({device="patch@1.0",["agent-info"]={["swap-in-progress"]=false}})error('Quantity must be greater than 0')end end;function mod.getFeeInfo(quantity)local result=ao.send({Target=FeeProcessId,Action="Get-Fee",Tags={Target=Owner,Quantity=quantity or"0",["Fee-Type"]="defi"}}).receive()local swapFeePercent=tonumber(result.Tags["Final-Fee-Percent"])local adjustedSwapFeePercent=math.floor(swapFeePercent*100)local feeSavings=utils.subtract(result.Tags["Original-Fee"],result.Tags["Final-Fee"])return{SwapFeePercent=tostring(adjustedSwapFeePercent),FeeRecipient=result.Tags["Fee-Recipient"],FeeSavings=feeSavings}end;function mod.retrySwap(msg,params)local dex=params.dex;local feeInfo=params.feeInfo;local pushedFor=params.pushedFor;local balance=params.balance;local retry=params.retry;if retry and Dex==enums.DexType.Auto then if dex==enums.DexType.PERMASWAP then mod.swapAOTokenWithPermaswap(msg,nil,feeInfo,pushedFor,balance,false)elseif dex==enums.DexType.BOTEGA then mod.swapAOTokenWithBotega(msg,nil,feeInfo,pushedFor,balance,false)end else SwapInProgress=false;Send({device="patch@1.0",["agent-info"]={["swap-in-progress"]=false}})end end;function mod.swapAOToken(msg,pushedFor)local tokenIn=constants.AO_PROCESS_ID;local tokenOut=msg.Tags["Token-Out"]or TokenOut;local permaswapPoolId=constants.PERMASWAP_POOL_IDS[tokenOut]local botegaPoolId=constants.BOTEGA_POOL_IDS[tokenOut]local amountIn=token.getAOBalance()or msg.Tags.Quantity;mod.assertQuantityGreaterThanZero(amountIn)local dex=(utils.isValidDex(msg.Tags["X-Dex"])and msg.Tags["X-Dex"])or Dex;if dex==enums.DexType.PERMASWAP then mod.swapAOTokenWithPermaswap(msg,nil,nil,pushedFor,amountIn,true)elseif dex==enums.DexType.BOTEGA then mod.swapAOTokenWithBotega(msg,nil,nil,pushedFor,amountIn,true)else local permaswapOutput=permaswap.getExpectedOutput(permaswapPoolId,tokenIn,amountIn)local botegaOutput=botega.getExpectedOutput(botegaPoolId,tokenIn,amountIn)local feeInfo=mod.getFeeInfo(amountIn)if(utils.isZero(permaswapOutput.amountOut)and utils.isZero(botegaOutput.amountOut))then mod.replyWithSwapError(msg,"Amount out is zero")SwapInProgress=false;Send({device="patch@1.0",["agent-info"]={["swap-in-progress"]=false}})return end;if utils.lt(permaswapOutput.amountOut,botegaOutput.amountOut)then mod.swapAOTokenWithBotega(msg,botegaOutput,feeInfo,pushedFor,amountIn,true)else mod.swapAOTokenWithPermaswap(msg,permaswapOutput,feeInfo,pushedFor,amountIn,true)end end end;function mod.swapAOTokenWithPermaswap(msg,expectedOutput,feeInfo,pushedFor,balance,retry)local tokenIn=constants.AO_PROCESS_ID;local tokenOut=msg.Tags["Token-Out"]or TokenOut;local poolId=constants.PERMASWAP_POOL_IDS[tokenOut]local quantity=balance or token.getAOBalance()mod.assertQuantityGreaterThanZero(quantity)feeInfo=feeInfo or mod.getFeeInfo(quantity)local fee=utils.div(utils.mul(quantity,feeInfo.SwapFeePercent),10000)local amountIn=utils.subtract(quantity,fee)local output=expectedOutput or permaswap.getExpectedOutput(poolId,tokenIn,amountIn)local retryParams={feeInfo=feeInfo,pushedFor=pushedFor,balance=quantity,retry=retry,dex=enums.DexType.BOTEGA}if(not utils.lte(output.expectedMinOutput,"0"))then local requestOrder=permaswap.requestOrder(poolId,tokenIn,tokenOut,amountIn,output.expectedMinOutput)local result={noteId=requestOrder.NoteID,noteSettle=requestOrder.NoteSettle,tokenIn=tokenIn,tokenOut=tokenOut,amountIn=tostring(amountIn),amountOut=tostring(output.amountOut),poolId=poolId,expectedMinOutput=tostring(output.expectedMinOutput)}if result.noteId and result.noteSettle then local success,swapResult=permaswap.swap(result)if(success)then token.transferToSelf(tokenOut,swapResult.Tags.Quantity)mod.swapSuccess(msg,{tokenOut=tokenOut,amountIn=quantity,amountOut=swapResult.Tags.Quantity,fee=fee,feeRecipient=feeInfo.FeeRecipient,dex=enums.DexType.PERMASWAP,swapDateFrom=msg.Tags["X-Swap-Date-From"],swapDateTo=msg.Tags["X-Swap-Date-To"],pushedFor=pushedFor,feeSavings=feeInfo.FeeSavings})else mod.replyWithSwapError(msg,result)mod.retrySwap(msg,retryParams)end else mod.replyWithSwapError(msg,result)mod.retrySwap(msg,retryParams)end else mod.replyWithSwapError(msg,"Amount out is zero")mod.retrySwap(msg,retryParams)end end;function mod.swapAOTokenWithBotega(msg,expectedOutput,feeInfo,pushedFor,balance,retry)local tokenIn=constants.AO_PROCESS_ID;local tokenOut=msg.Tags["Token-Out"]or TokenOut;local poolId=constants.BOTEGA_POOL_IDS[tokenOut]local quantity=balance or token.getAOBalance()mod.assertQuantityGreaterThanZero(quantity)feeInfo=feeInfo or mod.getFeeInfo(quantity)local fee=utils.div(utils.mul(quantity,feeInfo.SwapFeePercent),10000)local amountIn=utils.subtract(quantity,fee)local output=expectedOutput or botega.getExpectedOutput(poolId,tokenIn,amountIn)local result={tokenIn=tokenIn,tokenOut=tokenOut,amountIn=tostring(amountIn),amountOut=tostring(output.amountOut),expectedMinOutput=tostring(output.expectedMinOutput),poolId=poolId}local retryParams={feeInfo=feeInfo,pushedFor=pushedFor,balance=quantity,retry=retry,dex=enums.DexType.PERMASWAP}if(not utils.lte(result.expectedMinOutput,"0"))then local success,swapResult=botega.swap(result)if(success)then mod.swapSuccess(msg,{tokenOut=tokenOut,amountIn=quantity,amountOut=swapResult.Tags["To-Quantity"],fee=fee,feeRecipient=feeInfo.FeeRecipient,dex=enums.DexType.BOTEGA,swapDateFrom=msg.Tags["X-Swap-Date-From"],swapDateTo=msg.Tags["X-Swap-Date-To"],pushedFor=pushedFor,feeSavings=feeInfo.FeeSavings})else mod.replyWithSwapError(msg,result)mod.retrySwap(msg,retryParams)end else mod.replyWithSwapError(msg,"Amount out is zero")mod.retrySwap(msg,retryParams)end end;function mod.swapSuccess(msg,swapInfo)local amountIn=swapInfo.amountIn;local fee=swapInfo.fee;local feeRecipient=swapInfo.feeRecipient;local currentTime=msg.Timestamp;local swapUpToDate=tonumber(swapInfo.swapDateTo)or currentTime;if(not SwappedUpToDate)then SwappedUpToDate=swapUpToDate else SwappedUpToDate=math.max(SwappedUpToDate,swapUpToDate)end;TotalTransactions=TotalTransactions+1;TotalAOSold=utils.add(TotalAOSold,amountIn)TotalWanderFee=utils.add(TotalWanderFee,fee)TotalBought[swapInfo.tokenOut]=utils.add(TotalBought[swapInfo.tokenOut]or"0",swapInfo.amountOut)SwapInProgress=false;if(utils.hasReachedEndDate())then Status=enums.AgentStatus.COMPLETED;ao.send({Target=ao.id,Action="Finalize-Agent"})end;Send({device="patch@1.0",["agent-info"]={status=Status,["total-transactions"]=TotalTransactions,["total-ao-sold"]=TotalAOSold,["total-wander-fee"]=TotalWanderFee,["total-bought"]=json.encode(TotalBought),["swap-in-progress"]=SwapInProgress,["swapped-up-to-date"]=SwappedUpToDate}})token.transferToRecipient(constants.AO_PROCESS_ID,fee,feeRecipient)mod.replyWithSwapSuccess({amountIn=amountIn,amountOut=swapInfo.amountOut,tokenIn=constants.AO_PROCESS_ID,tokenOut=swapInfo.tokenOut,fee=fee,swapDateFrom=swapInfo.swapDateFrom,swapDateTo=swapInfo.swapDateTo,dex=swapInfo.dex,parentTxId=swapInfo.pushedFor,feeSavings=swapInfo.feeSavings})end;function mod.replyWithSwapSuccess(swapResult)local tags={["Amount-In"]=swapResult.amountIn,["Amount-Out"]=swapResult.amountOut,["Swap-Fee"]=swapResult.fee,["Token-In"]=swapResult.tokenIn,["Token-Out"]=swapResult.tokenOut,["Agent-Version"]=AgentVersion,["Dex"]=swapResult.dex,["Parent-Tx-Id"]=swapResult.parentTxId,["Fee-Savings"]=swapResult.feeSavings,["Agent-Owner"]=Owner}if swapResult.swapDateFrom and swapResult.swapDateTo then tags["Swap-Date-From"]=swapResult.swapDateFrom;tags["Swap-Date-To"]=swapResult.swapDateTo end;ao.send({Target=Owner,Data="Swap request processed successfully",Action="Swap-Success",Tags=tags})end;function mod.replyWithSwapError(msg,result)msg.reply({Data="Failed to process swap: "..(json.encode(result)or"Unknown error occurred"),Action="Swap-Error",Tags={Error="SwapProcessingFailed",Status="Failed",["Agent-Version"]=AgentVersion}})end;return mod end;_G.package.loaded["src.ao_yield_agent.libs.swap"]=_loaded_mod_src_ao_yield_agent_libs_swap()local function _loaded_mod_src_ao_yield_agent_libs_assertions()local utils=require("src.ao_yield_agent.utils")local enums=require("src.ao_yield_agent.libs.enums")local mod={}function mod.isTokenQuantity(name,quantity)local numQuantity=tonumber(quantity)assert(utils.isTokenQuantity(numQuantity),"Invalid quantity `"..name.."`. Must be a valid token quantity.")end;mod.isAddress=function(name,value)assert(utils.isAddress(value),"Invalid address `"..name.."`. Must be a valid Arweave address.")end;mod.isPercentage=function(name,value)assert(utils.isPercentage(value),"Invalid percentage `"..name.."`. Must be a valid percentage.")end;mod.checkWalletForPermission=function(msg,errorMessage)assert(ao.id==msg.From or Owner==msg.From,errorMessage or"Wallet does not have permission to update.")end;mod.isValidDex=function(name,value)assert(utils.isValidDex(value),"Invalid dex `"..name.."`. Must be a valid dex.")end;mod.isValidSlippage=function(name,value)assert(utils.isValidSlippage(value),"Invalid slippage `"..name.."`. Must be a valid slippage.")end;mod.isValidRunningTime=function(name1,name2,startDate,endDate)assert(utils.isValidRunningTime(startDate,endDate),"Invalid running time `"..name1.."` and `"..name2.."`. Must be a valid running time.")end;mod.isAgentActive=function()assert(Status==enums.AgentStatus.ACTIVE,"Agent is not active.")end;return mod end;_G.package.loaded["src.ao_yield_agent.libs.assertions"]=_loaded_mod_src_ao_yield_agent_libs_assertions()local function _loaded_mod_src_ao_yield_agent_libs_patch()local json=require('json')local mod={}mod.getFullPatchData=function()return{["start-date"]=StartDate,["end-date"]=EndDate,dex=Dex,["token-out"]=TokenOut,slippage=Slippage,status=Status,["run-indefinitely"]=RunIndefinitely,["conversion-percentage"]=ConversionPercentage,["total-transactions"]=TotalTransactions,["total-ao-sold"]=TotalAOSold,["total-wander-fee"]=TotalWanderFee,["total-bought"]=json.encode(TotalBought),["swap-in-progress"]=SwapInProgress,["processed-up-to-date"]=ProcessedUpToDate,["swapped-up-to-date"]=SwappedUpToDate,["agent-version"]=AgentVersion}end;return mod end;_G.package.loaded["src.ao_yield_agent.libs.patch"]=_loaded_mod_src_ao_yield_agent_libs_patch()local constants=require('src.ao_yield_agent.libs.constants')local token=require('src.ao_yield_agent.libs.token')local swap=require('src.ao_yield_agent.libs.swap')local assertions=require('src.ao_yield_agent.libs.assertions')local utils=require('src.ao_yield_agent.utils')local enums=require('src.ao_yield_agent.libs.enums')local patch=require('src.ao_yield_agent.libs.patch')local json=require('json')Status=Status or enums.AgentStatus.ACTIVE;Dex=Dex or ao.env.Process.Tags["Dex"]or enums.DexType.Auto;TokenOut=TokenOut or ao.env.Process.Tags["Token-Out"]or constants.WAR_PROCESS_ID;Slippage=Slippage or tonumber(ao.env.Process.Tags["Slippage"])or 0.5;StartDate=StartDate or tonumber(ao.env.Process.Tags["Start-Date"])or os.time()EndDate=EndDate or tonumber(ao.env.Process.Tags["End-Date"])or math.huge;RunIndefinitely=RunIndefinitely or ao.env.Process.Tags["Run-Indefinitely"]=="true"ConversionPercentage=ConversionPercentage or tonumber(ao.env.Process.Tags["Conversion-Percentage"])or 50;TotalTransactions=TotalTransactions or 0;TotalAOSold=TotalAOSold or"0"TotalWanderFee=TotalWanderFee or"0"TotalBought=TotalBought or{}ProcessedUpToDate=ProcessedUpToDate or nil;SwapInProgress=SwapInProgress or false;SwappedUpToDate=SwappedUpToDate or nil;FeeProcessId=FeeProcessId or"rkAezEIgacJZ_dVuZHOKJR8WKpSDqLGfgPJrs_Es7CA"AgentVersion=AgentVersion or ao.env.Process.Tags["Agent-Version"]or"1.0.2"IsPatched=IsPatched or false;if not IsPatched then IsPatched=true;Send({device="patch@1.0",["agent-info"]=patch.getFullPatchData()})end;Handlers.add("Info","Info",function(msg)msg.reply({Action="Info-Response",["Start-Date"]=tostring(StartDate),["End-Date"]=tostring(EndDate),Dex=Dex,["Token-Out"]=TokenOut,Slippage=tostring(Slippage),Status=Status,["Run-Indefinitely"]=tostring(RunIndefinitely),["Conversion-Percentage"]=tostring(ConversionPercentage),["Total-Transactions"]=tostring(TotalTransactions),["Total-AO-Sold"]=tostring(TotalAOSold),["Total-Wander-Fee"]=tostring(TotalWanderFee),["Total-Bought"]=json.encode(TotalBought),["Swap-In-Progress"]=tostring(SwapInProgress),["Processed-Up-To-Date"]=tostring(ProcessedUpToDate),["Swapped-Up-To-Date"]=tostring(SwappedUpToDate),["Agent-Version"]=AgentVersion})end)Handlers.add("Update-Agent","Update-Agent",function(msg)assertions.checkWalletForPermission(msg)assertions.isAgentActive()local patchData={}if(utils.isValidDex(msg.Tags.Dex))then Dex=msg.Tags.Dex;patchData.dex=Dex end;if(utils.isValidSlippage(tonumber(msg.Tags.Slippage)))then Slippage=tonumber(msg.Tags.Slippage)patchData.slippage=Slippage end;if(utils.isValidRunningTime(tonumber(msg.Tags["Start-Date"]),tonumber(msg.Tags["End-Date"])))then StartDate=tonumber(msg.Tags["Start-Date"])EndDate=tonumber(msg.Tags["End-Date"])patchData["start-date"]=StartDate;patchData["end-date"]=EndDate end;if(utils.isAddress(msg.Tags["Token-Out"]))then TokenOut=msg.Tags["Token-Out"]patchData["token-out"]=TokenOut end;if(utils.isValidBoolean(msg.Tags["Run-Indefinitely"]))then RunIndefinitely=msg.Tags["Run-Indefinitely"]=="true"patchData["run-indefinitely"]=RunIndefinitely end;if(utils.isPercentage(tonumber(msg.Tags["Conversion-Percentage"])))then ConversionPercentage=tonumber(msg.Tags["Conversion-Percentage"])patchData["conversion-percentage"]=ConversionPercentage end;if(utils.isValidStatus(msg.Tags.Status))then Status=msg.Tags.Status;patchData.status=Status;if(Status==enums.AgentStatus.COMPLETED or Status==enums.AgentStatus.CANCELLED)then ao.send({Target=ao.id,Action="Finalize-Agent"})end end;if(utils.isValidAgentVersion(msg.Tags["Agent-Version"]))then AgentVersion=msg.Tags["Agent-Version"]patchData["agent-version"]=AgentVersion end;if(msg.Tags["Full-Patch"]=="true")then local fullPatchData=patch.getFullPatchData()for k,v in pairs(fullPatchData)do patchData[k]=v end end;Send({device="patch@1.0",["agent-info"]=patchData})end)Handlers.add("Swap-Permaswap","Swap-Permaswap",function(msg)assertions.checkWalletForPermission(msg,"Wallet does not have permission to swap.")SwapInProgress=true;Send({device="patch@1.0",["agent-info"]={["swap-in-progress"]=true}})swap.swapAOTokenWithPermaswap(msg,nil,nil,msg.Id)end)Handlers.add("Swap-Botega","Swap-Botega",function(msg)assertions.checkWalletForPermission(msg,"Wallet does not have permission to swap.")SwapInProgress=true;Send({device="patch@1.0",["agent-info"]={["swap-in-progress"]=true}})swap.swapAOTokenWithBotega(msg,nil,nil,msg.Id)end)Handlers.add("Withdraw","Withdraw",function(msg)assertions.checkWalletForPermission(msg,"Wallet does not have permission to withdraw.")local tokenId=msg.Tags["Token-Id"]local quantity=msg.Tags["Quantity"]assertions.isAddress("Token-Id",tokenId)assertions.isTokenQuantity("Quantity",quantity)token.transferToSelf(tokenId,quantity)end)Handlers.add("Credit-Notice","Credit-Notice",function(msg)local tokenId=msg.From or msg.Tags["From-Process"]local quantity=msg.Tags.Quantity;local pushedFor=msg.Tags["Pushed-For"]if tokenId==constants.AO_PROCESS_ID then SwapInProgress=true;ProcessedUpToDate=tonumber(msg.Tags["X-Swap-Date-To"])or os.time()Send({device="patch@1.0",["agent-info"]={["swap-in-progress"]=true,["processed-up-to-date"]=ProcessedUpToDate}})swap.swapAOToken(msg,pushedFor)elseif tokenId==constants.WAR_PROCESS_ID or tokenId==constants.WUSDC_PROCESS_ID then token.transferToSelf(tokenId,quantity)end end)Handlers.add("Finalize-Agent","Finalize-Agent",function(msg)assertions.checkWalletForPermission(msg,"Wallet does not have permission to finalize the agent.")local aoBalance=token.getAOBalance()if(not utils.isZero(aoBalance))then SwapInProgress=true;Send({device="patch@1.0",["agent-info"]={["swap-in-progress"]=true}})swap.swapAOToken(msg,msg.Id)end;token.transferRemainingBalanceToSelf()end) \ No newline at end of file diff --git a/src/api/background/handlers/alarms/ao-yield-agent/ao-yield-agent-alarm.handler.ts b/src/api/background/handlers/alarms/ao-yield-agent/ao-yield-agent-alarm.handler.ts index f6f91786c..61f4f5769 100644 --- a/src/api/background/handlers/alarms/ao-yield-agent/ao-yield-agent-alarm.handler.ts +++ b/src/api/background/handlers/alarms/ao-yield-agent/ao-yield-agent-alarm.handler.ts @@ -1,11 +1,7 @@ import type { Alarms } from "webextension-polyfill"; -import { - AO_YIELD_AGENT_ALARM_NAME, - AO_YIELD_AGENT_RECENT_TXS_CHECK_ALARM_NAME, - AO_YIELD_AGENT_SYNC_ALARM_NAME_PREFIX, -} from "~utils/agents/constants"; +import { AO_YIELD_AGENT_ALARM_NAME, AO_YIELD_AGENT_RECENT_TXS_CHECK_ALARM_NAME } from "~utils/agents/constants"; import { checkIfRecentTxSwapSucceeded, executeAutomaticSwapIfNeeded } from "~utils/agents/swap"; -import { checkAndSyncAgents } from "~utils/agents/sync"; +import { agentSyncManager } from "~utils/agents/sync"; /** * Alarm handler for executing automatic token swaps via AO yield agent. @@ -31,11 +27,8 @@ export async function handleAOYieldAgentRecentTxsCheck(alarmInfo?: Alarms.Alarm) /** * Alarm handler for syncing agents. - * Checks if any agents need to be synced and syncs them if conditions are met. */ export async function handleAOYieldAgentSync(alarmInfo?: Alarms.Alarm) { - if (!alarmInfo?.name.startsWith(AO_YIELD_AGENT_SYNC_ALARM_NAME_PREFIX)) return; - - const address = alarmInfo.name.replace(AO_YIELD_AGENT_SYNC_ALARM_NAME_PREFIX, ""); - await checkAndSyncAgents(address); + if (!alarmInfo?.name) return; + await agentSyncManager.handleAlarm(alarmInfo.name); } diff --git a/src/constants/api.ts b/src/constants/api.ts index 9a0573fe5..30f38fb4e 100644 --- a/src/constants/api.ts +++ b/src/constants/api.ts @@ -1 +1,3 @@ export const CACHE_API = "https://cache.wander.app"; +export const WNDR_HB_NODE = "https://hyperbeam.ar"; +export const FWD_HB_NODE = "https://forward.computer"; diff --git a/src/routes/popup/agents/ao-yield/confirm-agent.tsx b/src/routes/popup/agents/ao-yield/confirm-agent.tsx index 0304a966c..5100f3e4a 100644 --- a/src/routes/popup/agents/ao-yield/confirm-agent.tsx +++ b/src/routes/popup/agents/ao-yield/confirm-agent.tsx @@ -22,7 +22,7 @@ import { getAOYieldAgents, setAOYieldAgents } from "~utils/agents/utils"; import { EventType, PageType, trackEvent, trackPage } from "~utils/analytics"; import { scheduleSwapExecution } from "~utils/agents/swap"; import { AGENT_VERSION } from "~utils/agents/constants"; -import { useAOYieldAgentProperties } from "~utils/agents/hooks"; +import { useAOYieldAgentProperties, useHasActiveAOYieldAgent } from "~utils/agents/hooks"; import { useDefiFeeDetails } from "~utils/tier/hooks"; import { useAoRateLimitedToast } from "~utils/toast/toast.hooks"; @@ -34,6 +34,7 @@ export function ConfirmAOYieldAgentView() { const passwordInput = useInput(); const theme = useTheme(); const [aoYieldAgent] = useStorage({ key: "ao-yield-agent", instance: TempTransactionStorage }); + const hasActiveAOYieldAgent = useHasActiveAOYieldAgent(); const defiFeeDetails = useDefiFeeDetails(); const { showAoRateLimitedToast } = useAoRateLimitedToast(); const [transferRequirePassword] = useStorage( @@ -99,8 +100,16 @@ export function ConfirmAOYieldAgentView() { name: "Slippage", value: aoYieldAgent.slippage.toString(), }, + { + name: "Agent-Version", + value: AGENT_VERSION, + }, ], forceSpawn: true, + retry: { + count: 3, + delay: 1000, + }, }); const activeAddress = await getActiveAddress(); @@ -116,6 +125,7 @@ export function ConfirmAOYieldAgentView() { runIndefinitely: aoYieldAgent.runIndefinitely, slippage: aoYieldAgent.slippage, version: AGENT_VERSION, + createdAt: Date.now(), }); await setAOYieldAgents(activeAddress, agents); @@ -214,7 +224,11 @@ export function ConfirmAOYieldAgentView() { )} - diff --git a/src/routes/popup/agents/ao-yield/edit-agent.tsx b/src/routes/popup/agents/ao-yield/edit-agent.tsx index 6e8a2cbfb..f8a7803b4 100644 --- a/src/routes/popup/agents/ao-yield/edit-agent.tsx +++ b/src/routes/popup/agents/ao-yield/edit-agent.tsx @@ -9,7 +9,7 @@ import { InputButton } from "~components/common/InputButton"; import { HorizontalLine } from "~components/HorizontalLine"; import { AssetSelectorModal } from "../components/ao-yield/AssetSelectorModal"; import { DateSelectorModal } from "../components/ao-yield/DateSelectorModal"; -import type { Asset } from "~utils/agents/types"; +import type { AOYieldAgent, Asset } from "~utils/agents/types"; import { assets, formatDate, getAOYieldActiveAgent, updateAOYieldAgent } from "~utils/agents/utils"; import { trackPage, PageType } from "~utils/analytics"; import { SlippageInputButton } from "~routes/popup/swap/components/SlippageInputButton"; @@ -18,6 +18,7 @@ import { useAoRateLimitedToast } from "~utils/toast/toast.hooks"; export function EditAOYieldAgentView() { const theme = useTheme(); const toasts = useToasts(); + const [agent, setAgent] = useState(null); const { showAoRateLimitedToast } = useAoRateLimitedToast(); const [isLoading, setIsLoading] = useState(false); const [agentId, setAgentId] = useState(null); @@ -57,17 +58,31 @@ export function EditAOYieldAgentView() { }; async function handleSave() { - if (!agentId) return; + if (!agentId || !agent) return; try { setIsLoading(true); - await updateAOYieldAgent(agentId, { - slippage: selectedSlippage, - tokenOut: selectedAsset.id, - startDate: startDate.getTime(), - endDate: endDate.getTime(), - runIndefinitely, - }); + const updateData: Partial = { + ...(selectedSlippage !== agent.slippage && { slippage: selectedSlippage }), + ...(selectedAsset.id !== agent.tokenOut && { tokenOut: selectedAsset.id }), + ...(startDate.getTime() !== agent.startDate && { startDate: startDate.getTime() }), + ...(endDate.getTime() !== agent.endDate && { endDate: endDate.getTime() }), + ...(runIndefinitely !== agent.runIndefinitely && { runIndefinitely }), + }; + + if (!Object.keys(updateData).length) { + toasts.setToast({ + content: browser.i18n.getMessage("no_changes_to_save"), + type: "info", + duration: 2400, + }); + return; + } + + await updateAOYieldAgent(agentId, updateData); + + const currentAgent = await getAOYieldActiveAgent(); + setAgent(currentAgent); toasts.setToast({ content: browser.i18n.getMessage("agent_updated"), @@ -99,6 +114,7 @@ export function EditAOYieldAgentView() { useEffect(() => { getAOYieldActiveAgent().then((agent) => { if (agent) { + setAgent(agent); const asset = assets.find((asset) => asset.id === agent.tokenOut); setAgentId(agent.id); setSelectedAsset(asset || assets[0]); diff --git a/src/routes/popup/agents/components/AOYieldAgentListItem.tsx b/src/routes/popup/agents/components/AOYieldAgentListItem.tsx index 6f11fb8cd..01c67b29e 100644 --- a/src/routes/popup/agents/components/AOYieldAgentListItem.tsx +++ b/src/routes/popup/agents/components/AOYieldAgentListItem.tsx @@ -63,7 +63,7 @@ const AOYieldAgentCreateListItem = () => { const AOYieldAgentActiveListItem = ({ aoAgent, isHistory }: AOYieldAgentListItemProps) => { const { navigate } = useLocation(); const { data: mintingStatus } = useAOMintingStatus(); - const { data: agentInfo } = useAOYieldAgentInfo(aoAgent?.id); + const { data: agentInfo } = useAOYieldAgentInfo(aoAgent?.id, aoAgent?.version); const theme = useTheme(); useAsyncEffect(async () => { diff --git a/src/routes/popup/agents/components/ao-yield/AgentUpdateModal.tsx b/src/routes/popup/agents/components/ao-yield/AgentUpdateModal.tsx new file mode 100644 index 000000000..9bee3ae84 --- /dev/null +++ b/src/routes/popup/agents/components/ao-yield/AgentUpdateModal.tsx @@ -0,0 +1,84 @@ +import browser from "webextension-polyfill"; +import { Flex } from "~components/common/Flex"; +import SliderMenu from "~components/SliderMenu"; +import { Button, Text, useToasts } from "@arconnect/components-rebrand"; +import { useState } from "react"; +import HedgehogHeadIcon from "url:/assets/agents/images/hedgehog-head.svg"; +import { deployContract } from "~utils/agents/deploy"; +import aoYieldAgentContract from "raw:/assets/agents/contracts/ao-yield-agent.lua"; +import { AGENT_VERSION } from "~utils/agents/constants"; +import { updateAOYieldAgent } from "~utils/agents/utils"; +import { useAoRateLimitedToast } from "~utils/toast/toast.hooks"; + +interface AgentUpdateModalProps { + open: boolean; + onClose: () => void; + agentId: string; +} + +export function AgentUpdateModal({ open, onClose, agentId }: AgentUpdateModalProps) { + const [isLoading, setIsLoading] = useState(false); + const toasts = useToasts(); + const { showAoRateLimitedToast } = useAoRateLimitedToast(); + + async function handleUpdate(e: React.FormEvent) { + e.preventDefault(); + + if (!agentId) return; + + setIsLoading(true); + try { + // Update agent code to the latest version + await deployContract({ + name: "ao-yield-agent", + contractPath: aoYieldAgentContract, + processId: agentId, + retry: { + count: 3, + delay: 1000, + }, + }); + + // Update agent version to the latest version + await updateAOYieldAgent(agentId, { version: AGENT_VERSION, fullPatch: true }); + + toasts.setToast({ + content: browser.i18n.getMessage("success_updating_agent"), + type: "success", + duration: 2400, + }); + onClose(); + } catch (error) { + toasts.setToast({ + content: browser.i18n.getMessage("error_updating_agent"), + type: "error", + duration: 2400, + }); + showAoRateLimitedToast(error); + } finally { + setIsLoading(false); + } + } + + return ( + +
+ + + Hedgehog Head + + {browser.i18n.getMessage("agent_update_title")} + + + {browser.i18n.getMessage("agent_update_description")} + + + + + +
+
+ ); +} diff --git a/src/routes/popup/agents/components/ao-yield/SlippageSelectorModal.tsx b/src/routes/popup/agents/components/ao-yield/SlippageSelectorModal.tsx index ee63fce95..013adf15e 100644 --- a/src/routes/popup/agents/components/ao-yield/SlippageSelectorModal.tsx +++ b/src/routes/popup/agents/components/ao-yield/SlippageSelectorModal.tsx @@ -1,6 +1,6 @@ import browser from "webextension-polyfill"; import { Flex } from "~components/common/Flex"; -import { useState, useRef, useCallback } from "react"; +import { useState, useRef, useCallback, useEffect } from "react"; import SliderMenu from "~components/SliderMenu"; import { Button, Text, useToasts } from "@arconnect/components-rebrand"; import { AlertTriangle } from "@untitled-ui/icons-react"; @@ -117,6 +117,10 @@ export function SlippageSelectorModal({ onClose(); }; + useEffect(() => { + setSelectedSlippage(slippage); + }, [slippage]); + return ( diff --git a/src/routes/popup/agents/components/ao-yield/agent-info.tsx b/src/routes/popup/agents/components/ao-yield/agent-info.tsx index 7204bb1df..c1dba6e5a 100644 --- a/src/routes/popup/agents/components/ao-yield/agent-info.tsx +++ b/src/routes/popup/agents/components/ao-yield/agent-info.tsx @@ -4,7 +4,7 @@ import browser from "webextension-polyfill"; import HeadV2 from "~components/popup/HeadV2"; import { Flex } from "~components/common/Flex"; import { PropertyName, PropertyValue, TransactionProperty } from "~routes/popup/transaction/[id]"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useAOYieldAgent, useAOYieldAgentInfo, useAOYieldAgentProperties } from "~utils/agents/hooks"; import { Divider } from "~components/Divider"; import { RemoveButton } from "~routes/popup/settings/wallets/[address]"; @@ -14,16 +14,10 @@ import { Settings01 } from "@untitled-ui/icons-react"; import { PopupPaths } from "~wallets/router/popup/popup.routes"; import { useLocation } from "~wallets/router/router.utils"; import { AgentCancelModal } from "./AgentCancelModal"; -import { - assets, - formatTokenQuantity, - getStatusColor, - getStatusText, - updateLocalAOYieldAgent, -} from "~utils/agents/utils"; +import { assets, formatTokenQuantity, getStatusColor, getStatusText, isVersionGte } from "~utils/agents/utils"; import type { MintingStatus } from "~utils/agents/types"; -import { useAsyncEffect } from "~utils/react/useAsyncEffect"; -import { EventType, trackEvent } from "~utils/analytics"; +import { AgentUpdateModal } from "./AgentUpdateModal"; +import { AGENT_VERSION } from "~utils/agents/constants"; interface AgentInfoProps { agentId: string; @@ -34,6 +28,7 @@ interface AgentInfoProps { export function AgentInfo({ agentId, headerTitle, mintingStatus, isHistory = false }: AgentInfoProps) { const [showCancelModal, setShowCancelModal] = useState(false); + const [showUpdateModal, setShowUpdateModal] = useState(false); const agent = useAOYieldAgent(agentId); const { data: agentInfo } = useAOYieldAgentInfo(agentId); const { navigate, previousLocation } = useLocation(); @@ -94,21 +89,11 @@ export function AgentInfo({ agentId, headerTitle, mintingStatus, isHistory = fal setShowCancelModal(true); } - useAsyncEffect(async () => { - if (!agent || !agentInfo) return; - - try { - if (agent.status === "Active" && agent.status !== agentInfo.status) { - const isCompleted = agent.status === "Active" && agentInfo.status === "Completed"; - const updated = await updateLocalAOYieldAgent(agentId, { status: agentInfo.status }); - if (updated && isCompleted) { - await trackEvent(EventType.AO_YIELD_AGENT_END, {}); - } - } - } catch (error) { - console.error("Error updating AO Yield Agent status", error); + useEffect(() => { + if (agent && agent.status === "Active" && !isVersionGte(agent.version, AGENT_VERSION)) { + setShowUpdateModal(true); } - }, [agent, agentInfo]); + }, [agent]); return ( <> @@ -219,6 +204,7 @@ export function AgentInfo({ agentId, headerTitle, mintingStatus, isHistory = fal )} setShowCancelModal(false)} agentId={agentId} /> + setShowUpdateModal(false)} agentId={agentId} /> ); } diff --git a/src/routes/popup/agents/index.tsx b/src/routes/popup/agents/index.tsx index 79e6f87ed..e355a879d 100644 --- a/src/routes/popup/agents/index.tsx +++ b/src/routes/popup/agents/index.tsx @@ -4,25 +4,35 @@ import browser from "webextension-polyfill"; import HeadV2 from "~components/popup/HeadV2"; import HedgehogHeadIcon from "url:/assets/agents/images/hedgehog-head.svg"; import { Flex } from "~components/common/Flex"; -import { ClockRewind } from "@untitled-ui/icons-react"; +import { ClockRewind, RefreshCw01 } from "@untitled-ui/icons-react"; import WanderAgentExplainerPopup from "./components/WanderAgentExplainerPopup"; -import { useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { ExtensionStorage, TempTransactionStorage } from "~utils/storage"; import { AOYieldAgentListItem } from "./components/AOYieldAgentListItem"; import { LiquidOpsAgentListItem } from "./components/LiquidOpsAgentListItem"; import { useLocation } from "~wallets/router/router.utils"; import { PopupPaths } from "~wallets/router/popup/popup.routes"; import { AOMintingPausedListItem } from "./components/AOMintingPausedListItem"; -import { useAOYieldLatestAgent } from "~utils/agents/hooks"; +import { useAOYieldAgentInfo, useAOYieldLatestAgent, useSyncAOYieldAgent } from "~utils/agents/hooks"; import { PageType, trackPage } from "~utils/analytics"; import { useActiveTokens } from "./liquidops/utils/hooks/useAvailableTokens"; import { HAS_SHOWN_AGENTS_EXPLAINER_POPUP } from "~utils/agents/constants"; +import { agentSyncManager } from "~utils/agents/sync"; +import { useActiveAddress } from "~wallets/hooks"; export function AgentsView() { const { navigate } = useLocation(); const [open, setOpen] = useState(false); const aoAgent = useAOYieldLatestAgent(); - const { data: activeTokens } = useActiveTokens(); + const { data: aoAgentInfo } = useAOYieldAgentInfo(aoAgent?.id); + const { + data: activeTokens, + refetch: refetchActiveLiquidOpsTokens, + isLoading: isLoadingActiveLiquidOpsTokens, + isRefetching: isRefetchingActiveLiquidOpsTokens, + } = useActiveTokens(); + const activeAddress = useActiveAddress(); + const [isRefreshing, setIsRefreshing] = useState(false); const isAgentAvailable = useMemo( () => activeTokens?.length > 0 || aoAgent?.status === "Active", @@ -37,6 +47,33 @@ export function AgentsView() { } } + const handleRefresh = useCallback(async () => { + if (!activeAddress || isRefreshing) return; + + setIsRefreshing(true); + try { + const refreshPromises = [agentSyncManager.manualAgentsSync([activeAddress])]; + + if (!isLoadingActiveLiquidOpsTokens && !isRefetchingActiveLiquidOpsTokens) { + // @ts-ignore + refreshPromises.push(refetchActiveLiquidOpsTokens()); + } + + await Promise.allSettled(refreshPromises); + } finally { + setIsRefreshing(false); + } + }, [ + activeAddress, + isRefreshing, + isLoadingActiveLiquidOpsTokens, + isRefetchingActiveLiquidOpsTokens, + refetchActiveLiquidOpsTokens, + ]); + + // Sync agent data with agent info + useSyncAOYieldAgent(aoAgent, aoAgentInfo); + useEffect(() => { checkAndShowAgentExplainerPopup(); TempTransactionStorage.remove("ao-yield-agent"); @@ -46,7 +83,19 @@ export function AgentsView() { return ( <> + {browser.i18n.getMessage("agents")} + + + + + } backIcon={} back={() => navigate(PopupPaths.AOYieldAgentHistory)} /> @@ -85,3 +134,48 @@ const Wrapper = styled(Section)` overflow-y: auto; padding-bottom: 100px; `; + +const RefreshButton = styled.button` + background: transparent; + border: none; + cursor: pointer; + padding: 4px; + display: flex; + align-items: center; + justify-content: center; + transition: + opacity 0.2s, + transform 0.2s; + + &:hover:not(:disabled) { + opacity: 0.7; + transform: scale(1.05); + } + + &:active:not(:disabled) { + transform: scale(0.95); + } + + &:disabled { + cursor: not-allowed; + opacity: 0.5; + } + + svg { + color: ${(props) => props.theme.theme}; + } + + @keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } + } +`; + +const PageTitle = styled(Text).attrs({ noMargin: true })` + font-size: 1.375rem; + font-weight: 500; +`; diff --git a/src/tokens/aoTokens/ao.ts b/src/tokens/aoTokens/ao.ts index cbd9bf524..7abca2d22 100644 --- a/src/tokens/aoTokens/ao.ts +++ b/src/tokens/aoTokens/ao.ts @@ -61,7 +61,7 @@ export const DEFAULT_CU_URL = "https://cu.ao-testnet.xyz"; const { dryrun: arDriveDryrun } = connect({ CU_URL: ARDRIVE_CU_URL }); const { dryrun: aoDevDryrun } = connect({ CU_URL: AO_DEV_CU_URL }); -const ARDRIVE_PROCESSES = [ARIO_MAINNET_PROCESS_ID, ARIO_TESTNET_PROCESS_ID, WNDR_PROCESS_ID, USDA_PROCESS_ID]; +const ARDRIVE_PROCESSES = [ARIO_MAINNET_PROCESS_ID, ARIO_TESTNET_PROCESS_ID, USDA_PROCESS_ID]; export const getDryrunForProcess = (processId: string) => { if (processId === UTD_PROCESS_ID) return { dryrunFn: aoDevDryrun, isCustomDryrun: true }; @@ -97,11 +97,32 @@ export function getTokenInfoFromData(res: any, id: string): TokenInfo { } } catch {} } - const Ticker = getTagValue("Ticker", msg.Tags); - const Name = getTagValue("Name", msg.Tags); - const Denomination = getTagValue("Denomination", msg.Tags); - const Logo = getTagValue("Logo", msg.Tags); - const Transferable = getTagValue("Transferable", msg.Tags); + + let Ticker: string, Name: string, Denomination: string, Logo: string, Transferable: string; + + for (let i = 0; i < msg.Tags.length; i++) { + const tag = msg.Tags[i]; + const name = tag.name.toLowerCase(); + const value = tag.value; + + switch (name) { + case "ticker": + Ticker ??= value; + break; + case "name": + Name ??= value; + break; + case "denomination": + Denomination ??= value; + break; + case "logo": + Logo ??= value; + break; + case "transferable": + Transferable ??= value; + break; + } + } if (!Ticker && !Name) continue; diff --git a/src/utils/agents/constants.ts b/src/utils/agents/constants.ts index fe1c3fd2f..f43e16f6c 100644 --- a/src/utils/agents/constants.ts +++ b/src/utils/agents/constants.ts @@ -24,4 +24,4 @@ export const AO_YIELD_AGENT_RECENT_TXS_CHECK_ALARM_NAME = "ao-yield-agent-recent export const AO_YIELD_AGENT_SYNC_ALARM_NAME_PREFIX = "ao-yield-agent-sync-alarm-"; export const AO_YIELD_AGENT_SYNC_STATUS_PREFIX_KEY = "ao-yield-agent-sync-status-"; -export const AGENT_VERSION = "1.0.0"; +export const AGENT_VERSION = "1.0.2"; diff --git a/src/utils/agents/deploy.ts b/src/utils/agents/deploy.ts index 807788f7f..2c6fcab87 100644 --- a/src/utils/agents/deploy.ts +++ b/src/utils/agents/deploy.ts @@ -9,6 +9,7 @@ import { retryWithDelay } from "~utils/promises/retry"; import { AOS_QUERY } from "./queries"; import { log, LOG_GROUP } from "~utils/log/log.utils"; import { isURL } from "~utils/urls/isURL"; +import { wndrAoInstance } from "~utils/aoconnect"; /** * Manages deployments of contracts to AO. @@ -215,11 +216,13 @@ export class DeploymentsManager { ); const { Output, Error: error } = await retryWithDelay( - async () => - aoInstance.result({ + async (attempt) => { + const aoInstanceToUse = attempt % 2 === 0 ? wndrAoInstance : aoInstance; + return aoInstanceToUse.result({ process: processId!, message: messageId, - }), + }); + }, retry.count, retry.delay, ); diff --git a/src/utils/agents/hooks.ts b/src/utils/agents/hooks.ts index 4ae4d83b0..3bfc7b51b 100644 --- a/src/utils/agents/hooks.ts +++ b/src/utils/agents/hooks.ts @@ -1,5 +1,14 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; -import { getAOYieldAgentInfo, getAOYieldAgents, getWanderFee, processTransactions, tokenIdInfoMap } from "./utils"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { + getAOYieldAgentInfo, + getAOYieldAgents, + getWanderFee, + isArweaveAddress, + isVersionGte, + processTransactions, + tokenIdInfoMap, + updateLocalAOYieldAgent, +} from "./utils"; import type { AOYieldAgent, AOYieldAgentCreate, @@ -21,6 +30,8 @@ import browser from "webextension-polyfill"; import type { DefiFeeDetails } from "~utils/tier/types"; import { useDelegationInfo } from "~utils/fair_launch/fair_launch.hooks"; import { useActiveAddress } from "~wallets/hooks"; +import { useAsyncEffect } from "~utils/react/useAsyncEffect"; +import { EventType, trackEvent } from "~utils/analytics"; interface UseAOYieldAgentsProps { status?: AOYieldAgentStatus; @@ -58,10 +69,30 @@ export function useAOYieldLatestAgent() { return useMemo(() => { if (!agents.length) return undefined; - return agents.find((agent) => agent.status === "Active") || agents[agents.length - 1]; + for (let i = agents.length - 1; i >= 0; i--) { + if (agents[i].status === "Active") { + return agents[i]; + } + } + + // If no active agents found, return the last one + return agents[agents.length - 1]; }, [agents]); } +export function useHasActiveAOYieldAgent() { + const [activeAddress] = useStorage({ key: "active_address", instance: ExtensionStorage }); + const [agents = []] = useStorage( + { + key: `ao_yield_agents_${activeAddress}`, + instance: ExtensionStorage, + }, + [], + ); + + return useMemo(() => agents.some((agent) => agent.status === "Active"), [agents]); +} + export function useAOYieldAgent(agentId: string, status?: AOYieldAgentStatus) { const [activeAddress] = useStorage({ key: "active_address", instance: ExtensionStorage }); const [agents] = useStorage( @@ -82,10 +113,19 @@ export function useAOYieldAgent(agentId: string, status?: AOYieldAgentStatus) { }, [agents, agentId, status]); } -export function useAOYieldAgentInfo(agentId: string) { +export function useAOYieldAgentInfo(agentId: string, currentAgentVersion?: string) { + const attemptRef = useRef(-1); + + useEffect(() => { + attemptRef.current = -1; + }, [agentId]); + return useQuery({ queryKey: ["ao-yield-agent-info", agentId], - queryFn: () => getAOYieldAgentInfo(agentId), + queryFn: () => { + attemptRef.current++; + return getAOYieldAgentInfo(agentId, currentAgentVersion, attemptRef.current); + }, enabled: !!agentId, refetchInterval: 60_000, staleTime: 60_000, @@ -276,3 +316,59 @@ export function useWanderFee() { ...defaultOptions, }); } + +// Field validation rules +const FIELD_VALIDATORS = { + startDate: (value: number) => !isNaN(value) && value > 0, + endDate: (value: number) => !isNaN(value) && value > 0, + totalTransactions: (value: number) => !isNaN(value) && value >= 0, + slippage: (value: number) => !isNaN(value) && value >= 0.5 && value <= 10, + conversionPercentage: (value: number) => !isNaN(value) && value > 0 && value <= 100, + runIndefinitely: (value: boolean) => typeof value === "boolean", + tokenOut: (value: string) => isArweaveAddress(value), + version: (value: string) => isVersionGte(value, "1.0.0"), + status: (value: string) => ["Active", "Cancelled", "Completed", "Paused"].includes(value), +} as const; + +export function useSyncAOYieldAgent(agent: AOYieldAgent | undefined, agentInfo: AOYieldAgentInfo | undefined) { + useAsyncEffect(async () => { + if (!agent || !agentInfo || agent?.status !== "Active") return; + + try { + const updateData: Partial = {}; + let hasChanges = false; + + // Validate and update status + if (agent.status === "Active" && agentInfo.status && agent.status !== agentInfo.status) { + updateData.status = agentInfo.status; + hasChanges = true; + + if (agentInfo.status === "Completed") { + trackEvent(EventType.AO_YIELD_AGENT_END, {}); + } + } + + // Validate and update fields + for (const [field, validator] of Object.entries(FIELD_VALIDATORS)) { + const agentValue = agent[field as keyof AOYieldAgent]; + const agentInfoValue = agentInfo[field as keyof AOYieldAgentInfo]; + + if ( + agentInfoValue !== undefined && + agentInfoValue !== null && + agentValue !== agentInfoValue && + validator(agentInfoValue as never) + ) { + (updateData as any)[field] = agentInfoValue; + hasChanges = true; + } + } + + if (hasChanges) { + await updateLocalAOYieldAgent(agent.id, updateData); + } + } catch (error) { + console.error("Error updating AO Yield Agent", error); + } + }, [agent, agentInfo]); +} diff --git a/src/utils/agents/queries.ts b/src/utils/agents/queries.ts index 7bcd0ce8c..730c20a4d 100644 --- a/src/utils/agents/queries.ts +++ b/src/utils/agents/queries.ts @@ -127,6 +127,7 @@ query ($address: String!, $after: String) { first: 100, after: $after, owners: [$address], + sort: INGESTED_AT_DESC, tags: [ {name: "Data-Protocol", values: ["ao"]}, {name: "Type", values: ["Process"]}, diff --git a/src/utils/agents/swap.ts b/src/utils/agents/swap.ts index 67227bda7..128618c0c 100644 --- a/src/utils/agents/swap.ts +++ b/src/utils/agents/swap.ts @@ -473,9 +473,13 @@ async function processAgentSwap(agent: AOYieldAgent, walletAddress: string): Pro } // Fetch fresh agent info + let attempt = -1; const agentInfo = await queryClient.fetchQuery({ queryKey: ["ao-yield-agent-info", agent.id], - queryFn: () => getAOYieldAgentInfo(agent.id), + queryFn: () => { + attempt++; + return getAOYieldAgentInfo(agent.id, agent.version, attempt); + }, staleTime: 0, // Force fresh data gcTime: 0, retry: 3, diff --git a/src/utils/agents/sync.ts b/src/utils/agents/sync.ts index fa083f56e..5bd406f42 100644 --- a/src/utils/agents/sync.ts +++ b/src/utils/agents/sync.ts @@ -1,161 +1,439 @@ -import { gql, gqlAll } from "~gateways/api"; +import { gqlAll } from "~gateways/api"; import { AO_YIELD_AGENT_SYNC_QUERY } from "./queries"; import { getAOYieldAgentInfo, getAOYieldAgents, setAOYieldAgents, updateAOYieldAgent } from "./utils"; import { log, LOG_GROUP } from "~utils/log/log.utils"; -import { - AO_YIELD_AGENT_SYNC_ALARM_NAME_PREFIX, - AO_YIELD_AGENT_SYNC_STATUS_PREFIX_KEY, - HAS_SHOWN_AGENTS_EXPLAINER_POPUP, - SHOW_CREATE_WANDER_AGENT_CTA, -} from "./constants"; +import { HAS_SHOWN_AGENTS_EXPLAINER_POPUP, SHOW_CREATE_WANDER_AGENT_CTA } from "./constants"; import browser from "webextension-polyfill"; -import type { AOYieldAgent } from "./types"; +import type { AOYieldAgent, AOYieldAgentInfo } from "./types"; import { IS_EMBEDDED_APP } from "~utils/embedded/embedded.constants"; import { pLimit } from "plimit-lit"; import { ExtensionStorage } from "~utils/storage"; import { queryClient } from "~utils/tanstack"; -import { isWalletUnlocked } from "~wallets/auth"; +import { getTagValue } from "~tokens/aoTokens/ao"; +import { createStorageArray } from "~routes/popup/swap/utils/storage/storage.array"; -const limit = pLimit(10); +interface AgentSyncConfig { + retryAttempts: number; + retryDelayMs: number; + alarmName: string; +} -export async function checkAndSyncAgents(address: string): Promise { - try { - await ExtensionStorage.set(AO_YIELD_AGENT_SYNC_STATUS_PREFIX_KEY + address, { - status: "in_progress", - timestamp: Date.now(), - }); +type SyncTrigger = "alarm" | "manual"; - if (!address) { - log(LOG_GROUP.AGENTS, "No address provided"); - return; +const STORAGE_KEYS = { + SYNC_QUEUE: "ao_yield_agent_sync_queue", + ACTIVE_SYNCS: "ao_yield_agent_active_syncs", +}; + +class AgentSyncManager { + private static instance: AgentSyncManager | null = null; + private syncQueue = createStorageArray(STORAGE_KEYS.SYNC_QUEUE, { preventDuplicates: true }); + private activeSyncs = createStorageArray(STORAGE_KEYS.ACTIVE_SYNCS, { preventDuplicates: true }); + private isProcessingQueue = false; + private config: AgentSyncConfig; + private limit = pLimit(10); + private isInitialized = false; + + private constructor() { + this.config = { + retryAttempts: 3, + retryDelayMs: 2000, + alarmName: "ao_yield_agent_sync_alarm", + }; + } + + public static getInstance(): AgentSyncManager { + if (!AgentSyncManager.instance) { + AgentSyncManager.instance = new AgentSyncManager(); + } + return AgentSyncManager.instance; + } + + public async initialize(): Promise { + if (this.isInitialized) return; + + log(LOG_GROUP.AGENTS_SYNC, "Initializing AgentSyncManager..."); + + const queueSize = await this.syncQueue.length(); + const activeSyncsSize = await this.activeSyncs.length(); + + // Check if alarm exists - if not, no sync is scheduled/running, safe to clear stale active syncs + const existingAlarm = await browser.alarms.get(this.config.alarmName); + + if (!existingAlarm && !this.isProcessingQueue) { + // No alarm and no local processing - clean up stale active syncs + if (activeSyncsSize > 0) { + log(LOG_GROUP.AGENTS_SYNC, `Clearing ${activeSyncsSize} stale active syncs (no alarm, no processing)`); + await this.activeSyncs.clear(); + } + log(LOG_GROUP.AGENTS_SYNC, "No alarm found and no sync is in progress, cleared stale state"); } - log(LOG_GROUP.AGENTS, "Checking and syncing agents for: ", address); + // Log restored addresses if any + if (queueSize > 0) { + log(LOG_GROUP.AGENTS_SYNC, `Restored ${queueSize} addresses from persistent queue`); + } + if (activeSyncsSize > 0) { + log(LOG_GROUP.AGENTS_SYNC, `Restored ${activeSyncsSize} active syncs from storage`); + } + + this.isInitialized = true; + log(LOG_GROUP.AGENTS_SYNC, "Initialization complete"); + } + + // Entry point for scheduling agent sync (called when adding/importing wallets) + public async scheduleAgentsSync(addresses: string[]): Promise { + if (IS_EMBEDDED_APP) return; + + await this.initialize(); // Ensure initialized + + log(LOG_GROUP.AGENTS_SYNC, `Scheduling sync for ${addresses.length} addresses:`, addresses); + + // Add addresses to sync queue (automatically persists) + const initialSize = await this.syncQueue.length(); + const validAddresses = addresses.filter((addr) => addr && addr.trim()).map((addr) => addr.trim()); + await this.syncQueue.push(...validAddresses); + + const newSize = await this.syncQueue.length(); + const newAddresses = newSize - initialSize; + log(LOG_GROUP.AGENTS_SYNC, `Added ${newAddresses} new addresses. Queue size: ${newSize}`); + + // Setup instant one-time alarm (fires immediately, works even if extension closes) + if (newSize > 0 && !this.isProcessingQueue) { + await this.setupAlarm(); + } + } + + // Process the sync queue + private async processQueueIfNeeded(trigger: SyncTrigger): Promise { + const queueSize = await this.syncQueue.length(); + + if (this.isProcessingQueue) { + log(LOG_GROUP.AGENTS_SYNC, `Skipping ${trigger} sync - another sync is already in progress`); + return; + } - let agents = await getAOYieldAgents(address); - if (agents.length > 0) { - log(LOG_GROUP.AGENTS, "Agents already present, no need to sync"); + if (queueSize === 0) { + log(LOG_GROUP.AGENTS_SYNC, `Skipping ${trigger} sync - queue is empty`); return; } - const edges = await gqlAll(AO_YIELD_AGENT_SYNC_QUERY, { address }); + // Set local processing flag + this.isProcessingQueue = true; + + try { + log(LOG_GROUP.AGENTS_SYNC, `Processing queue (${queueSize} addresses) - trigger: ${trigger}`); + + const addressesToProcess = await this.syncQueue.getAll(); + const results = await Promise.allSettled(addressesToProcess.map((address) => this.syncAgentsForAddress(address))); + + // Process results and remove successful syncs from queue + let successCount = 0; + for (let i = 0; i < results.length; i++) { + const result = results[i]; + const address = addressesToProcess[i]; + if (result.status === "fulfilled") { + await this.syncQueue.removeWhere((addr) => addr === address); + successCount++; + log(LOG_GROUP.AGENTS_SYNC, `Successfully synced agents for address: ${address}`); + } else { + log(LOG_GROUP.AGENTS_SYNC, `Failed to sync agents for address: ${address}`, result.reason); + } + } + + const remainingSize = await this.syncQueue.length(); + log(LOG_GROUP.AGENTS_SYNC, `Queue processing complete. Synced: ${successCount}, Remaining: ${remainingSize}`); - if (edges.length === 0) { - log(LOG_GROUP.AGENTS, "No agents found"); + // Clear queue and alarm if empty + if (remainingSize === 0) { + await this.clearAlarm(); + await this.syncQueue.clear(); + log(LOG_GROUP.AGENTS_SYNC, "Queue empty - cleared queue and alarm"); + } + } catch (error) { + log(LOG_GROUP.AGENTS_SYNC, "Error processing queue:", error); + } finally { + this.isProcessingQueue = false; + } + } + + // Core sync logic for a specific address + private async syncAgentsForAddress(address: string): Promise { + // Check if already syncing this address + const isAlreadySyncing = await this.activeSyncs.includes(address); + if (isAlreadySyncing) { + log(LOG_GROUP.AGENTS_SYNC, `Skipping sync for ${address} - already in progress`); return; } - // sort edges by timestamp in ascending order - const sortedEdges = edges.sort((a, b) => { - const aDate = new Date(a.node.block?.timestamp ? a.node.block.timestamp * 1000 : Date.now()); - const bDate = new Date(b.node.block?.timestamp ? b.node.block.timestamp * 1000 : Date.now()); - return aDate.getTime() - bDate.getTime(); - }); + // Mark address as being synced (automatically persisted) + await this.activeSyncs.push(address); - const agentIds = sortedEdges.map((edge) => edge.node.id); + try { + log(LOG_GROUP.AGENTS_SYNC, `Syncing agents for address: ${address}`); - // Set extension storage values immediately since we know agents exist - await ExtensionStorage.set(HAS_SHOWN_AGENTS_EXPLAINER_POPUP, true); - await ExtensionStorage.set(SHOW_CREATE_WANDER_AGENT_CTA, false); + const currentAgents = await getAOYieldAgents(address); + const existingAgentIds = new Set(currentAgents.map((agent) => agent.id)); - // Read existing agents once and maintain ordered slots - const currentAgents = await getAOYieldAgents(address); - const agentSlots: (AOYieldAgent | null)[] = new Array(agentIds.length).fill(null); - let successCount = 0; + const edges = await gqlAll(AO_YIELD_AGENT_SYNC_QUERY, { address }); + const filteredEdges = edges.filter((edge) => !existingAgentIds.has(edge.node.id)); - const agentInfoPromises = agentIds.map((agentId, index) => - limit(async () => { - try { - log(LOG_GROUP.AGENTS, `Fetching agent info for ${agentId}`); - const agentInfo = await queryClient.fetchQuery({ - queryKey: ["ao-yield-agent-info", agentId], - queryFn: () => getAOYieldAgentInfo(agentId), - staleTime: 0, // Force fresh data - gcTime: 0, - retry: 1, - retryDelay: (attemptIndex: number) => Math.min(1000 * 2 ** attemptIndex, 30000), - }); - - if (!agentInfo) { - log(LOG_GROUP.AGENTS, `Agent info not found for ${agentId}`); - return null; - } + if (filteredEdges.length === 0) { + log(LOG_GROUP.AGENTS_SYNC, "No new agents found"); + return; + } + + // Sort edges by timestamp in ascending order + const sortedEdges = filteredEdges.sort((a, b) => { + const aDate = new Date(a.node.block?.timestamp ? a.node.block.timestamp * 1000 : Date.now()); + const bDate = new Date(b.node.block?.timestamp ? b.node.block.timestamp * 1000 : Date.now()); + return aDate.getTime() - bDate.getTime(); + }); + + const agentIds = sortedEdges.map((edge) => edge.node.id); + const foundAgents = sortedEdges.map((edge) => { + const agentVersion = getTagValue("Agent-Version", edge?.node?.tags) || "1.0.0"; + const createdAt = edge.node.block?.timestamp ? edge.node.block.timestamp * 1000 : Date.now(); + return { agentId: edge.node.id, agentVersion, createdAt }; + }); + + // Update feature flags + await this.updateFeatureFlags(agentIds); + + const currentDate = Date.now(); + let successCount = 0; + + const agentInfoPromises = foundAgents.map(({ agentId, agentVersion, createdAt }) => + this.limit(async () => { + try { + log(LOG_GROUP.AGENTS_SYNC, `Fetching agent info for ${agentId}`); + let attempt = -1; + const agentInfo = await queryClient.fetchQuery({ + queryKey: ["ao-yield-agent-info", agentId], + queryFn: async (): Promise => { + attempt++; + return getAOYieldAgentInfo(agentId, agentVersion, attempt); + }, + staleTime: 0, // Force fresh data + gcTime: 0, + retry: this.config.retryAttempts, + retryDelay: (attemptIndex: number) => Math.min(1000 * 2 ** attemptIndex, 30000), + }); + + if ( + !agentInfo || + !agentInfo.startDate || + !agentInfo.endDate || + !agentInfo.status || + !agentInfo.conversionPercentage || + !agentInfo.tokenOut || + !agentInfo.slippage || + !agentInfo.version + ) { + log(LOG_GROUP.AGENTS_SYNC, `Agent info not found for ${agentId}`); + return null; + } + + log(LOG_GROUP.AGENTS_SYNC, `Agent info fetched for ${agentId}`); + + const agent: AOYieldAgent = { + id: agentId, + status: agentInfo.status, + conversionPercentage: agentInfo.conversionPercentage, + tokenOut: agentInfo.tokenOut, + startDate: agentInfo.startDate, + endDate: agentInfo.endDate, + runIndefinitely: agentInfo.runIndefinitely, + slippage: agentInfo.slippage, + version: agentInfo.version, + totalTransactions: agentInfo.totalTransactions ?? 0, + createdAt, + }; - log(LOG_GROUP.AGENTS, `Agent info fetched for ${agentId}`); + if (agent.status === "Active" && currentDate > agent.endDate) { + const newStatus = agent.endDate === agentInfo.swappedUpToDate ? "Completed" : "Cancelled"; + agent.status = newStatus; + updateAOYieldAgent(agentId, { status: newStatus }, true); + } - if (!agentInfo.agentVersion) { - log(LOG_GROUP.AGENTS, `Agent version not found for ${agentId}`); + // Progressive update: Add agent immediately and write to storage + const existingAgents = await getAOYieldAgents(address); + const agentsMap = new Map(); + + // Add existing agents + existingAgents.forEach((a) => agentsMap.set(a.id, a)); + + // Add/update new agent + agentsMap.set(agent.id, agent); + + // Sort and save + const updatedAgents = Array.from(agentsMap.values()); + updatedAgents.sort((a, b) => { + const diff = (a.createdAt || a.startDate) - (b.createdAt || b.startDate); + if (diff !== 0) return diff; + return Number(a.status === "Active") - Number(b.status === "Active"); + }); + await setAOYieldAgents(address, updatedAgents); + successCount++; + + log(LOG_GROUP.AGENTS_SYNC, `Agent ${agentId} added (${successCount}/${agentIds.length})`); + + return agent; + } catch (error) { + log(LOG_GROUP.AGENTS_SYNC, `Error fetching agent info for ${agentId}:`, error); return null; } + }), + ); - const agent: AOYieldAgent = { - id: agentId, - status: agentInfo.status, - conversionPercentage: agentInfo.conversionPercentage, - tokenOut: agentInfo.tokenOut, - startDate: agentInfo.startDate, - endDate: agentInfo.endDate, - runIndefinitely: agentInfo.runIndefinitely, - slippage: agentInfo.slippage, - version: agentInfo.agentVersion, - }; - - // Store agent in correct position and update storage with ordered agents - agentSlots[index] = agent; - const orderedNewAgents = agentSlots.filter((agent): agent is AOYieldAgent => agent !== null); - await setAOYieldAgents(address, [...currentAgents, ...orderedNewAgents]); - successCount++; - log(LOG_GROUP.AGENTS, `Agent ${agentId} added at position ${index} (${successCount}/${agentIds.length})`); + await Promise.allSettled(agentInfoPromises); - return agent; - } catch (error) { - log(LOG_GROUP.AGENTS, `Error fetching agent info for ${agentId}:`, error); - return null; - } - }), - ); + if (successCount === 0) { + log(LOG_GROUP.AGENTS_SYNC, "No valid agents were fetched successfully"); + return; + } - await Promise.allSettled(agentInfoPromises); + log(LOG_GROUP.AGENTS_SYNC, `Successfully synced ${successCount} agents for address: ${address}`); + } catch (error) { + log(LOG_GROUP.AGENTS_SYNC, `Error syncing agents for address ${address}:`, error); + throw error; + } finally { + // Always remove address from active syncs when done (automatically persisted) + await this.activeSyncs.removeWhere((addr) => addr === address); + log(LOG_GROUP.AGENTS_SYNC, `Sync completed for address: ${address}`); + } + } + // Update feature flags based on agents + private async updateFeatureFlags(agents: string[]): Promise { try { - log(LOG_GROUP.AGENTS, "Checking for expired agents"); - const aoAgents = await getAOYieldAgents(address); - const expiredAgents = aoAgents.filter((agent) => agent.status === "Active" && agent.endDate < Date.now()); - const expiredPromises = expiredAgents.map(async (agent) => { - const walletUnlocked = await isWalletUnlocked(); - if (walletUnlocked) { - await updateAOYieldAgent(agent.id, { status: "Completed" }); - } - }); + if (agents.length > 0) { + await ExtensionStorage.set(HAS_SHOWN_AGENTS_EXPLAINER_POPUP, true); + await ExtensionStorage.set(SHOW_CREATE_WANDER_AGENT_CTA, false); + } + } catch (error) { + log(LOG_GROUP.AGENTS_SYNC, "Error updating feature flags:", error); + } + } + + // Alarm management + private async setupAlarm(): Promise { + try { + const existingAlarm = await browser.alarms.get(this.config.alarmName); + if (existingAlarm) return; // Alarm already exists + + // Create alarm that fires immediately to check queue + browser.alarms.create(this.config.alarmName, { when: Date.now() }); - log(LOG_GROUP.AGENTS, `Updating ${expiredAgents.length} expired agents status to completed`); - await Promise.allSettled(expiredPromises); + log(LOG_GROUP.AGENTS_SYNC, "Setup immediate alarm for queue processing"); } catch (error) { - log(LOG_GROUP.AGENTS, "Error checking for expired agents: ", error); + log(LOG_GROUP.AGENTS_SYNC, "Error setting up alarm:", error); } + } - if (successCount > 0) { - log(LOG_GROUP.AGENTS, `Successfully synced ${successCount} agents progressively`); - } else { - log(LOG_GROUP.AGENTS, "No valid agents were fetched successfully"); + private async clearAlarm(): Promise { + try { + await browser.alarms.clear(this.config.alarmName); + log(LOG_GROUP.AGENTS_SYNC, "Cleared alarm"); + } catch (error) { + log(LOG_GROUP.AGENTS_SYNC, "Error clearing alarm:", error); } - } catch (error) { - log(LOG_GROUP.AGENTS, "Error checking and syncing agents: ", error); - } finally { - await ExtensionStorage.remove(AO_YIELD_AGENT_SYNC_STATUS_PREFIX_KEY + address); } -} -export async function scheduleAgentsSync(address: string) { - if (IS_EMBEDDED_APP) return; + // Handle alarm trigger (should be called from background script) + public async handleAlarm(alarmName: string): Promise { + if (alarmName !== this.config.alarmName) return; + + log(LOG_GROUP.AGENTS_SYNC, "Alarm triggered, processing queue"); + await this.initialize(); + await this.processQueueIfNeeded("alarm"); + } + + // Public utility methods + public async getQueueSize(): Promise { + return await this.syncQueue.length(); + } + + public async getQueueAddresses(): Promise { + return await this.syncQueue.getAll(); + } + + public isProcessing(): boolean { + return this.isProcessingQueue; + } + + public async isAddressSyncing(address: string): Promise { + return await this.activeSyncs.includes(address); + } + + public async getActiveSyncs(): Promise { + return await this.activeSyncs.getAll(); + } + + // Manual sync - immediate sync without setting up alarms + public async manualAgentsSync(addresses: string[]): Promise { + if (IS_EMBEDDED_APP) return; + + await this.initialize(); + + log(LOG_GROUP.AGENTS_SYNC, `Manual sync triggered for ${addresses.length} address(es): ${addresses.join(", ")}`); + + // Filter and validate addresses + const validAddresses = addresses.filter((addr) => addr && addr.trim()).map((addr) => addr.trim()); + + if (validAddresses.length === 0) { + log(LOG_GROUP.AGENTS_SYNC, "No valid addresses to sync manually"); + return; + } + + // Filter out addresses that are already being synced + const activeSyncsList = await this.activeSyncs.getAll(); + const activeSyncsSet = new Set(activeSyncsList); + const availableAddresses = validAddresses.filter((addr) => !activeSyncsSet.has(addr)); + const alreadySyncing = validAddresses.filter((addr) => activeSyncsSet.has(addr)); - try { - const alarmName = AO_YIELD_AGENT_SYNC_ALARM_NAME_PREFIX + address; - const alarms = await browser.alarms.get(alarmName); - if (alarms) return; + if (alreadySyncing.length > 0) { + log( + LOG_GROUP.AGENTS_SYNC, + `Skipping ${alreadySyncing.length} address(es) already syncing: ${alreadySyncing.join(", ")}`, + ); + } + + if (availableAddresses.length === 0) { + log(LOG_GROUP.AGENTS_SYNC, "All requested addresses are already syncing"); + return; + } + + const results = await Promise.allSettled(availableAddresses.map((address) => this.syncAgentsForAddress(address))); + + // Log results + let successCount = 0; + results.forEach((result, index) => { + const address = availableAddresses[index]; + if (result.status === "fulfilled") { + successCount++; + log(LOG_GROUP.AGENTS_SYNC, `Manual sync successful for address: ${address}`); + } else { + log(LOG_GROUP.AGENTS_SYNC, `Manual sync failed for address: ${address}`, result.reason); + } + }); + + log(LOG_GROUP.AGENTS_SYNC, `Manual sync complete. Success: ${successCount}/${availableAddresses.length}`); + } - browser.alarms.create(alarmName, { when: Date.now() }); - } catch (error) { - log(LOG_GROUP.AGENTS, "Error scheduling agents sync: ", error); + public async forceSync(): Promise { + const queueSize = await this.syncQueue.length(); + if (queueSize > 0) { + await this.processQueueIfNeeded("manual"); + } + } + + public async destroy(): Promise { + await this.syncQueue.clear(); + await this.activeSyncs.clear(); + this.isProcessingQueue = false; } } + +// Export singleton instance +export const agentSyncManager = AgentSyncManager.getInstance(); diff --git a/src/utils/agents/types.ts b/src/utils/agents/types.ts index 1562b210c..42099ba72 100644 --- a/src/utils/agents/types.ts +++ b/src/utils/agents/types.ts @@ -142,6 +142,7 @@ export interface AOYieldAgent { slippage: number; totalTransactions?: number; version: string; + createdAt?: number; } export interface AOYieldAgentInfo extends AOYieldAgent { @@ -153,7 +154,6 @@ export interface AOYieldAgentInfo extends AOYieldAgent { swapInProgress: boolean; processedUpToDate?: number; swappedUpToDate?: number; - agentVersion: string; } export interface AOYieldAgentCreate { diff --git a/src/utils/agents/utils/index.ts b/src/utils/agents/utils/index.ts index 50f1207f6..a9e61dabb 100644 --- a/src/utils/agents/utils/index.ts +++ b/src/utils/agents/utils/index.ts @@ -2,8 +2,6 @@ import Arweave from "arweave"; import { defaultGateway } from "~gateways/gateway"; import { ExtensionStorage } from "~utils/storage"; import type { AOYieldAgent, AOYieldAgentInfo, AOYieldAgentStatus, MintingStatus, RecentTx, Tag } from "../types"; -import { connect } from "@permaweb/aoconnect"; -import { defaultConfig } from "~tokens/aoTokens/config"; import { createDataItemSigner, getTagValue } from "~tokens/aoTokens/ao"; import { getActiveAddress, getActiveKeyfile } from "~wallets"; import { isLocalWallet } from "~utils/assertions"; @@ -22,6 +20,9 @@ import { isURL } from "~utils/urls/isURL"; import { queryClient } from "~utils/tanstack"; import { Mutex } from "~utils/mutex"; import { Id, Owner, WAR_PROCESS_ID, WUSDC_PROCESS_ID } from "~tokens/aoTokens/ao.constants"; +import { log, LOG_GROUP } from "~utils/log/log.utils"; +import { FWD_HB_NODE, WNDR_HB_NODE } from "~constants/api"; +import { aoInstance, wndrAoInstance } from "~utils/aoconnect"; const agentStorageMutex = new Mutex(); @@ -30,6 +31,33 @@ const agentStorageMutex = new Mutex(); */ export const arweave = Arweave.init(defaultGateway); +/** + * Checks if version a is greater than or equal to version b + * @param a - The first version to compare + * @param b - The second version to compare + * @returns True if version a is greater than or equal to version b, false otherwise + */ +export function isVersionGte(a: string, b: string): boolean { + try { + const pa = a.split(".").map(Number); + const pb = b.split(".").map(Number); + const len = Math.max(pa.length, pb.length); + + for (let i = 0; i < len; i++) { + const na = pa[i] ?? 0; + const nb = pb[i] ?? 0; + + if (na > nb) return true; + if (na < nb) return false; + } + + return true; // Equal versions + } catch (error) { + console.error("Error comparing versions: ", a, b, error); + return false; + } +} + /** * Parses a gateway URL and returns an object containing the host, port, and protocol. * @@ -232,65 +260,125 @@ export async function getAOYieldActiveAgent() { return agents[agents.length - 1]; } -export async function getAOYieldAgentInfo(agentId: string) { - const aoInstance = connect(defaultConfig); +export async function getAOYieldAgentInfo(agentId: string, currentAgentVersion: string = "1.0.0", attempt: number = 0) { + try { + if (!isVersionGte(currentAgentVersion, "1.0.2")) { + throw new Error("Agent version must be greater or equal to 1.0.2"); + } - const dryrunRes = await aoInstance.dryrun({ - Id, - Owner, - process: agentId, - tags: [{ name: "Action", value: "Info" }], - }); + log(LOG_GROUP.AGENTS, `Fetching agent info from the HB node with agent version: ${currentAgentVersion}`); + const hbNode = attempt % 2 === 0 ? WNDR_HB_NODE : FWD_HB_NODE; + const response = await fetch(`${hbNode}/${agentId}/~process@1.0/now/agent-info/~json@1.0/serialize?bundle`); + if (!response.ok) { + throw new Error("Failed to fetch agent info"); + } + const data = await response.json(); + + const dex = data.dex; + const status = data.status; + const tokenOut = data["token-out"] ?? data.tokenOut; + const conversionPercentage = data["conversion-percentage"] ?? data.conversionPercentage; + const startDate = data["start-date"] ?? data.startDate; + const endDate = data["end-date"] ?? data.endDate; + const runIndefinitely = data["run-indefinitely"] ?? data.runIndefinitely; + const slippage = data.slippage; + const totalAOSold = data["total-ao-sold"] ?? data.totalAOSold; + const totalBought = data["total-bought"] ?? data.totalBought; + const totalTransactions = data["total-transactions"] ?? data.totalTransactions; + const totalWanderFee = data["total-wander-fee"] ?? data.totalWanderFee; + const swapInProgress = data["swap-in-progress"] ?? data.swapInProgress; + const processedUpToDate = data["processed-up-to-date"] ?? data.processedUpToDate; + const swappedUpToDate = data["swapped-up-to-date"] ?? data.swappedUpToDate; + const agentVersion = data["agent-version"] ?? data.agentVersion; + + let totalBoughtObj = {}; + if (typeof totalBought === "object" && totalBought !== null) { + totalBoughtObj = totalBought; + } else { + try { + totalBoughtObj = JSON.parse(totalBought); + } catch {} + } + + return { + id: agentId, + status, + dex, + tokenOut, + conversionPercentage: Number(conversionPercentage), + startDate: Number(startDate), + endDate: Number(endDate), + runIndefinitely: runIndefinitely === "true", + slippage: Number(slippage), + totalAOSold, + totalBought: totalBoughtObj, + totalTransactions: Number(totalTransactions), + totalWanderFee, + swapInProgress: swapInProgress === "true", + processedUpToDate: processedUpToDate && processedUpToDate !== "nil" ? Number(processedUpToDate) : undefined, + swappedUpToDate: swappedUpToDate && swappedUpToDate !== "nil" ? Number(swappedUpToDate) : undefined, + version: agentVersion, + } as AOYieldAgentInfo; + } catch (error) { + log(LOG_GROUP.AGENTS, `Fetching agent info from the CU node with agent version: ${currentAgentVersion}`); + const aoInstanceToUse = attempt % 2 === 0 ? wndrAoInstance : aoInstance; + const dryrunRes = await aoInstanceToUse.dryrun({ + Id, + Owner, + process: agentId, + tags: [{ name: "Action", value: "Info" }], + }); - const message = dryrunRes.Messages?.[0]; - const tags = message?.Tags; - - const dex = getTagValue("Dex", tags); - const status = getTagValue("Status", tags); - const tokenOut = getTagValue("Token-Out", tags); - const conversionPercentage = getTagValue("Conversion-Percentage", tags); - const startDate = getTagValue("Start-Date", tags); - const endDate = getTagValue("End-Date", tags); - const runIndefinitely = getTagValue("Run-Indefinitely", tags); - const slippage = getTagValue("Slippage", tags); - const totalAOSold = getTagValue("Total-AO-Sold", tags); - const totalBought = getTagValue("Total-Bought", tags); - const totalTransactions = getTagValue("Total-Transactions", tags); - const totalWanderFee = getTagValue("Total-Wander-Fee", tags); - const swapInProgress = getTagValue("Swap-In-Progress", tags); - const processedUpToDate = getTagValue("Processed-Up-To-Date", tags); - const swappedUpToDate = getTagValue("Swapped-Up-To-Date", tags); - const agentVersion = getTagValue("Agent-Version", tags); + const message = dryrunRes.Messages?.[0]; + const tags = message?.Tags; + + const dex = getTagValue("Dex", tags); + const status = getTagValue("Status", tags); + const tokenOut = getTagValue("Token-Out", tags); + const conversionPercentage = getTagValue("Conversion-Percentage", tags); + const startDate = getTagValue("Start-Date", tags); + const endDate = getTagValue("End-Date", tags); + const runIndefinitely = getTagValue("Run-Indefinitely", tags); + const slippage = getTagValue("Slippage", tags); + const totalAOSold = getTagValue("Total-AO-Sold", tags); + const totalBought = getTagValue("Total-Bought", tags); + const totalTransactions = getTagValue("Total-Transactions", tags); + const totalWanderFee = getTagValue("Total-Wander-Fee", tags); + const swapInProgress = getTagValue("Swap-In-Progress", tags); + const processedUpToDate = getTagValue("Processed-Up-To-Date", tags); + const swappedUpToDate = getTagValue("Swapped-Up-To-Date", tags); + const agentVersion = getTagValue("Agent-Version", tags); - return { - id: agentId, - status, - dex, - tokenOut, - conversionPercentage: Number(conversionPercentage), - startDate: Number(startDate), - endDate: Number(endDate), - runIndefinitely: runIndefinitely === "true", - slippage: Number(slippage), - totalAOSold, - totalBought: JSON.parse(totalBought), - totalTransactions: Number(totalTransactions), - totalWanderFee, - swapInProgress: swapInProgress === "true", - processedUpToDate: processedUpToDate !== "nil" ? Number(processedUpToDate) : undefined, - swappedUpToDate: swappedUpToDate !== "nil" ? Number(swappedUpToDate) : undefined, - agentVersion, - } as AOYieldAgentInfo; + return { + id: agentId, + status, + dex, + tokenOut, + conversionPercentage: Number(conversionPercentage), + startDate: Number(startDate), + endDate: Number(endDate), + runIndefinitely: runIndefinitely === "true", + slippage: Number(slippage), + totalAOSold, + totalBought: JSON.parse(totalBought), + totalTransactions: Number(totalTransactions), + totalWanderFee, + swapInProgress: swapInProgress === "true", + processedUpToDate: processedUpToDate && processedUpToDate !== "nil" ? Number(processedUpToDate) : undefined, + swappedUpToDate: swappedUpToDate && swappedUpToDate !== "nil" ? Number(swappedUpToDate) : undefined, + version: agentVersion, + } as AOYieldAgentInfo; + } } /** * Builds tags array for agent update based on the update data */ -function buildUpdateTags(updateData: Partial): Tag[] { +function buildUpdateTags(updateData: Partial & { fullPatch?: boolean }): Tag[] { const tags: Tag[] = [{ name: "Action", value: "Update-Agent" }]; const tagMappings: Array<{ - key: keyof Partial; + key: keyof Partial | "fullPatch"; tagName: string; transform?: (value: any) => string; }> = [ @@ -299,6 +387,8 @@ function buildUpdateTags(updateData: Partial): Tag[] { { key: "startDate", tagName: "Start-Date", transform: (v) => v.toString() }, { key: "endDate", tagName: "End-Date", transform: (v) => v.toString() }, { key: "runIndefinitely", tagName: "Run-Indefinitely", transform: (v) => v.toString() }, + { key: "version", tagName: "Agent-Version" }, + { key: "fullPatch", tagName: "Full-Patch", transform: (v) => v.toString() }, { key: "status", tagName: "Status" }, ]; @@ -338,10 +428,12 @@ function updateAgentProperties(agent: AOYieldAgent, updateData: Partial) { +export async function updateAOYieldAgent( + agentId: string, + updateData: Partial & { fullPatch?: boolean }, + skipLocalUpdate: boolean = false, +) { try { - const aoInstance = connect(defaultConfig); - const decryptedWallet = await getActiveKeyfile(); isLocalWallet(decryptedWallet); const keyfile = decryptedWallet.keyfile; @@ -358,19 +450,26 @@ export async function updateAOYieldAgent(agentId: string, updateData: Partial ({ Error: undefined })); + const result = await retryWithDelay( + (attempt) => { + const aoInstanceToUse = attempt % 2 === 0 ? wndrAoInstance : aoInstance; + return aoInstanceToUse.result({ + process: agentId, + message: messageId, + }); + }, + 2, + 1000, + ).catch(() => ({ Error: undefined })); if (result.Error) { throw new Error(`Failed to update agent: ${result.Error}`); } - await updateLocalAOYieldAgent(agentId, updateData); - await queryClient.invalidateQueries({ queryKey: ["ao-yield-agent-info", agentId] }); + if (!skipLocalUpdate) { + await updateLocalAOYieldAgent(agentId, updateData); + await queryClient.invalidateQueries({ queryKey: ["ao-yield-agent-info", agentId] }); + } } catch (error) { throw new Error(`Failed to update AO Yield Agent: ${error instanceof Error ? error.message : String(error)}`); } @@ -447,8 +546,6 @@ export function formatDate(date: Date | null, fallbackLabel: string) { export async function getWanderFee() { const defaultFee = "0.25"; try { - const aoInstance = connect(defaultConfig); - const dryrunRes = await aoInstance.dryrun({ Id, Owner, diff --git a/src/utils/aoconnect.ts b/src/utils/aoconnect.ts new file mode 100644 index 000000000..d09aa1d21 --- /dev/null +++ b/src/utils/aoconnect.ts @@ -0,0 +1,11 @@ +import { connect } from "@permaweb/aoconnect"; +import { defaultConfig } from "~tokens/aoTokens/config"; + +const ARDRIVE_CU_URL = "https://cu.ardrive.io"; +const WNDR_CU_URL = "https://gateway.ar"; +export const DATAITEM_SIGNER_KIND = "ans104"; +export const HTTP_SIGNER_KIND = "httpsig"; + +export const aoInstance = connect(defaultConfig); +export const wndrAoInstance = connect({ CU_URL: WNDR_CU_URL }); +export const ardriveAoInstance = connect({ CU_URL: ARDRIVE_CU_URL }); diff --git a/src/utils/log/log.utils.ts b/src/utils/log/log.utils.ts index a9c537844..969d093e9 100644 --- a/src/utils/log/log.utils.ts +++ b/src/utils/log/log.utils.ts @@ -11,6 +11,7 @@ export enum LOG_GROUP { SESSION = "SESSION", STORAGE = "STORAGE", AGENTS = "AGENTS", + AGENTS_SYNC = "AGENTS_SYNC", TIERS = "TIERS", TRANSAK = "TRANSAK", FAIR_LAUNCH = "FAIR_LAUNCH", @@ -32,8 +33,9 @@ const LOG_GROUPS_ENABLED: Record = { [LOG_GROUP.WALLET_GENERATION]: false, [LOG_GROUP.SESSION]: false, [LOG_GROUP.STORAGE]: false, - [LOG_GROUP.AGENTS]: false, - [LOG_GROUP.TIERS]: false, + [LOG_GROUP.AGENTS]: process.env.NODE_ENV === "development", + [LOG_GROUP.AGENTS_SYNC]: process.env.NODE_ENV === "development", + [LOG_GROUP.TIERS]: process.env.NODE_ENV === "development", [LOG_GROUP.TRANSAK]: false, [LOG_GROUP.FAIR_LAUNCH]: false, [LOG_GROUP.TRANSACTIONS]: false, diff --git a/src/utils/tier/utils.ts b/src/utils/tier/utils.ts index a0dc6cd21..b0bd8b5bc 100644 --- a/src/utils/tier/utils.ts +++ b/src/utils/tier/utils.ts @@ -1,5 +1,5 @@ -import { dryrun, message } from "@permaweb/aoconnect/browser"; -import { createDataItemSigner, getTagValue } from "~tokens/aoTokens/ao"; +import { message } from "@permaweb/aoconnect/browser"; +import { createDataItemSigner } from "~tokens/aoTokens/ao"; import { tierIdToTierName, TIER_PROCESS_ID } from "./constants"; import type { ActiveTier, ActiveTierFromApi, DefiFeeDetails, Tier, WalletSavings } from "./types"; import { defiFeePercent, defiFeeReductionsInPercent } from "./constants"; @@ -10,8 +10,7 @@ import { isLocalWallet } from "~utils/assertions"; import { ExtensionStorage } from "~utils/storage"; import { scheduleRefreshWalletLifetimeSavings } from "./alarms"; import { retryWithDelay } from "~utils/promises/retry"; -import { Id } from "~tokens/aoTokens/ao.constants"; -import { CACHE_API } from "~constants/api"; +import { CACHE_API, WNDR_HB_NODE } from "~constants/api"; const ONE_HUNDRED = BigNumber(100); const THREE_HOURS_MS = 10_800_000; @@ -69,27 +68,51 @@ export async function getActiveTier(walletAddress: string, retry = false): Promi data = responseData; } catch { - const dryrunParams = { - Id, - Owner: walletAddress, - process: TIER_PROCESS_ID, - tags: [{ name: "Action", value: "Get-Wallet-Info" }], - }; + const url = `${WNDR_HB_NODE}/${TIER_PROCESS_ID}~process@1.0/now/wallets-tier-info/${walletAddress}/~json@1.0/serialize`; - const dryrunRes = retry + const response = retry ? await retryWithDelay( - () => dryrun(dryrunParams), + async () => { + const response = await fetch(url); + console.log(response.ok, response.status, typeof response.status); + if (!response.ok && response.status !== 404) { + throw new Error("Failed to fetch tier info from HB node"); + } + return response; + }, 3, 1000, (attempt) => Math.min(1000 * 2 ** attempt, 30000), ) - : await dryrun(dryrunParams); - - const message = dryrunRes.Messages?.[0]; - const parsedData = JSON.parse(message?.Data || "{}"); + : await fetch(url); + + let parsedData: ActiveTierFromApi; + + if (response.status === 404) { + const response = await fetch(`${WNDR_HB_NODE}/${TIER_PROCESS_ID}~process@1.0/now/tier-info/~json@1.0/serialize`); + const responseData = await response.json(); + parsedData = { + balance: "0", + progress: 0, + rank: "", + snapshotTimestamp: responseData["snapshot-timestamp"], + tier: 5, + totalHolders: responseData["total-holders"], + } as ActiveTierFromApi; + } else { + const responseData = await response.json(); + parsedData = { + balance: responseData.balance, + progress: responseData.progress, + rank: responseData.rank, + snapshotTimestamp: responseData["snapshot-timestamp"], + tier: responseData.tier, + totalHolders: responseData["total-holders"], + } as ActiveTierFromApi; + } if (!isValidTierInfo(parsedData)) { - throw new Error("Invalid tier info data from WNDR tier process"); + throw new Error("Invalid tier info data from HB node"); } data = parsedData; @@ -129,16 +152,14 @@ export async function getWalletLifetimeSavings(walletAddress: string): Promise