Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add app data repo functions #14

Merged
merged 33 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
85a0793
refactor repo configs/readme
ribeirojose Apr 25, 2024
f2324d7
add codegen module
ribeirojose Apr 25, 2024
cd653d9
add common module
ribeirojose Apr 25, 2024
15df625
remove stale subgraphs/core modules
ribeirojose Apr 25, 2024
4b923e4
add contracts module
ribeirojose Apr 25, 2024
04f9139
add order_book module
ribeirojose Apr 25, 2024
661fe9b
add order posting example
ribeirojose Apr 25, 2024
b1a949c
add generated subgraph files
ribeirojose Apr 25, 2024
9cf4021
add contract abis
ribeirojose Apr 25, 2024
e9a8acd
add generated codegen
ribeirojose Apr 25, 2024
ca26b90
add generated order_book
ribeirojose Apr 25, 2024
25ae6a5
add downloaded schemas
yvesfracari Aug 15, 2024
92db8a8
add utils of app data module
yvesfracari Aug 15, 2024
013a097
wip: start convertion from hex to cid
yvesfracari Aug 15, 2024
8b78530
add generated api model
yvesfracari Aug 16, 2024
0f938d5
add python-dotenv package
yvesfracari Aug 16, 2024
5576b57
refactor importing sort
yvesfracari Aug 16, 2024
a3e1187
refactor .env usage and variables requested
yvesfracari Aug 16, 2024
4dd9452
Merge branch 'cow-4-order-book' into cow-5-generated
yvesfracari Aug 16, 2024
08f1d03
add appDataHex object and its convertion to cid
yvesfracari Aug 16, 2024
dc75e36
wip: add converstions between cid, hex and doc
yvesfracari Aug 16, 2024
659499d
rename files to snakecase
yvesfracari Aug 26, 2024
afa6893
enable env modify ipfs urls
yvesfracari Aug 26, 2024
9b3407a
add app data doc tests
yvesfracari Aug 26, 2024
ba4c3c4
wip: start app data doc to cid
yvesfracari Aug 26, 2024
b289572
add app data and remove legacy code
yvesfracari Aug 29, 2024
4d8554b
fix digest util functions
yvesfracari Aug 29, 2024
571264c
merge into base branch
yvesfracari Aug 29, 2024
20799ee
fix import
yvesfracari Aug 29, 2024
2c445e6
Merge branch 'main' into pedro/cow-129-adddata-repo-functions
ribeirojose Sep 10, 2024
8e8de09
chore: relock poetry.lock
ribeirojose Sep 10, 2024
ea2346e
chore: add multiformats
ribeirojose Sep 10, 2024
c6cf3f7
chore(app_data): remove unused app_data schemas
ribeirojose Sep 10, 2024
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
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
USER_ADDRESS=
PRIVATE_KEY=
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
strategy:
matrix:
python-version: [
"3.8", "3.9", "3.10", "3.11", "3.12",
"3.10", "3.11", "3.12",
]
timeout-minutes: 10
steps:
Expand All @@ -32,6 +32,10 @@ jobs:
cache: poetry
cache-dependency-path: poetry.lock

- name: Poetry lock
run: |
poetry lock

- name: Install dependencies
run: |
poetry install
Expand Down
17 changes: 9 additions & 8 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.1.4
hooks:
# Run the linter.
- id: ruff
# Run the formatter.
- id: ruff-format
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.3.7
hooks:
# Run the linter.
- id: ruff
args: [--fix]
# Run the formatter.
- id: ruff-format
27 changes: 27 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
.PHONY: codegen web3_codegen orderbook_codegen subgraph_codegen test lint format remove_unused_imports

codegen: web3_codegen orderbook_codegen subgraph_codegen

# web3_codegen:
# poetry run web3_codegen

orderbook_codegen:
poetry run datamodel-codegen --url="https://raw.githubusercontent.com/cowprotocol/services/v2.245.1/crates/orderbook/openapi.yml" --output cow_py/order_book/generated/model.py --target-python-version 3.12 --output-model-type pydantic_v2.BaseModel --input-file-type openapi

subgraph_codegen:
poetry run ariadne-codegen

test:
poetry run pytest -s

lint:
poetry run ruff check . --fix

format: remove_unused_imports
poetry run ruff format

remove_unused_imports:
poetry run pycln --all .

typecheck:
poetry run pyright
80 changes: 68 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Welcome to the CoW Protocol Python SDK (cow_py), a developer-friendly Python lib

## 🐄 Features

- Querying CoW Protocol subgraphs.
- Querying CoW Protocol subgraph.
- Managing orders on the CoW Protocol.
- Interacting with CoW Protocol smart contracts.
- Encoding orders metadata and pinning to CID.
Expand All @@ -27,14 +27,14 @@ pip install cow_py
Here's a simple example to get your hooves dirty:

```python
# TODO: this code is aspirational, this API doesn't exist
from cow_py.order_book import OrderBook

# Initialize the OrderBook
order_book = OrderBook()
from cow_py.order_book.api import OrderBookApi, UID

# Initialize the OrderBookApi
order_book_api = OrderBookApi()

# Fetch and display orders
orders = order_book.get_orders()
orders = order_book.get_order_by_uid(UID("0x..."))
print(orders)
```

Expand All @@ -44,19 +44,47 @@ print(orders)
- `contracts/`(TODO): A pasture of Smart contract ABIs for interaction.
- `order_book/`(TODO): Functions to wrangle orders on the CoW Protocol.
- `order_signing/`(TODO): Tools for signing and validating orders. Anything inside this module should use higher level modules, and the process of actually signing (ie. calling the web3 function to generate the signature, should be handled in contracts, not here).
- `subgraphs/`(WIP): GraphQL clients for querying CoW Protocol data.
- `subgraph/`(WIP): GraphQL client for querying CoW Protocol's Subgraph.
- `web3/`: Web3 providers for blockchain interactions.

## 🐄 How to Use

### Querying the Subgraph (WIP)
### Querying the Subgraph

Using the built-in GraphQL client, you can query the CoW Protocol's Subgraph to get real-time data on the CoW Protocol. You can query the Subgraph by using the `SubgraphClient` class and passing in the URL of the Subgraph.

```python
from cow_py.subgraphs.queries import TotalsQuery
from cow_py.subgraph.client import SubgraphClient

totals_query = TotalsQuery()
totals = await totals_query.execute()
print(totals)
url = build_subgraph_url() # Default network is Chain.MAINNET and env SubgraphEnvironment.PRODUCTION
client = SubgraphClient(url=url)

# Fetch the total supply of the CoW Protocol, defined in a query in cow_py/subgraph/queries
totals = await client.totals()
print(totals) # Pydantic model, defined in cow_py/subgraph/graphql_client/{query_name}.py
```

Or you can leverage `SubgraphClient` to use a custom query and get the results as JSON:

```python
from pprint import pprint
from cow_py.subgraph.client import SubgraphClient

url = build_subgraph_url() # Default network is Chain.MAINNET and env SubgraphEnvironment.PRODUCTION
client = SubgraphClient(url=url)

response = await client.execute(query="""
query LastDaysVolume($days: Int!) {
dailyTotals(orderBy: timestamp, orderDirection: desc, first: $days) {
timestamp
volumeUsd
}
}
""", variables=dict(days=2)
)

data = client.get_data(response)
pprint(data)
```

### Signing an Order (TODO)
Expand All @@ -75,6 +103,34 @@ signed_order = sign_order(order_details, private_key="your_private_key")
print(signed_order)
```

## 🐄 Development

### 🐄 Tests

Run tests to ensure everything's working:

```bash
make test # or poetry run pytest
```

### 🐄 Formatting/Linting

Run the formatter and linter:

```bash
make format # or ruff check . --fix
make lint # or ruff format
```

### 🐄 Codegen

Generate the SDK from the CoW Protocol smart contracts, Subgraph, and Orderbook API:

```bash
make codegen
```


## 🐄 Contributing to the Herd

Interested in contributing? Here's how you can help:
Expand Down
File renamed without changes.
17 changes: 17 additions & 0 deletions cow_py/app_data/appDataCid.py
yvesfracari marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from typing import Any, Dict
import httpx
from multiformats import CID, multibase

from cow_py.app_data.consts import DEFAULT_IPFS_READ_URI
from cow_py.app_data.utils import extract_digest, fetch_doc_from_cid


class AppDataCid:
def __init__(self, app_data_cid: str):
self.app_data_cid = app_data_cid

async def to_doc(self, ipfs_uri: str = DEFAULT_IPFS_READ_URI) -> Dict[str, Any]:
return await fetch_doc_from_cid(self.app_data_cid, ipfs_uri)

def to_hex(self) -> str:
return extract_digest(self.app_data_cid)
85 changes: 85 additions & 0 deletions cow_py/app_data/appDataDoc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from typing import Dict, Any, TypedDict, Optional

import httpx
yvesfracari marked this conversation as resolved.
Show resolved Hide resolved

from cow_py.app_data.consts import DEFAULT_IPFS_WRITE_URI, MetaDataError
from cow_py.app_data.utils import extract_digest, stringify_deterministic


class IpfsUploadResult(TypedDict):
appData: str
cid: str


class PinataPinResponse(TypedDict):
IpfsHash: str
PinSize: int
Timestamp: str


class Ipfs(TypedDict):
writeUri: str
pinataApiKey: str
pinataApiSecret: str


class AppDataDoc:
def __init__(self, app_data_doc: Dict[str, Any]):
self.app_data_doc = app_data_doc

# TODO: Missing test
async def upload_to_ipfs_legacy(
self, ipfs_config: Ipfs
) -> Optional[IpfsUploadResult]:
"""
Uploads a appDocument to IPFS

@deprecated Pinata IPFS automatically pins the uploaded document using some implicit encoding and hashing algorithm.
This method is not used anymore to make it more explicit these parameters and therefore less dependent on the default implementation of Pinata

@param app_data_doc Document to upload
@param ipfs_config keys to access the IPFS API

@returns the IPFS CID v0 of the content
"""
pin_response = await self._pin_json_in_pinata_ipfs(
self.app_data_doc, ipfs_config
)
cid = pin_response["IpfsHash"]
return {
"appData": extract_digest(cid),
"cid": cid,
}

async def _pin_json_in_pinata_ipfs(
self, file: Any, ipfs_config: Ipfs
) -> PinataPinResponse:
write_uri = ipfs_config.get("writeUri", DEFAULT_IPFS_WRITE_URI)
pinata_api_key = ipfs_config.get("pinataApiKey", "")
pinata_api_secret = ipfs_config.get("pinataApiSecret", "")

yvesfracari marked this conversation as resolved.
Show resolved Hide resolved
if not pinata_api_key or not pinata_api_secret:
raise MetaDataError("You need to pass IPFS api credentials.")

body = stringify_deterministic(
{
"pinataContent": file,
"pinataMetadata": {"name": "appData"},
}
)

pinata_url = f"{write_uri}/pinning/pinJSONToIPFS"
headers = {
"Content-Type": "application/json",
"pinata_api_key": pinata_api_key,
"pinata_secret_api_key": pinata_api_secret,
}

async with httpx.AsyncClient() as client:
response = await client.post(pinata_url, json=body, headers=headers)
data = response.json()
if response.status_code != 200:
raise Exception(
data.get("error", {}).get("details") or data.get("error")
)
return data
85 changes: 85 additions & 0 deletions cow_py/app_data/appDataHex.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from typing import Dict, Any
from web3 import Web3
from multiformats import multibase, CID

from cow_py.app_data.appDataCid import AppDataCid
from cow_py.app_data.appDataDoc import AppDataDoc
from cow_py.app_data.consts import DEFAULT_IPFS_READ_URI, MetaDataError
from cow_py.app_data.utils import fetch_doc_from_cid


class AppDataHex:
def __init__(self, app_data_hex: str):
self.app_data_hex = app_data_hex

def to_cid(self) -> str:
cid = self._app_data_hex_to_cid()
self._assert_cid(cid)
return cid

def to_cid_legacy(self) -> str:
cid = self._app_data_hex_to_cid_legacy()
self._assert_cid(cid)
return cid

async def to_doc(self, ipfs_uri: str = DEFAULT_IPFS_READ_URI) -> Dict[str, Any]:
try:
cid = self.to_cid()
return await fetch_doc_from_cid(cid, ipfs_uri)
except Exception as e:
raise MetaDataError(
f"Unexpected error decoding AppData: appDataHex={self.app_data_hex}, message={e}"
)

async def to_doc_legacy(
self, ipfs_uri: str = DEFAULT_IPFS_READ_URI
) -> Dict[str, Any]:
try:
cid = self.to_cid_legacy()
return await fetch_doc_from_cid(cid, ipfs_uri)
except Exception as e:
raise MetaDataError(
f"Unexpected error decoding AppData: appDataHex={self.app_data_hex}, message={e}"
)

def _assert_cid(self, cid: str):
if not cid:
raise MetaDataError(
f"Error getting CID from appDataHex: {self.app_data_hex}"
)

def _app_data_hex_to_cid(self) -> str:
cid_bytes = self._to_cid_bytes(
{
"version": 0x01, # CIDv1
"multicodec": 0x55, # Raw codec
"hashing_algorithm": 0x1B, # keccak hash algorithm
"hashing_length": 32, # keccak hash length (0x20 = 32)
yvesfracari marked this conversation as resolved.
Show resolved Hide resolved
"multihash_hex": self.app_data_hex, # 32 bytes of the keccak256 hash
}
)
return multibase.encode(cid_bytes, "base16")

def _app_data_hex_to_cid_legacy(self) -> str:
cid_bytes = self._to_cid_bytes(
{
"version": 0x01, # CIDv1
"multicodec": 0x70, # dag-pb
"hashing_algorithm": 0x12, # sha2-256 hash algorithm
"hashing_length": 32, # SHA-256 length (0x20 = 32)
"multihash_hex": self.app_data_hex, # 32 bytes of the sha2-256 hash
}
)
return str(CID.decode(cid_bytes).set(version=0))

def _to_cid_bytes(self, params: Dict[str, Any]) -> bytes:
hash_bytes = Web3.to_bytes(hexstr=params["multihash_hex"])
cid_prefix = bytes(
[
params["version"],
params["multicodec"],
params["hashing_algorithm"],
params["hashing_length"],
]
)
return cid_prefix + hash_bytes
6 changes: 6 additions & 0 deletions cow_py/app_data/consts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
DEFAULT_IPFS_READ_URI = "https://cloudflare-ipfs.com/ipfs"
DEFAULT_IPFS_WRITE_URI = "https://api.pinata.cloud"
yvesfracari marked this conversation as resolved.
Show resolved Hide resolved


class MetaDataError(Exception):
pass
Loading