diff --git a/src/services/hypernativeApi.ts b/src/services/hypernativeApi.ts index c79b911..d5e74d4 100644 --- a/src/services/hypernativeApi.ts +++ b/src/services/hypernativeApi.ts @@ -15,6 +15,33 @@ class HypernativeApi { this.clientSecret = clientSecret } + // Python source for the python_processing node in rate-revert agents. + // Tracks consecutive revert count in the account-wide KV store so that the + // alert only fires after 100 consecutive reverts. + private static buildRevertPythonSource(kvKey: string): string { + return `def my_function(extracted_variables): + """Your custom Python logic goes here. + The \`extracted_variables\` dictionary + contains variables from previous nodes. + The return value will be stored in the + variable defined below.""" + is_null = extracted_variables["is_null"] + key = "${kvKey}" + base = "https://api.hypernative.xyz/custom-agents/values" + + if is_null == 1: + existing = hn_api_get(f"{base}/{key}").json()["data"] + if existing is None: + count = 1 + else: + count = int(existing) + 1 + else: + count = 0 + + hn_api_put(f"{base}/{key}", {"value": str(count)}) + return count` + } + // Mapping of viem chain names to Hypernative chain strings private static chainNameToHypernativeKey: { [key: string]: string } = { 'Arbitrum One': 'arbitrum', @@ -85,28 +112,46 @@ class HypernativeApi { } public async createCustomAgentRateRevert(input: CustomAgentInput): Promise { - // This custom agent rule is already complete it only needs modification for rate provider address - const customAgentRule = { ...rateProviderRateRevertRule } + // Deep-clone so the shared template isn't mutated across calls + const customAgentRule = JSON.parse(JSON.stringify(rateProviderRateRevertRule)) + const chainKey = this.getValidChainNameFromViemChain(input.chain) + + // Per-agent KV key for the persistent revert counter (account-wide store) + const kvKey = `revert_count_${chainKey}_${input.contractAddress.slice(-4).toLowerCase()}` + const pythonSource = HypernativeApi.buildRevertPythonSource(kvKey) + const pythonSourceB64 = Buffer.from(pythonSource, 'utf-8').toString('base64') // Modify the rule based on input - customAgentRule.rule.chain = this.getValidChainNameFromViemChain(input.chain) + customAgentRule.rule.chain = chainKey customAgentRule.rule.ruleString = input.ruleString // TODO: This address has no impact on the rule execution customAgentRule.rule.contractAddress = input.contractAddress customAgentRule.agentName = input.agentName + customAgentRule.rule.customDescription = input.ruleString - customAgentRule.rule.conditions[1].operands[0].variable_extraction[3].contract_address = input.contractAddress - customAgentRule.rule.conditions[1].operands[0].eval.custom_description = input.ruleString - - customAgentRule.rule.time_based_trigger.chain = this.getValidChainNameFromViemChain(input.chain) + const extractions = customAgentRule.rule.conditions[1].operands[0].variable_extraction + // [3] gcr node + extractions[3].contract_address = input.contractAddress.toLowerCase() + extractions[3].chain = chainKey + // [5] python_processing node — inject per-agent source + const pythonExt = extractions.find((e: any) => e.type === 'python_processing') + if (pythonExt) pythonExt.source_code = pythonSourceB64 - customAgentRule.graphData.nodes[0].data.chain = this.getValidChainNameFromViemChain(input.chain) + customAgentRule.rule.conditions[1].operands[0].eval.custom_description = input.ruleString - customAgentRule.graphData.nodes[1].data.chain = this.getValidChainNameFromViemChain(input.chain) - customAgentRule.graphData.nodes[1].data.contractAddress = input.contractAddress - customAgentRule.graphData.nodes[1].data.contractAddressAlias = input.contractAddress + customAgentRule.rule.time_based_trigger.chain = chainKey - customAgentRule.graphData.nodes[4].data.message = input.ruleString + // Look up graph nodes by type so positional shifts don't break the wiring + const nodesByType: Record = {} + for (const node of customAgentRule.graphData.nodes) { + nodesByType[node.type] = node + } + nodesByType['time-based-node'].data.chain = chainKey + nodesByType['read-contract-node'].data.chain = chainKey + nodesByType['read-contract-node'].data.contractAddress = input.contractAddress + nodesByType['read-contract-node'].data.contractAddressAlias = input.contractAddress + nodesByType['send-alert-node'].data.message = input.ruleString + nodesByType['python-function-node'].data.pythonCode = pythonSourceB64 const requestBody = customAgentRule diff --git a/src/utils/hypernative/rate-provider-rate-revert.ts b/src/utils/hypernative/rate-provider-rate-revert.ts index ea7c3a7..4bc0b65 100644 --- a/src/utils/hypernative/rate-provider-rate-revert.ts +++ b/src/utils/hypernative/rate-provider-rate-revert.ts @@ -13,6 +13,7 @@ export const rateProviderRateRevertRule = { inputDataType: [], outputDataType: [], idlJson: {}, + abi: {}, contractFunctionObject: {}, outputIndex: '', funcSig: '', @@ -23,6 +24,7 @@ export const rateProviderRateRevertRule = { period_unit: 'blocks', chain: 'ethereum', }, + typeIds: [], fileName: 'some_file_name.json', operands: [], operator: '', @@ -67,6 +69,9 @@ export const rateProviderRateRevertRule = { func_sig: 'getRate()', chain: 'base', var_name: 'getRateOutput', + contract_function_object: null, + idl_json: null, + abi_string: null, }, { type: 'json_processing', @@ -74,9 +79,15 @@ export const rateProviderRateRevertRule = { json_path_engine: 'jsonata', var_name: 'is_null', }, + { + type: 'python_processing', + source_code: '', + var_name: 'consecutive_revert_count', + entry_point: 'my_function', + }, { type: 'json_processing', - json_path: 'aXNfbnVsbCA+IDA=', + json_path: 'Y29uc2VjdXRpdmVfcmV2ZXJ0X2NvdW50ID49IDEwMA==', json_path_engine: 'jsonata', var_name: 'condition_0', }, @@ -92,6 +103,10 @@ export const rateProviderRateRevertRule = { }, ], isReminderEnabled: false, + module: '', + moduleName: '', + typeArguments: [], + genericTypeParams: [], advanced: ['customDescription'], customDescription: 'On Base - the getRate() function reverted. contract 0xe1b1..4ddd', ruleString: '', @@ -105,6 +120,8 @@ export const rateProviderRateRevertRule = { }, ], remindersConfigurations: [], + endAlertsOption: 'sameAsStart', + endAlertsConfigurations: [], delay: 600, securitySuitIds: [1373], graphData: { @@ -119,16 +136,21 @@ export const rateProviderRateRevertRule = { source: '07b6cf44-db10-47b8-8a89-5c2b43d775e6', target: '6c41586c-ced7-4db6-86cb-fe2f85f73f9e', }, - { - id: 'xy-edge__6c41586c-ced7-4db6-86cb-fe2f85f73f9e-6aecd52f-8b2f-49f5-9707-12952ef36557', - source: '6c41586c-ced7-4db6-86cb-fe2f85f73f9e', - target: '6aecd52f-8b2f-49f5-9707-12952ef36557', - }, { id: 'xy-edge__6aecd52f-8b2f-49f5-9707-12952ef36557-b6d012b6-03e1-4295-9389-a2288fbfb211', source: '6aecd52f-8b2f-49f5-9707-12952ef36557', target: 'b6d012b6-03e1-4295-9389-a2288fbfb211', }, + { + id: 'xy-edge__6c41586c-ced7-4db6-86cb-fe2f85f73f9e-cf43c1f0-b099-4f21-af4c-4925c07d7ec5', + source: '6c41586c-ced7-4db6-86cb-fe2f85f73f9e', + target: 'cf43c1f0-b099-4f21-af4c-4925c07d7ec5', + }, + { + id: 'xy-edge__cf43c1f0-b099-4f21-af4c-4925c07d7ec5-6aecd52f-8b2f-49f5-9707-12952ef36557', + source: 'cf43c1f0-b099-4f21-af4c-4925c07d7ec5', + target: '6aecd52f-8b2f-49f5-9707-12952ef36557', + }, ], nodes: [ { @@ -141,7 +163,7 @@ export const rateProviderRateRevertRule = { type: 'time-based-node', measured: { width: 158, - height: 99, + height: 107, }, position: { x: 1000, @@ -180,7 +202,7 @@ export const rateProviderRateRevertRule = { type: 'read-contract-node', measured: { width: 269, - height: 286, + height: 341, }, position: { x: 710, @@ -198,7 +220,7 @@ export const rateProviderRateRevertRule = { type: 'calculation-node', measured: { width: 269, - height: 148, + height: 157, }, position: { x: 1093, @@ -208,17 +230,18 @@ export const rateProviderRateRevertRule = { { id: '6aecd52f-8b2f-49f5-9707-12952ef36557', data: { - valueA: 'is_null', - valueB: '0', - condition: '>', + valueA: 'consecutive_revert_count', + valueB: '100', + condition: 'gte', description: '', valueAMultiplier: 0, valueBMultiplier: 0, + valueCMultiplier: 0, }, type: 'condition-node', measured: { width: 269, - height: 84, + height: 92, }, position: { x: 1223, @@ -241,7 +264,27 @@ export const rateProviderRateRevertRule = { y: 455, }, }, + { + id: 'cf43c1f0-b099-4f21-af4c-4925c07d7ec5', + data: { + alias: 'consecutive_revert_count', + aliasType: 'number', + entryPoint: 'my_function', + pythonCode: '', + description: '', + }, + type: 'python-function-node', + measured: { + width: 269, + height: 111, + }, + position: { + x: 1414.49634369287, + y: 149.54816042047526, + }, + }, ], description: '', }, + mode: 'onchain', }