Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,10 @@ infra/*

# Tools
**/.claude/settings.local.json

## Python
venv/
__pycache__/
# TODO: move wavs_py & layer_trigger_world to its own pip package, similar to how it's done in Go
wavs_py
layer_trigger_world/
32 changes: 32 additions & 0 deletions components/py-evm-price-oracle/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
WAVS_PACKAGE=wavs:worker@0.4.0-beta.4
WAVS_WIT_WORLD=wavs:worker/layer-trigger-world
OUTPUT_DIR?=../../compiled

check-package:
@if [ ! -f $(WAVS_PACKAGE).wasm ]; then \
echo "Downloading WAVS package: $(WAVS_PACKAGE)"; \
wkg get ${WAVS_PACKAGE} --overwrite --format wasm --output $(WAVS_PACKAGE).wasm; \
fi

# converts the entire .wasm package into a single .wit file, easily consumable by the jco command
convert-wasm-to-wit:
@wasm-tools component wit $(WAVS_PACKAGE).wasm -o $(WAVS_PACKAGE).wit

## build-bindings: building the WAVS bindings
build-bindings: check-jco check-package
@npx jco types $(WAVS_PACKAGE).wasm --out-dir out/

## wasi-build: building the WAVS wasi component
wasi-build: check-jco build-bindings convert-wasm-to-wit
@echo "Building component: js_evm_price_oracle"
@npx tsc --outDir out/ --target es6 --strict --module preserve index.ts
@npx esbuild ./index.js --bundle --outfile=out/out.js --platform=node --format=esm
@npx jco componentize out/out.js --wit $(WAVS_PACKAGE).wit --world-name $(WAVS_WIT_WORLD) --out ../../compiled/js_evm_price_oracle.wasm

.PHONY: help
help: Makefile
@echo
@echo " Choose a command run"
@echo
@sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /'
@echo
36 changes: 36 additions & 0 deletions components/py-evm-price-oracle/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@

### Python

```bash
cd components/py-evm-price-oracle

python3 -m venv venv
source venv/bin/activate
# deactivate

pip install -r requirements.txt

# initial bindings gen
make check-package
make convert-wasm-to-wit
rm -rf layer_trigger_world wavs_py
componentize-py --wit-path wavs:worker@0.4.0-beta.4.wit --world wavs:worker/layer-trigger-world bindings .

componentize-py --wit-path wavs:worker@0.4.0-beta.4.wit --world wavs:worker/layer-trigger-world bindings wavs_py
```



## WAVS

## Run

```bash
# compile
componentize-py --wit-path wavs:worker@0.4.0-beta.4.wit --world wavs:worker/layer-trigger-world componentize app -o ../../compiled/py_evm_price_oracle.wasm

# TODO: http networking required with something like https://www.fermyon.com/blog/introducing-componentize-py ?

# this must be run from the root of the repo to work, this is why we cd ../../
(cd ../../; make wasi-exec COMPONENT_FILENAME=py_evm_price_oracle.wasm COIN_MARKET_CAP_ID=2)
```
98 changes: 98 additions & 0 deletions components/py-evm-price-oracle/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import json
import time
import urllib
from enum import Enum
from typing import Optional

import encodings.idna

import wavs_py.layer_trigger_world as layer_trigger_world
from cmc import PriceFeedData, fetch_crypto_price
from wavs_py.layer_trigger_world.imports import layer_types


class Destination(Enum):
Ethereum = "Ethereum"
Cosmos = "Cosmos"
CliOutput = "CliOutput"


class LayerTriggerWorld(layer_trigger_world.LayerTriggerWorld):
def run(self, trigger_action: layer_types.TriggerAction) -> Optional[bytes]:
print("LayerTriggerWorld.run called")

decoded = decode_trigger_event(trigger_action.data)

print(f"Trigger ID: {decoded.triggerID}")
print(f"Request Input: {decoded.requestInput}")
print(f"Destination: {decoded.dest}")

result = compute(decoded.requestInput, Destination(decoded.dest))
print(f"Computation Result: {result.decode('utf-8')}")

return route_result(decoded.triggerID, result, decoded.dest)

def route_result(trigger_id: int, result: bytes, dest: str) -> Optional[bytes]:
"""Sends the computation result to the appropriate destination"""
if dest == Destination.CliOutput.value:
return result
elif dest == Destination.Ethereum.value:
# WAVS & the contract expects abi encoded data
encoded = encode_trigger_output(trigger_id, result)
print(f"Encoded output (raw): {encoded.hex()}")
return encoded
else:
raise ValueError(f"Unsupported destination: {dest}")

# TODO:
def encode_trigger_output(trigger_id: int, result: bytes) -> bytes:
# This is a placeholder - implement according to your Ethereum ABI encoding requirements
return trigger_id.to_bytes(8, byteorder='big') + result

def compute(input_bytes: bytes, destination: Destination) -> bytes:
# Don't shadow the parameter name with a new variable
data = input_bytes

# TODO: needs a proper python abi decoding library, this is very hacky
if destination == Destination.CliOutput:
data = data.rstrip(b'\x00')

try:
crypto_id = int(data.decode('utf-8'))
except ValueError:
raise ValueError(f"Failed to parse input: {data}")

print(f"Cleaned input: {crypto_id}")

price_feed = fetch_crypto_price(crypto_id)

return price_feed.to_json().encode('utf-8')



class DecodedResult:
triggerID: int
requestInput: bytes
dest: str

def __init__(self, triggerID: int, requestInput: bytes, dest: str):
self.triggerID = triggerID
self.requestInput = requestInput
self.dest = dest

def decode_trigger_event(trigger_data: layer_types.TriggerData) -> DecodedResult:
# Check by attribute instead of isinstance
print("Has 'value' attribute:", hasattr(trigger_data, 'value'))

# Check the class name as a string
class_name = trigger_data.__class__.__name__
print("Class name:", class_name)

if hasattr(trigger_data, 'value') and class_name == 'TriggerData_Raw':
print("Processing as TriggerData_Raw using attribute check")
result = DecodedResult(0, trigger_data.value, Destination.CliOutput.value)
return result
else:
print("Not recognized as TriggerData_Raw")
# Default fallback to avoid None return
return DecodedResult(0, b'1', Destination.CliOutput.value)
64 changes: 64 additions & 0 deletions components/py-evm-price-oracle/cmc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Use urllib.request with HTTP
import json
import time
import urllib.request
from dataclasses import dataclass

import encodings.idna

@dataclass
class PriceFeedData:
symbol: str
price: float
timestamp: int

def to_json(self) -> str:
return json.dumps({
"symbol": self.symbol,
"price": self.price,
"timestamp": self.timestamp
})


def fetch_crypto_price(id: int) -> PriceFeedData:
# Create URL - using HTTP instead of HTTPS
url = f"http://api.coinmarketcap.com/data-api/v3/cryptocurrency/detail?id={id}&range=1h"

# Set up the request
headers = {
"Accept": "application/json",
"Content-Type": "application/json",
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36",
"Cookie": f"myrandom_cookie={int(time.time())}"
}

# Create request object
req = urllib.request.Request(url, headers=headers)

try:
# Make the request
with urllib.request.urlopen(req) as response:
body = response.read()

# Parse the JSON
root = json.loads(body)

# Extract data
symbol = root['data']['symbol']
price = root['data']['statistics']['price']
timestamp = root['status']['timestamp']

# Create and return the price feed data
return PriceFeedData(
symbol=symbol,
price=price,
timestamp=timestamp
)
except Exception as e:
raise RuntimeError(f"Failed to fetch price: {str(e)}")


if __name__ == "__main__":
# Example usage
price_data = fetch_crypto_price(1)
print(price_data.to_json())
11 changes: 11 additions & 0 deletions components/py-evm-price-oracle/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
anyio==4.8.0
certifi==2025.1.31
charset-normalizer==3.4.1
componentize-py==0.16.0
h11==0.14.0
httpcore==1.0.7
idna==3.10
requests==2.32.3
sniffio==1.3.1
typing_extensions==4.12.2
urllib3==2.3.0
Loading