mirror of
https://github.com/FlipsideCrypto/fsc-utils.git
synced 2026-02-06 10:56:49 +00:00
updates
This commit is contained in:
parent
3697967c46
commit
b0e51e2b4d
@ -293,5 +293,19 @@
|
||||
sql: |
|
||||
{{ fsc_utils.create_udf_stablecoin_data_parse() | indent(4) }}
|
||||
|
||||
- name: {{ schema }}.udf_encode_contract_call
|
||||
signature:
|
||||
- [function_abi, VARIANT]
|
||||
- [input_values, ARRAY]
|
||||
return_type: STRING
|
||||
options: |
|
||||
LANGUAGE PYTHON
|
||||
RUNTIME_VERSION = '3.10'
|
||||
PACKAGES = ('eth-abi')
|
||||
HANDLER = 'encode_call'
|
||||
COMMENT = '{{ fsc_utils.udf_encode_contract_call_comment() }}'
|
||||
sql: |
|
||||
{{ fsc_utils.create_udf_encode_contract_call() | indent(4) }}
|
||||
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
@ -784,4 +784,353 @@ class udf_stablecoin_data_parse:
|
||||
|
||||
except Exception as error:
|
||||
raise Exception(f'Error parsing peggedData content: {str(error)}')
|
||||
{% endmacro %}
|
||||
|
||||
{% macro create_udf_encode_contract_call() %}
|
||||
|
||||
def encode_call(function_abi, input_values):
|
||||
"""
|
||||
Encodes EVM contract function calls into ABI-encoded calldata.
|
||||
|
||||
This function generates complete calldata (selector + encoded params) that can be
|
||||
used directly in eth_call JSON-RPC requests to query contract state.
|
||||
"""
|
||||
import eth_abi
|
||||
from eth_hash.auto import keccak
|
||||
import json
|
||||
|
||||
def get_function_signature(abi):
|
||||
"""
|
||||
Generate function signature using the same logic as utils.udf_evm_text_signature.
|
||||
|
||||
Examples:
|
||||
balanceOf(address)
|
||||
transfer(address,uint256)
|
||||
swap((address,address,uint256))
|
||||
"""
|
||||
def generate_signature(inputs):
|
||||
signature_parts = []
|
||||
for input_data in inputs:
|
||||
if 'components' in input_data:
|
||||
# Handle nested tuples
|
||||
component_signature_parts = []
|
||||
components = input_data['components']
|
||||
component_signature_parts.extend(generate_signature(components))
|
||||
component_signature_parts[-1] = component_signature_parts[-1].rstrip(",")
|
||||
if input_data['type'].endswith('[]'):
|
||||
signature_parts.append("(" + "".join(component_signature_parts) + ")[],")
|
||||
else:
|
||||
signature_parts.append("(" + "".join(component_signature_parts) + "),")
|
||||
else:
|
||||
# Clean up Solidity-specific modifiers
|
||||
signature_parts.append(input_data['type'].replace('enum ', '').replace(' payable', '') + ",")
|
||||
return signature_parts
|
||||
|
||||
signature_parts = [abi['name'] + "("]
|
||||
signature_parts.extend(generate_signature(abi.get('inputs', [])))
|
||||
if len(signature_parts) > 1:
|
||||
signature_parts[-1] = signature_parts[-1].rstrip(",") + ")"
|
||||
else:
|
||||
signature_parts.append(")")
|
||||
return "".join(signature_parts)
|
||||
|
||||
def function_selector(abi):
|
||||
"""Calculate 4-byte function selector using Keccak256 hash."""
|
||||
signature = get_function_signature(abi)
|
||||
hash_bytes = keccak(signature.encode('utf-8'))
|
||||
return hash_bytes[:4].hex(), signature
|
||||
|
||||
def get_canonical_type(input_spec):
|
||||
"""
|
||||
Convert ABI input spec to canonical type string for eth_abi encoding.
|
||||
|
||||
Handles tuple expansion: tuple -> (address,uint256,bytes)
|
||||
"""
|
||||
param_type = input_spec['type']
|
||||
|
||||
if param_type.startswith('tuple'):
|
||||
components = input_spec.get('components', [])
|
||||
component_types = ','.join([get_canonical_type(comp) for comp in components])
|
||||
canonical = f"({component_types})"
|
||||
|
||||
# Preserve array suffixes: tuple[] -> (address,uint256)[]
|
||||
if param_type.endswith('[]'):
|
||||
array_suffix = param_type[5:] # Everything after 'tuple'
|
||||
canonical += array_suffix
|
||||
|
||||
return canonical
|
||||
|
||||
return param_type
|
||||
|
||||
def prepare_value(value, param_type, components=None):
|
||||
"""
|
||||
Convert Snowflake values to Python types suitable for eth_abi encoding.
|
||||
|
||||
Handles type coercion and format normalization for all Solidity types.
|
||||
"""
|
||||
# Handle null/None values with sensible defaults
|
||||
if value is None:
|
||||
if param_type.startswith('uint') or param_type.startswith('int'):
|
||||
return 0
|
||||
elif param_type == 'address':
|
||||
return '0x' + '0' * 40
|
||||
elif param_type == 'bool':
|
||||
return False
|
||||
elif param_type.startswith('bytes'):
|
||||
return b''
|
||||
else:
|
||||
return value
|
||||
|
||||
# CRITICAL: Check arrays FIRST (before base types)
|
||||
# This prevents bytes[] from matching the bytes check
|
||||
if param_type.endswith('[]'):
|
||||
base_type = param_type[:-2]
|
||||
if not isinstance(value, list):
|
||||
return []
|
||||
|
||||
# Special handling for tuple arrays
|
||||
if base_type == 'tuple' and components:
|
||||
return [prepare_tuple(v, components) for v in value]
|
||||
else:
|
||||
return [prepare_value(v, base_type) for v in value]
|
||||
|
||||
# Base type conversions
|
||||
if param_type == 'address':
|
||||
addr = str(value).lower()
|
||||
if not addr.startswith('0x'):
|
||||
addr = '0x' + addr
|
||||
return addr
|
||||
|
||||
if param_type.startswith('uint') or param_type.startswith('int'):
|
||||
return int(value)
|
||||
|
||||
if param_type == 'bool':
|
||||
if isinstance(value, str):
|
||||
return value.lower() in ('true', '1', 'yes')
|
||||
return bool(value)
|
||||
|
||||
if param_type.startswith('bytes'):
|
||||
if isinstance(value, str):
|
||||
if value.startswith('0x'):
|
||||
value = value[2:]
|
||||
return bytes.fromhex(value)
|
||||
return value
|
||||
|
||||
if param_type == 'string':
|
||||
return str(value)
|
||||
|
||||
return value
|
||||
|
||||
def prepare_tuple(value, components):
|
||||
"""
|
||||
Recursively prepare tuple values, handling nested structures.
|
||||
|
||||
Tuples can contain other tuples, arrays, or tuple arrays.
|
||||
"""
|
||||
if not isinstance(value, (list, tuple)):
|
||||
# Support dict-style input (by component name)
|
||||
if isinstance(value, dict):
|
||||
value = [value.get(comp.get('name', f'field_{i}'))
|
||||
for i, comp in enumerate(components)]
|
||||
else:
|
||||
return value
|
||||
|
||||
result = []
|
||||
for i, comp in enumerate(components):
|
||||
if i >= len(value):
|
||||
result.append(None)
|
||||
continue
|
||||
|
||||
comp_type = comp['type']
|
||||
val = value[i]
|
||||
|
||||
# Handle tuple arrays within tuples
|
||||
if comp_type.endswith('[]') and comp_type.startswith('tuple'):
|
||||
sub_components = comp.get('components', [])
|
||||
result.append(prepare_value(val, comp_type, sub_components))
|
||||
elif comp_type.startswith('tuple'):
|
||||
# Single tuple (not array)
|
||||
sub_components = comp.get('components', [])
|
||||
result.append(prepare_tuple(val, sub_components))
|
||||
else:
|
||||
result.append(prepare_value(val, comp_type))
|
||||
|
||||
return tuple(result)
|
||||
|
||||
try:
|
||||
inputs = function_abi.get('inputs', [])
|
||||
|
||||
# Calculate selector using battle-tested signature generation
|
||||
selector_hex, signature = function_selector(function_abi)
|
||||
|
||||
# Functions with no inputs only need the selector
|
||||
if not inputs:
|
||||
return '0x' + selector_hex
|
||||
|
||||
# Prepare values for encoding
|
||||
prepared_values = []
|
||||
for i, inp in enumerate(inputs):
|
||||
if i >= len(input_values):
|
||||
prepared_values.append(None)
|
||||
continue
|
||||
|
||||
value = input_values[i]
|
||||
param_type = inp['type']
|
||||
|
||||
# Handle tuple arrays at top level
|
||||
if param_type.endswith('[]') and param_type.startswith('tuple'):
|
||||
components = inp.get('components', [])
|
||||
prepared_values.append(prepare_value(value, param_type, components))
|
||||
elif param_type.startswith('tuple'):
|
||||
# Single tuple (not array)
|
||||
components = inp.get('components', [])
|
||||
prepared_values.append(prepare_tuple(value, components))
|
||||
else:
|
||||
prepared_values.append(prepare_value(value, param_type))
|
||||
|
||||
# Get canonical type strings for eth_abi (expands tuples)
|
||||
types = [get_canonical_type(inp) for inp in inputs]
|
||||
|
||||
# Encode parameters using eth_abi
|
||||
encoded_params = eth_abi.encode(types, prepared_values).hex()
|
||||
|
||||
# Return complete calldata: selector + encoded params
|
||||
return '0x' + selector_hex + encoded_params
|
||||
|
||||
except Exception as e:
|
||||
# Return structured error for debugging
|
||||
import traceback
|
||||
return json.dumps({
|
||||
'error': str(e),
|
||||
'traceback': traceback.format_exc(),
|
||||
'function': function_abi.get('name', 'unknown'),
|
||||
'signature': signature if 'signature' in locals() else 'not computed',
|
||||
'selector': '0x' + selector_hex if 'selector_hex' in locals() else 'not computed',
|
||||
'types': types if 'types' in locals() else 'not computed'
|
||||
})
|
||||
|
||||
{% endmacro %}
|
||||
|
||||
{% macro udf_encode_contract_call_comment() %}
|
||||
Encodes EVM contract function calls into hex calldata format for eth_call RPC requests.
|
||||
|
||||
PURPOSE:
|
||||
Converts human-readable function parameters into ABI-encoded calldata that can be sent
|
||||
to Ethereum nodes via JSON-RPC. Handles all Solidity types including complex nested
|
||||
structures like tuples and arrays.
|
||||
|
||||
PARAMETERS:
|
||||
function_abi (VARIANT):
|
||||
- JSON object containing the function ABI definition
|
||||
- Must include: "name" (string) and "inputs" (array of input definitions)
|
||||
- Each input needs: "name", "type", and optionally "components" for tuples
|
||||
|
||||
input_values (ARRAY):
|
||||
- Array of values matching the function inputs in order
|
||||
- Values should be provided as native Snowflake types:
|
||||
* addresses: strings (with or without 0x prefix)
|
||||
* uint/int: numbers
|
||||
* bool: booleans
|
||||
* bytes/bytes32: hex strings (with or without 0x prefix)
|
||||
* arrays: Snowflake arrays
|
||||
* tuples: Snowflake arrays in component order
|
||||
|
||||
RETURNS:
|
||||
STRING: Complete calldata as hex string with 0x prefix
|
||||
- Format: 0x{4-byte selector}{encoded parameters}
|
||||
- Can be used directly in eth_call RPC requests
|
||||
- Returns JSON error object if encoding fails
|
||||
|
||||
EXAMPLES:
|
||||
|
||||
-- Simple function with no inputs
|
||||
SELECT crosschain_dev.utils.udf_encode_contract_call(
|
||||
PARSE_JSON(''{"name": "totalSupply", "inputs": []}''),
|
||||
ARRAY_CONSTRUCT()
|
||||
);
|
||||
-- Returns: 0x18160ddd
|
||||
|
||||
-- Function with single address parameter
|
||||
SELECT crosschain_dev.utils.udf_encode_contract_call(
|
||||
PARSE_JSON(''{
|
||||
"name": "balanceOf",
|
||||
"inputs": [{"name": "account", "type": "address"}]
|
||||
}''),
|
||||
ARRAY_CONSTRUCT(''0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'')
|
||||
);
|
||||
-- Returns: 0x70a08231000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48
|
||||
|
||||
-- Function with multiple parameters
|
||||
SELECT crosschain_dev.utils.udf_encode_contract_call(
|
||||
PARSE_JSON(''{
|
||||
"name": "transfer",
|
||||
"inputs": [
|
||||
{"name": "to", "type": "address"},
|
||||
{"name": "amount", "type": "uint256"}
|
||||
]
|
||||
}''),
|
||||
ARRAY_CONSTRUCT(''0x1234567890123456789012345678901234567890'', 1000000)
|
||||
);
|
||||
|
||||
-- Complex function with nested tuples
|
||||
SELECT crosschain_dev.utils.udf_encode_contract_call(
|
||||
PARSE_JSON(''{
|
||||
"name": "swap",
|
||||
"inputs": [{
|
||||
"name": "params",
|
||||
"type": "tuple",
|
||||
"components": [
|
||||
{"name": "tokenIn", "type": "address"},
|
||||
{"name": "tokenOut", "type": "address"},
|
||||
{"name": "amountIn", "type": "uint256"}
|
||||
]
|
||||
}]
|
||||
}''),
|
||||
ARRAY_CONSTRUCT(
|
||||
ARRAY_CONSTRUCT(
|
||||
''0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'',
|
||||
''0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'',
|
||||
1000000
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
TYPICAL WORKFLOW:
|
||||
1. Get function ABI from crosschain.evm.dim_contract_abis
|
||||
2. Prepare input values as Snowflake arrays
|
||||
3. Encode using this function
|
||||
4. Execute via eth_call RPC (ai.live.udf_api)
|
||||
5. Decode response using streamline_dev.utils.udf_evm_decode_trace
|
||||
|
||||
SUPPORTED TYPES:
|
||||
- address: Ethereum addresses
|
||||
- uint8, uint16, ..., uint256: Unsigned integers
|
||||
- int8, int16, ..., int256: Signed integers
|
||||
- bool: Boolean values
|
||||
- bytes, bytes1, ..., bytes32: Fixed and dynamic byte arrays
|
||||
- string: Dynamic strings
|
||||
- Arrays: Any type followed by []
|
||||
- Tuples: Nested structures with components
|
||||
- Nested combinations: tuple[], tuple[][], etc.
|
||||
|
||||
NOTES:
|
||||
- Function selector is automatically calculated using Keccak256
|
||||
- Compatible with existing utils.udf_evm_text_signature and utils.udf_keccak256
|
||||
- Handles gas-optimized function names (e.g., selector 0x00000000)
|
||||
- Tuples must be provided as arrays in component order
|
||||
- Empty arrays are valid for array-type parameters
|
||||
|
||||
ERROR HANDLING:
|
||||
- Returns JSON error object on failure
|
||||
- Check if result starts with "{" to detect errors
|
||||
- Error object includes: error message, traceback, function name, types
|
||||
|
||||
RELATED FUNCTIONS:
|
||||
- utils.udf_evm_text_signature: Generate function signature
|
||||
- utils.udf_keccak256: Calculate function selector
|
||||
- streamline_dev.utils.udf_evm_decode_trace: Decode call results
|
||||
|
||||
VERSION: 1.0
|
||||
AUTHOR: Flipside Crypto Data Engineering
|
||||
LAST UPDATED: 2024-12-09
|
||||
{% endmacro %}
|
||||
Loading…
Reference in New Issue
Block a user