Compare commits

..

29 Commits

Author SHA1 Message Date
Austin
a363429861
Merge pull request #70 from FlipsideCrypto/fix/udf-compile-error
Fix/udf compile error
2025-12-10 13:50:06 -05:00
Austin
f0ef82c39a int to hex 2025-12-10 13:42:54 -05:00
Austin
e84c7f4601 db 2025-12-10 13:38:21 -05:00
Austin
4dd7280484 move comments to read me 2025-12-10 13:34:41 -05:00
Austin
1036b6833a remove comments 2025-12-10 13:29:48 -05:00
Austin
a91069c976 updates 2025-12-10 13:28:43 -05:00
Austin
c9a5b819c8
Merge pull request #69 from FlipsideCrypto/DAT2-195/read-functions
Dat2 195/read functions
2025-12-10 13:19:47 -05:00
Austin
d0e3f57772 format 2025-12-10 13:15:16 -05:00
Austin
70d5fc1c3e updates 2025-12-10 12:58:54 -05:00
Austin
027f73276c dbs 2025-12-09 13:36:48 -05:00
Austin
6981b8b42d db 2025-12-09 13:31:48 -05:00
Austin
1fd0466311 comment 2025-12-09 13:28:53 -05:00
Austin
b0e51e2b4d updates 2025-12-09 13:24:24 -05:00
drethereum
3697967c46
Merge pull request #68 from FlipsideCrypto/addnew/decompress-zlib-udf
addnew/decompress-zlib-udf
2025-11-04 13:45:28 -07:00
drethereum
70e238a548 closing macro 2025-11-04 12:33:39 -07:00
drethereum
4317c353a5 merge 2025-10-22 17:12:17 -06:00
drethereum
3985d78199 udf 2025-10-22 16:54:55 -06:00
Jensen Yap
6415fc4873
Merge pull request #66 from FlipsideCrypto/hotfix/upgrade-livequery-models-bug
Upgrade livequery models revision to v1.10.2
2025-08-19 11:33:25 +09:00
Jensen Yap
de65b99f86 Upgrade livequery models revision to v1.10.2 2025-08-16 02:34:40 +09:00
Jensen Yap
45fcf86aea
Merge pull request #65 from FlipsideCrypto/STREAM-1324/upgrade-livequery-version 2025-08-13 02:29:50 +09:00
Jensen Yap
ef0f0deec0 update to 1.10.1 2025-08-13 02:03:12 +09:00
Jensen Yap
76b46b9026 update livequery models revision to v1.10.0 2025-08-08 14:06:49 +09:00
Jensen Yap
4799e897e1 fix branch name 2025-08-08 13:56:37 +09:00
Jensen Yap
7b6feb4a40 update 2025-08-08 13:55:55 +09:00
Jensen Yap
36dab6002f bump version to 1.10.0 2025-08-08 12:20:27 +09:00
Jensen Yap
a0672aff35 update livequery models version 2025-08-08 01:21:33 +09:00
Jensen Yap
88e94f5160 update 2025-08-07 23:39:45 +09:00
Jensen Yap
567b311ca8 upgrade livequery models version 2025-08-07 16:13:09 +09:00
Matt Romano
3def5e5c44
Merge pull request #64 from FlipsideCrypto/add-coingecko-stablecoin-parse-udf
add-coingecko-stablecoin-parse-udf
2025-07-30 12:30:30 -07:00
4 changed files with 498 additions and 6 deletions

116
README.md
View File

@ -159,6 +159,122 @@ The `fsc_utils` dbt package is a centralized repository consisting of various db
```
- `utils.udf_encode_contract_call`: Encodes EVM contract function calls into ABI-encoded calldata format for eth_call RPC requests. Handles all Solidity types including tuples and arrays.
```
-- Simple function with no inputs
SELECT utils.udf_encode_contract_call(
PARSE_JSON('{"name": "totalSupply", "inputs": []}'),
ARRAY_CONSTRUCT()
);
-- Returns: 0x18160ddd
-- Function with single address parameter
SELECT utils.udf_encode_contract_call(
PARSE_JSON('{
"name": "balanceOf",
"inputs": [{"name": "account", "type": "address"}]
}'),
ARRAY_CONSTRUCT('0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48')
);
-- Returns: 0x70a08231000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48
-- Function with multiple parameters
SELECT 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 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
)
)
);
```
- `utils.udf_create_eth_call`: Creates an eth_call JSON-RPC request object from contract address and encoded calldata. Supports block parameter as string or number (auto-converts numbers to hex).
```
-- Using default 'latest' block
SELECT utils.udf_create_eth_call(
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
'0x70a08231000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'
);
-- Using specific block number (auto-converted to hex)
SELECT utils.udf_create_eth_call(
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
'0x70a08231000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
18500000
);
```
- `utils.udf_create_eth_call_from_abi`: Convenience function that combines contract call encoding and JSON-RPC request creation in a single call. Recommended for most use cases.
```
-- Simple balanceOf call with default 'latest' block
SELECT utils.udf_create_eth_call_from_abi(
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
PARSE_JSON('{
"name": "balanceOf",
"inputs": [{"name": "account", "type": "address"}]
}'),
ARRAY_CONSTRUCT('0xbcca60bb61934080951369a648fb03df4f96263c')
);
-- Same call but at a specific block number
SELECT utils.udf_create_eth_call_from_abi(
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
PARSE_JSON('{
"name": "balanceOf",
"inputs": [{"name": "account", "type": "address"}]
}'),
ARRAY_CONSTRUCT('0xbcca60bb61934080951369a648fb03df4f96263c'),
18500000
);
-- Using ABI from a table
WITH abi_data AS (
SELECT
abi,
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' as contract_address,
'0xbcca60bb61934080951369a648fb03df4f96263c' as user_address
FROM ethereum.silver.flat_function_abis
WHERE contract_address = LOWER('0x43506849d7c04f9138d1a2050bbf3a0c054402dd')
AND function_name = 'balanceOf'
)
SELECT
utils.udf_create_eth_call_from_abi(
contract_address,
abi,
ARRAY_CONSTRUCT(user_address)
) as rpc_call
FROM abi_data;
```
## **Streamline V 2.0 Functions**
The `Streamline V 2.0` functions are a set of macros and UDFs that are designed to be used with `Streamline V 2.0` deployments.

View File

@ -30,6 +30,18 @@
sql: |
{{ fsc_utils.python_udf_hex_to_int_with_encoding() | indent(4) }}
- name: {{ schema }}.udf_int_to_hex
signature:
- [int, NUMBER]
return_type: VARCHAR(16777216)
options: |
NULL
LANGUAGE SQL
STRICT IMMUTABLE
sql: |
SELECT CONCAT('0x', TRIM(TO_CHAR(int, 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX')))
- name: {{ schema }}.udf_hex_to_string
signature:
- [hex, STRING]
@ -254,6 +266,18 @@
sql: |
{{ fsc_utils.create_udtf_flatten_overflowed_responses() | indent(4) }}
- name: {{ schema }}.udf_decompress_zlib
signature:
- [compressed_string, STRING]
return_type: STRING
options: |
LANGUAGE PYTHON
RUNTIME_VERSION = '3.10'
COMMENT = 'Decompresses zlib/deflate-compressed data from Python bytes literal string format'
HANDLER = 'decompress_zlib'
sql: |
{{ fsc_utils.create_udf_decompress_zlib() | indent(4) }}
- name: {{ schema }}.udf_stablecoin_data_parse
signature:
- [peggeddata_content, STRING]
@ -281,5 +305,105 @@
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 = 'Encodes EVM contract function calls into ABI-encoded calldata format for eth_call RPC requests. Handles all Solidity types including tuples and arrays.'
sql: |
{{ fsc_utils.create_udf_encode_contract_call() | indent(4) }}
- name: {{ schema }}.udf_create_eth_call
signature:
- [contract_address, STRING]
- [encoded_calldata, STRING]
return_type: OBJECT
options: |
NULL
LANGUAGE SQL
STRICT IMMUTABLE
COMMENT = 'Creates an eth_call JSON-RPC request object with default block parameter "latest".'
sql: |
{{ schema }}.udf_json_rpc_call(
'eth_call',
ARRAY_CONSTRUCT(
OBJECT_CONSTRUCT(
'to', contract_address,
'data', encoded_calldata
),
'latest'
)
)
- name: {{ schema }}.udf_create_eth_call
signature:
- [contract_address, STRING]
- [encoded_calldata, STRING]
- [block_parameter, VARIANT]
return_type: OBJECT
options: |
NULL
LANGUAGE SQL
STRICT IMMUTABLE
COMMENT = 'Creates an eth_call JSON-RPC request object. Accepts contract address, encoded calldata, and optional block parameter (string or number). If block_parameter is a number, it will be converted to hex format using ai.utils.udf_int_to_hex.'
sql: |
{{ schema }}.udf_json_rpc_call(
'eth_call',
ARRAY_CONSTRUCT(
OBJECT_CONSTRUCT(
'to', contract_address,
'data', encoded_calldata
),
CASE
WHEN block_parameter IS NULL THEN 'latest'
WHEN TYPEOF(block_parameter) IN ('INTEGER', 'NUMBER', 'FIXED', 'FLOAT') THEN
{{ schema }}.udf_int_to_hex(block_parameter::NUMBER)
ELSE block_parameter::STRING
END
)
)
- name: {{ schema }}.udf_create_eth_call_from_abi
signature:
- [contract_address, STRING]
- [function_abi, VARIANT]
- [input_values, ARRAY]
return_type: OBJECT
options: |
NULL
LANGUAGE SQL
STRICT IMMUTABLE
COMMENT = 'Convenience function that combines contract call encoding and JSON-RPC request creation for eth_call. Encodes function call from ABI and creates RPC request with default block parameter "latest".'
sql: |
{{ schema }}.udf_create_eth_call(
contract_address,
{{ schema }}.udf_encode_contract_call(function_abi, input_values)
)
- name: {{ schema }}.udf_create_eth_call_from_abi
signature:
- [contract_address, STRING]
- [function_abi, VARIANT]
- [input_values, ARRAY]
- [block_parameter, VARIANT]
return_type: OBJECT
options: |
NULL
LANGUAGE SQL
STRICT IMMUTABLE
COMMENT = 'Convenience function that combines contract call encoding and JSON-RPC request creation for eth_call. Encodes function call from ABI and creates RPC request with specified block parameter.'
sql: |
{{ schema }}.udf_create_eth_call(
contract_address,
{{ schema }}.udf_encode_contract_call(function_abi, input_values),
block_parameter
)
{% endmacro %}

View File

@ -542,6 +542,37 @@ class FlattenRows:
return list(cleansed[np.roll(cleansed.columns.values, 1).tolist()].itertuples(index=False, name=None))
{% endmacro %}
{% macro create_udf_decompress_zlib() %}
import zlib
import codecs
def decompress_zlib(compressed_string):
try:
if not compressed_string:
return None
# Remove b prefix and suffix if present
if compressed_string.startswith("b'") and compressed_string.endswith("'"):
compressed_string = compressed_string[2:-1]
elif compressed_string.startswith('b"') and compressed_string.endswith('"'):
compressed_string = compressed_string[2:-1]
# Decode the escaped string to bytes
compressed_bytes = codecs.decode(compressed_string, 'unicode_escape')
# Convert to bytes if string
if isinstance(compressed_bytes, str):
compressed_bytes = compressed_bytes.encode('latin-1')
# Decompress the zlib data
decompressed = zlib.decompress(compressed_bytes)
# Return as UTF-8 string
return decompressed.decode('utf-8')
except Exception as e:
return f"Error decompressing: {str(e)}"
{% endmacro %}
{% macro create_udf_stablecoin_data_parse() %}
import re
@ -753,4 +784,229 @@ class udf_stablecoin_data_parse:
except Exception as error:
raise Exception(f'Error parsing peggedData content: {str(error)}')
{% endmacro %}
{% 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 %}

View File

@ -1,7 +1,3 @@
packages:
- package: calogica/dbt_expectations
version: [">=0.8.0", "<0.9.0"]
- package: dbt-labs/dbt_utils
version: [">=1.0.0", "<1.1.0"]
- git: https://github.com/FlipsideCrypto/livequery-models.git
revision: "v1.9.0"
revision: "v1.10.2"