kopia lustrzana https://github.com/bugout-dev/moonstream
Merge branch 'dataset-nfts' of github.com:bugout-dev/moonstream into dataset-nfts
commit
9a8c6444f7
|
@ -0,0 +1,45 @@
|
|||
name: Publish Moonstream Python client library
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "clients/python/v*"
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: clients/python
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: "3.9"
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -e .[distribute]
|
||||
- name: Build and publish
|
||||
env:
|
||||
TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
|
||||
run: |
|
||||
python setup.py sdist bdist_wheel
|
||||
twine upload dist/*
|
||||
create_release:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/create-release@v1
|
||||
id: create_release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: "Moonstream Python client library - ${{ github.ref }}"
|
||||
body: |
|
||||
Version ${{ github.ref }} of the Moonstream Python client library.
|
||||
draft: true
|
||||
prerelease: false
|
|
@ -0,0 +1,38 @@
|
|||
name: Linting and tests for the Moonstream Python client library
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
paths:
|
||||
- "clients/python/**"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: "3.9"
|
||||
- name: Install test requirements
|
||||
working-directory: ./clients/python
|
||||
run: pip install -e .[dev]
|
||||
- name: Mypy type check
|
||||
working-directory: ./clients/python
|
||||
run: mypy moonstream/
|
||||
- name: Black syntax check
|
||||
working-directory: ./clients/python
|
||||
run: black --check moonstream/
|
||||
- name: Unit tests
|
||||
working-directory: ./clients/python
|
||||
run: python -m unittest discover -v
|
||||
- name: Check that versions are synchronized
|
||||
working-directory: ./clients/python
|
||||
run: |
|
||||
CLIENT_VERSION=$(python -c "from moonstream.client import CLIENT_VERSION; print(CLIENT_VERSION)")
|
||||
SETUP_PY_VERSION=$(python setup.py --version)
|
||||
echo "Client version: $CLIENT_VERSION"
|
||||
echo "setup.py version: $SETUP_PY_VERSION"
|
||||
test "$CLIENT_VERSION" = "$SETUP_PY_VERSION"
|
|
@ -5,6 +5,8 @@ from enum import Enum
|
|||
import uuid
|
||||
|
||||
import boto3 # type: ignore
|
||||
from bugout.data import BugoutSearchResults
|
||||
from bugout.journal import SearchOrder
|
||||
from moonstreamdb.models import (
|
||||
EthereumAddress,
|
||||
EthereumLabel,
|
||||
|
@ -20,12 +22,20 @@ from .settings import (
|
|||
MOONSTREAM_APPLICATION_ID,
|
||||
bugout_client as bc,
|
||||
BUGOUT_REQUEST_TIMEOUT_SECONDS,
|
||||
MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
MOONSTREAM_DATA_JOURNAL_ID,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
ETHERSCAN_SMARTCONTRACT_LABEL_NAME = "etherscan_smartcontract"
|
||||
|
||||
|
||||
class StatusAPIException(Exception):
|
||||
"""
|
||||
Raised during checking Moonstream API statuses.
|
||||
"""
|
||||
|
||||
|
||||
def get_contract_source_info(
|
||||
db_session: Session, contract_address: str
|
||||
) -> Optional[data.EthereumSmartContractSourceInfo]:
|
||||
|
@ -192,3 +202,29 @@ def create_onboarding_resource(
|
|||
timeout=BUGOUT_REQUEST_TIMEOUT_SECONDS,
|
||||
)
|
||||
return resource
|
||||
|
||||
|
||||
def check_api_status():
|
||||
crawl_types_timestamp: Dict[str, Any] = {
|
||||
"ethereum_txpool": None,
|
||||
"ethereum_trending": None,
|
||||
}
|
||||
for crawl_type in crawl_types_timestamp.keys():
|
||||
try:
|
||||
search_results: BugoutSearchResults = bc.search(
|
||||
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
journal_id=MOONSTREAM_DATA_JOURNAL_ID,
|
||||
query=f"tag:crawl_type:{crawl_type}",
|
||||
limit=1,
|
||||
content=False,
|
||||
timeout=10.0,
|
||||
order=SearchOrder.DESCENDING,
|
||||
)
|
||||
if len(search_results.results) == 1:
|
||||
crawl_types_timestamp[crawl_type] = search_results.results[0].created_at
|
||||
except Exception:
|
||||
raise StatusAPIException(
|
||||
f"Unable to get status for crawler with type: {crawl_type}"
|
||||
)
|
||||
|
||||
return crawl_types_timestamp
|
||||
|
|
|
@ -7,9 +7,12 @@ import time
|
|||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from . import actions
|
||||
from . import data
|
||||
from .middleware import MoonstreamHTTPException
|
||||
from .routes.address_info import app as addressinfo_api
|
||||
from .routes.nft import app as nft_api
|
||||
from .routes.whales import app as whales_api
|
||||
from .routes.subscriptions import app as subscriptions_api
|
||||
from .routes.streams import app as streams_api
|
||||
from .routes.txinfo import app as txinfo_api
|
||||
|
@ -46,9 +49,31 @@ async def now_handler() -> data.NowResponse:
|
|||
return data.NowResponse(epoch_time=time.time())
|
||||
|
||||
|
||||
@app.get("/status", response_model=data.StatusResponse)
|
||||
async def status_handler() -> data.StatusResponse:
|
||||
"""
|
||||
Get latest records and their creation timestamp for crawlers:
|
||||
- ethereum_txpool
|
||||
- ethereum_trending
|
||||
"""
|
||||
try:
|
||||
crawl_types_timestamp = actions.check_api_status()
|
||||
except actions.StatusAPIException:
|
||||
raise MoonstreamHTTPException(status_code=500)
|
||||
except Exception as e:
|
||||
logger.error(f"Unhandled status exception, error: {e}")
|
||||
raise MoonstreamHTTPException(status_code=500)
|
||||
|
||||
return data.StatusResponse(
|
||||
ethereum_txpool_timestamp=crawl_types_timestamp["ethereum_txpool"],
|
||||
ethereum_trending_timestamp=crawl_types_timestamp["ethereum_trending"],
|
||||
)
|
||||
|
||||
|
||||
app.mount("/subscriptions", subscriptions_api)
|
||||
app.mount("/users", users_api)
|
||||
app.mount("/streams", streams_api)
|
||||
app.mount("/txinfo", txinfo_api)
|
||||
app.mount("/address_info", addressinfo_api)
|
||||
app.mount("/nft", nft_api)
|
||||
app.mount("/whales", whales_api)
|
||||
|
|
|
@ -67,6 +67,11 @@ class NowResponse(BaseModel):
|
|||
epoch_time: float
|
||||
|
||||
|
||||
class StatusResponse(BaseModel):
|
||||
ethereum_txpool_timestamp: Optional[datetime] = None
|
||||
ethereum_trending_timestamp: Optional[datetime] = None
|
||||
|
||||
|
||||
class SubscriptionUpdate(BaseModel):
|
||||
update: Dict[str, Any]
|
||||
drop_keys: List[str] = Field(default_factory=list)
|
||||
|
|
|
@ -16,10 +16,13 @@ from sqlalchemy.orm import Session
|
|||
from .. import data
|
||||
from ..stream_queries import StreamQuery
|
||||
|
||||
from ..settings import ETHTXPOOL_HUMBUG_CLIENT_ID
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.WARN)
|
||||
|
||||
allowed_tags = ["tag:erc721"]
|
||||
|
||||
|
||||
class BugoutEventProviderError(Exception):
|
||||
"""
|
||||
|
@ -314,14 +317,17 @@ class EthereumTXPoolProvider(BugoutEventProvider):
|
|||
]
|
||||
subscriptions_filters = []
|
||||
for address in addresses:
|
||||
subscriptions_filters.extend(
|
||||
[f"?#from_address:{address}", f"?#to_address:{address}"]
|
||||
)
|
||||
if address in allowed_tags:
|
||||
subscriptions_filters.append(address)
|
||||
else:
|
||||
subscriptions_filters.extend(
|
||||
[f"?#from_address:{address}", f"?#to_address:{address}"]
|
||||
)
|
||||
|
||||
return subscriptions_filters
|
||||
|
||||
|
||||
class NftProvider(BugoutEventProvider):
|
||||
class PublicDataProvider(BugoutEventProvider):
|
||||
def __init__(
|
||||
self,
|
||||
event_type: str,
|
||||
|
@ -358,7 +364,7 @@ Shows the top 10 addresses active on the Ethereum blockchain over the last hour
|
|||
4. Amount (in WEI) received
|
||||
|
||||
To restrict your queries to this provider, add a filter of \"type:ethereum_whalewatch\" to your query (query parameter: \"q\") on the /streams endpoint."""
|
||||
whalewatch_provider = BugoutEventProvider(
|
||||
whalewatch_provider = PublicDataProvider(
|
||||
event_type="ethereum_whalewatch",
|
||||
description=whalewatch_description,
|
||||
default_time_interval_seconds=310,
|
||||
|
@ -376,7 +382,7 @@ ethereum_txpool_provider = EthereumTXPoolProvider(
|
|||
description=ethereum_txpool_description,
|
||||
default_time_interval_seconds=5,
|
||||
estimated_events_per_time_interval=50,
|
||||
tags=["client:ethereum-txpool-crawler-0"],
|
||||
tags=[f"client:{ETHTXPOOL_HUMBUG_CLIENT_ID}"],
|
||||
)
|
||||
|
||||
nft_summary_description = """Event provider for NFT market summaries.
|
||||
|
@ -388,7 +394,7 @@ Currently, it summarizes the activities on the following NFT markets:
|
|||
|
||||
This provider is currently not accessible for subscription. The data from this provider is publicly
|
||||
available at the /nft endpoint."""
|
||||
nft_summary_provider = NftProvider(
|
||||
nft_summary_provider = PublicDataProvider(
|
||||
event_type="nft_summary",
|
||||
description=nft_summary_description,
|
||||
# 40 blocks per summary, 15 seconds per block + 2 seconds wiggle room.
|
||||
|
|
|
@ -8,11 +8,14 @@ from bugout.data import BugoutResource
|
|||
from moonstreamdb.models import (
|
||||
EthereumBlock,
|
||||
EthereumTransaction,
|
||||
EthereumAddress,
|
||||
EthereumLabel,
|
||||
)
|
||||
from sqlalchemy import or_, and_, text
|
||||
from sqlalchemy.orm import Session, Query
|
||||
from sqlalchemy.sql.functions import user
|
||||
|
||||
|
||||
from .. import data
|
||||
from ..stream_boundaries import validate_stream_boundary
|
||||
from ..stream_queries import StreamQuery
|
||||
|
@ -23,6 +26,7 @@ logger.setLevel(logging.WARN)
|
|||
|
||||
|
||||
event_type = "ethereum_blockchain"
|
||||
allowed_tags = ["tag:erc721"]
|
||||
|
||||
description = f"""Event provider for transactions from the Ethereum blockchain.
|
||||
|
||||
|
@ -79,6 +83,7 @@ class Filters:
|
|||
|
||||
from_addresses: List[str] = field(default_factory=list)
|
||||
to_addresses: List[str] = field(default_factory=list)
|
||||
labels: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
def default_filters(subscriptions: List[BugoutResource]) -> Filters:
|
||||
|
@ -91,8 +96,11 @@ def default_filters(subscriptions: List[BugoutResource]) -> Filters:
|
|||
Optional[str], subscription.resource_data.get("address")
|
||||
)
|
||||
if subscription_address is not None:
|
||||
filters.from_addresses.append(subscription_address)
|
||||
filters.to_addresses.append(subscription_address)
|
||||
if subscription_address in allowed_tags:
|
||||
filters.labels.append(subscription_address.split(":")[1])
|
||||
else:
|
||||
filters.from_addresses.append(subscription_address)
|
||||
filters.to_addresses.append(subscription_address)
|
||||
else:
|
||||
logger.warn(
|
||||
f"Could not find subscription address for subscription with resource id: {subscription.id}"
|
||||
|
@ -157,14 +165,20 @@ def parse_filters(
|
|||
parsed_filters.from_addresses.append(address)
|
||||
parsed_filters.to_addresses.append(address)
|
||||
|
||||
if not (parsed_filters.from_addresses or parsed_filters.to_addresses):
|
||||
if not (
|
||||
parsed_filters.from_addresses
|
||||
or parsed_filters.to_addresses
|
||||
or parsed_filters.labels
|
||||
):
|
||||
return None
|
||||
|
||||
return parsed_filters
|
||||
|
||||
|
||||
def query_ethereum_transactions(
|
||||
db_session: Session, stream_boundary: data.StreamBoundary, parsed_filters: Filters
|
||||
db_session: Session,
|
||||
stream_boundary: data.StreamBoundary,
|
||||
parsed_filters: Filters,
|
||||
) -> Query:
|
||||
"""
|
||||
Builds a database query for Ethereum transactions that occurred within the window of time that
|
||||
|
@ -198,15 +212,41 @@ def query_ethereum_transactions(
|
|||
query = query.filter(EthereumBlock.timestamp <= stream_boundary.end_time)
|
||||
|
||||
# We want to take a big disjunction (OR) over ALL the filters, be they on "from" address or "to" address
|
||||
address_clauses = [
|
||||
EthereumTransaction.from_address == address
|
||||
for address in parsed_filters.from_addresses
|
||||
] + [
|
||||
EthereumTransaction.to_address == address
|
||||
for address in parsed_filters.to_addresses
|
||||
]
|
||||
if address_clauses:
|
||||
query = query.filter(or_(*address_clauses))
|
||||
address_clauses = []
|
||||
|
||||
address_clauses.extend(
|
||||
[
|
||||
EthereumTransaction.from_address == address
|
||||
for address in parsed_filters.from_addresses
|
||||
]
|
||||
+ [
|
||||
EthereumTransaction.to_address == address
|
||||
for address in parsed_filters.to_addresses
|
||||
]
|
||||
)
|
||||
|
||||
labels_clause = []
|
||||
|
||||
if parsed_filters.labels:
|
||||
label_clause = (
|
||||
db_session.query(EthereumAddress)
|
||||
.join(EthereumLabel, EthereumAddress.id == EthereumLabel.address_id)
|
||||
.filter(
|
||||
or_(
|
||||
*[
|
||||
EthereumLabel.label.contains(label)
|
||||
for label in list(set(parsed_filters.labels))
|
||||
]
|
||||
)
|
||||
)
|
||||
.exists()
|
||||
)
|
||||
labels_clause.append(label_clause)
|
||||
|
||||
subscriptions_clause = address_clauses + labels_clause
|
||||
|
||||
if subscriptions_clause:
|
||||
query = query.filter(or_(*subscriptions_clause))
|
||||
|
||||
return query
|
||||
|
||||
|
@ -353,8 +393,7 @@ def next_event(
|
|||
query_ethereum_transactions(db_session, next_stream_boundary, parsed_filters)
|
||||
.order_by(text("timestamp asc"))
|
||||
.limit(1)
|
||||
.one_or_none()
|
||||
)
|
||||
).one_or_none()
|
||||
|
||||
if maybe_ethereum_transaction is None:
|
||||
return None
|
||||
|
@ -394,9 +433,7 @@ def previous_event(
|
|||
)
|
||||
.order_by(text("timestamp desc"))
|
||||
.limit(1)
|
||||
.one_or_none()
|
||||
)
|
||||
|
||||
).one_or_none()
|
||||
if maybe_ethereum_transaction is None:
|
||||
return None
|
||||
return ethereum_transaction_event(maybe_ethereum_transaction)
|
||||
|
|
|
@ -7,7 +7,6 @@ import uuid
|
|||
|
||||
from bugout.data import BugoutToken, BugoutUser, BugoutResource, BugoutUserTokens
|
||||
from bugout.exceptions import BugoutResponseException
|
||||
|
||||
from fastapi import (
|
||||
Body,
|
||||
FastAPI,
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
"""
|
||||
Moonstream's /whales endpoints.
|
||||
|
||||
These endpoints provide public access to whale watch summaries. No authentication required.
|
||||
"""
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
from bugout.data import BugoutResource
|
||||
|
||||
from fastapi import Depends, FastAPI, Query
|
||||
from moonstreamdb import db
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .. import data
|
||||
from ..providers.bugout import whalewatch_provider
|
||||
from ..settings import (
|
||||
bugout_client,
|
||||
DOCS_TARGET_PATH,
|
||||
MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
MOONSTREAM_DATA_JOURNAL_ID,
|
||||
ORIGINS,
|
||||
)
|
||||
from ..stream_queries import StreamQuery
|
||||
from ..version import MOONSTREAM_VERSION
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
tags_metadata = [
|
||||
{"name": "whales", "description": "Whales summaries"},
|
||||
]
|
||||
|
||||
app = FastAPI(
|
||||
title=f"Moonstream /whales API",
|
||||
description="User, token and password handlers.",
|
||||
version=MOONSTREAM_VERSION,
|
||||
openapi_tags=tags_metadata,
|
||||
openapi_url="/openapi.json",
|
||||
docs_url=None,
|
||||
redoc_url=f"/{DOCS_TARGET_PATH}",
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=ORIGINS,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
@app.get("/", tags=["whales"], response_model=data.GetEventsResponse)
|
||||
async def stream_handler(
|
||||
start_time: int = Query(0),
|
||||
end_time: Optional[int] = Query(None),
|
||||
include_start: bool = Query(False),
|
||||
include_end: bool = Query(False),
|
||||
db_session: Session = Depends(db.yield_db_session),
|
||||
) -> data.GetEventsResponse:
|
||||
"""
|
||||
Retrieves the list of whales spotted over given stream boundary
|
||||
|
||||
- **start_time**: Timestamp. Must be provided otherwise this request will hang
|
||||
- **end_time**: Timestamp. Optional.
|
||||
- **include_start** (string): is start_time inclusive or not
|
||||
- **include_end** (string): is end_time inclusive or not
|
||||
"""
|
||||
stream_boundary = data.StreamBoundary(
|
||||
start_time=start_time,
|
||||
end_time=end_time,
|
||||
include_start=include_start,
|
||||
include_end=include_end,
|
||||
)
|
||||
|
||||
result = whalewatch_provider.get_events(
|
||||
db_session=db_session,
|
||||
bugout_client=bugout_client,
|
||||
data_journal_id=MOONSTREAM_DATA_JOURNAL_ID,
|
||||
data_access_token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
stream_boundary=stream_boundary,
|
||||
user_subscriptions={whalewatch_provider.event_type: []},
|
||||
query=StreamQuery(subscription_types=[whalewatch_provider.event_type]),
|
||||
)
|
||||
|
||||
if result is None:
|
||||
return data.GetEventsResponse(stream_boundary=stream_boundary, events=[])
|
||||
|
||||
provider_stream_boundary, events = result
|
||||
return data.GetEventsResponse(
|
||||
stream_boundary=provider_stream_boundary, events=events
|
||||
)
|
|
@ -46,6 +46,10 @@ for path in MOONSTREAM_OPENAPI_LIST:
|
|||
|
||||
DEFAULT_STREAM_TIMEINTERVAL = 5 * 60
|
||||
|
||||
ETHTXPOOL_HUMBUG_CLIENT_ID = os.environ.get(
|
||||
"ETHTXPOOL_HUMBUG_CLIENT_ID", "client:ethereum-txpool-crawler-0"
|
||||
)
|
||||
|
||||
# S3 Bucket
|
||||
ETHERSCAN_SMARTCONTRACTS_BUCKET = os.environ.get("AWS_S3_SMARTCONTRACT_BUCKET")
|
||||
if ETHERSCAN_SMARTCONTRACTS_BUCKET is None:
|
||||
|
|
|
@ -9,3 +9,4 @@ export AWS_S3_SMARTCONTRACT_BUCKET="<AWS S3 bucket to store smart contracts>"
|
|||
export BUGOUT_BROOD_URL="https://auth.bugout.dev"
|
||||
export BUGOUT_SPIRE_URL="https://spire.bugout.dev"
|
||||
export HUMBUG_REPORTER_BACKEND_TOKEN="<Bugout Humbug token for crash reports>"
|
||||
export ETHTXPOOL_HUMBUG_CLIENT_ID="<Bugout Humbug client id for txpool transactions in journal>"
|
|
@ -0,0 +1,147 @@
|
|||
# Created by https://www.toptal.com/developers/gitignore/api/python
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=python
|
||||
|
||||
### Python ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/python
|
||||
|
||||
.moonstream-py/
|
||||
.venv/
|
|
@ -0,0 +1,13 @@
|
|||
# Moonstream Python client
|
||||
|
||||
This is the Python client library for the Moonstream API.
|
||||
|
||||
## Installation
|
||||
|
||||
This library assumes you are using Python 3.6 or greater.
|
||||
|
||||
Install using `pip`:
|
||||
|
||||
```bash
|
||||
pip install moonstream
|
||||
```
|
|
@ -0,0 +1,424 @@
|
|||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import requests
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
log_level = logging.INFO
|
||||
if os.environ.get("DEBUG", "").lower() in ["true", "1"]:
|
||||
log_level = logging.DEBUG
|
||||
logger.setLevel(log_level)
|
||||
|
||||
|
||||
# Keep this synchronized with the version in setup.py
|
||||
CLIENT_VERSION = "0.0.2"
|
||||
|
||||
ENDPOINT_PING = "/ping"
|
||||
ENDPOINT_VERSION = "/version"
|
||||
ENDPOINT_NOW = "/now"
|
||||
ENDPOINT_TOKEN = "/users/token"
|
||||
ENDPOINT_SUBSCRIPTIONS = "/subscriptions/"
|
||||
ENDPOINT_SUBSCRIPTION_TYPES = "/subscriptions/types"
|
||||
ENDPOINT_STREAMS = "/streams/"
|
||||
ENDPOINT_STREAMS_LATEST = "/streams/latest"
|
||||
ENDPOINT_STREAMS_NEXT = "/streams/next"
|
||||
ENDPOINT_STREAMS_PREVIOUS = "/streams/previous"
|
||||
|
||||
ENDPOINTS = [
|
||||
ENDPOINT_PING,
|
||||
ENDPOINT_VERSION,
|
||||
ENDPOINT_NOW,
|
||||
ENDPOINT_TOKEN,
|
||||
ENDPOINT_SUBSCRIPTIONS,
|
||||
ENDPOINT_SUBSCRIPTION_TYPES,
|
||||
ENDPOINT_STREAMS,
|
||||
ENDPOINT_STREAMS_LATEST,
|
||||
ENDPOINT_STREAMS_NEXT,
|
||||
ENDPOINT_STREAMS_PREVIOUS,
|
||||
]
|
||||
|
||||
|
||||
def moonstream_endpoints(url: str) -> Dict[str, str]:
|
||||
"""
|
||||
Creates a dictionary of Moonstream API endpoints at the given Moonstream API URL.
|
||||
"""
|
||||
url_with_protocol = url
|
||||
if not (
|
||||
url_with_protocol.startswith("http://")
|
||||
or url_with_protocol.startswith("https://")
|
||||
):
|
||||
url_with_protocol = f"http://{url_with_protocol}"
|
||||
|
||||
normalized_url = url_with_protocol.rstrip("/")
|
||||
|
||||
return {endpoint: f"{normalized_url}{endpoint}" for endpoint in ENDPOINTS}
|
||||
|
||||
|
||||
class UnexpectedResponse(Exception):
|
||||
"""
|
||||
Raised when a server response cannot be parsed into the appropriate/expected Python structure.
|
||||
"""
|
||||
|
||||
|
||||
class Unauthenticated(Exception):
|
||||
"""
|
||||
Raised when a user tries to make a request that needs to be authenticated by they are not authenticated.
|
||||
"""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class APISpec:
|
||||
url: str
|
||||
endpoints: Dict[str, str]
|
||||
|
||||
|
||||
class Moonstream:
|
||||
"""
|
||||
A Moonstream client configured to communicate with a given Moonstream API server.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
url: str = "https://api.moonstream.to",
|
||||
timeout: Optional[float] = None,
|
||||
):
|
||||
"""
|
||||
Initializes a Moonstream API client.
|
||||
|
||||
Arguments:
|
||||
url - Moonstream API URL. By default this points to the production Moonstream API at https://api.moonstream.to,
|
||||
but you can replace it with the URL of any other Moonstream API instance.
|
||||
timeout - Timeout (in seconds) for Moonstream API requests. Default is None, which means that
|
||||
Moonstream API requests will never time out.
|
||||
|
||||
Returns: A Moonstream client.
|
||||
"""
|
||||
endpoints = moonstream_endpoints(url)
|
||||
self.api = APISpec(url=url, endpoints=endpoints)
|
||||
self.timeout = timeout
|
||||
self._session = requests.Session()
|
||||
self._session.headers.update(
|
||||
{"User-Agent": f"Moonstream Python client (version {CLIENT_VERSION})"}
|
||||
)
|
||||
|
||||
def ping(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Checks that you have a connection to the Moonstream API.
|
||||
"""
|
||||
r = self._session.get(self.api.endpoints[ENDPOINT_PING])
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def version(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Gets the Moonstream API version information from the server.
|
||||
"""
|
||||
r = self._session.get(self.api.endpoints[ENDPOINT_VERSION])
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def server_time(self) -> float:
|
||||
"""
|
||||
Gets the current time (as microseconds since the Unix epoch) on the server.
|
||||
"""
|
||||
r = self._session.get(self.api.endpoints[ENDPOINT_NOW])
|
||||
r.raise_for_status()
|
||||
result = r.json()
|
||||
raw_epoch_time = result.get("epoch_time")
|
||||
if raw_epoch_time is None:
|
||||
raise UnexpectedResponse(
|
||||
f'Server response does not contain "epoch_time": {result}'
|
||||
)
|
||||
|
||||
try:
|
||||
epoch_time = float(raw_epoch_time)
|
||||
except:
|
||||
raise UnexpectedResponse(
|
||||
f"Could not process epoch time as a float: {raw_epoch_time}"
|
||||
)
|
||||
|
||||
return epoch_time
|
||||
|
||||
def authorize(self, access_token: str) -> None:
|
||||
if not access_token:
|
||||
logger.warning("Setting authorization header to empty token.")
|
||||
self._session.headers.update({"Authorization": f"Bearer {access_token}"})
|
||||
|
||||
def requires_authorization(self):
|
||||
if self._session.headers.get("Authorization") is None:
|
||||
raise Unauthenticated(
|
||||
'This method requires that you authenticate to the API, either by calling the "authorize" method with an API token or by calling the "login" method.'
|
||||
)
|
||||
|
||||
def login(self, username: str, password: Optional[str] = None) -> str:
|
||||
"""
|
||||
Authorizes this client to act as the given user when communicating with the Moonstream API.
|
||||
|
||||
To register an account on the production Moonstream API, go to https://moonstream.to.
|
||||
|
||||
Arguments:
|
||||
username - Username of the user to authenticate as.
|
||||
password - Optional password for the user. If this is not provided, you will be prompted for
|
||||
the password.
|
||||
"""
|
||||
if password is None:
|
||||
password = input(f"Moonstream password for {username}: ")
|
||||
|
||||
r = self._session.post(
|
||||
self.api.endpoints[ENDPOINT_TOKEN],
|
||||
data={"username": username, "password": password},
|
||||
)
|
||||
r.raise_for_status()
|
||||
|
||||
token = r.json()
|
||||
self.authorize(token["id"])
|
||||
return token
|
||||
|
||||
def logout(self) -> None:
|
||||
"""
|
||||
Logs the current user out of the Moonstream client.
|
||||
"""
|
||||
self._session.delete(self.api.endpoints[ENDPOINT_TOKEN])
|
||||
self._session.headers.pop("Authorization")
|
||||
|
||||
def subscription_types(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Gets the currently available subscription types on the Moonstream API.
|
||||
"""
|
||||
r = self._session.get(self.api.endpoints[ENDPOINT_SUBSCRIPTION_TYPES])
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def list_subscriptions(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Gets the currently authorized user's subscriptions from the API server.
|
||||
"""
|
||||
self.requires_authorization()
|
||||
r = self._session.get(self.api.endpoints[ENDPOINT_SUBSCRIPTIONS])
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def create_subscription(
|
||||
self, subscription_type: str, label: str, color: str, specifier: str = ""
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Creates a subscription.
|
||||
|
||||
Arguments:
|
||||
subscription_type - The type of subscription you would like to create. To see the available subscription
|
||||
types, call the "subscription_types" method on this Moonstream client. This argument must be
|
||||
the "id" if the subscription type you want.
|
||||
label - A label for the subscription. This will identify the subscription to you in your stream.
|
||||
color - A hexadecimal color to associate with the subscription.
|
||||
specifier - A specifier for the subscription, which must correspond to one of the choices in the
|
||||
subscription type. This is optional because some subscription types do not require a specifier.
|
||||
|
||||
Returns: The subscription resource that was created on the backend.
|
||||
"""
|
||||
self.requires_authorization()
|
||||
r = self._session.post(
|
||||
self.api.endpoints[ENDPOINT_SUBSCRIPTIONS],
|
||||
data={
|
||||
"subscription_type_id": subscription_type,
|
||||
"label": label,
|
||||
"color": color,
|
||||
"address": specifier,
|
||||
},
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def delete_subscription(self, id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Delete a subscription by ID.
|
||||
|
||||
Arguments:
|
||||
id - ID of the subscription to delete.
|
||||
|
||||
Returns: The subscription resource that was deleted.
|
||||
"""
|
||||
self.requires_authorization()
|
||||
r = self._session.delete(f"{self.api.endpoints[ENDPOINT_SUBSCRIPTIONS]}{id}")
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def update_subscription(
|
||||
self, id: str, label: Optional[str] = None, color: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Update a subscription label or color.
|
||||
|
||||
Arguments:
|
||||
label - New label for subscription (optional).
|
||||
color - New color for subscription (optional).
|
||||
|
||||
Returns - If neither label or color are specified, raises a ValueError. Otherwise PUTs the updated
|
||||
information to the server and returns the updated subscription resource.
|
||||
"""
|
||||
if label is None and color is None:
|
||||
raise ValueError(
|
||||
"At least one of the arguments to this method should not be None."
|
||||
)
|
||||
self.requires_authorization()
|
||||
data = {}
|
||||
if label is not None:
|
||||
data["label"] = label
|
||||
if color is not None:
|
||||
data["color"] = color
|
||||
|
||||
r = self._session.put(
|
||||
f"{self.api.endpoints[ENDPOINT_SUBSCRIPTIONS]}{id}", data=data
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def latest_events(self, q: str = "") -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Returns the latest events in your stream. You can optionally provide a query parameter to
|
||||
constrain the query to specific subscription types or to specific subscriptions.
|
||||
|
||||
Arguments:
|
||||
- q - Optional query (default is the empty string). The syntax to constrain to a particular
|
||||
type of subscription is "type:<subscription_type>". For example, to get the latest event from
|
||||
your Ethereum transaction pool subscriptions, you would use "type:ethereum_txpool".
|
||||
|
||||
Returns: A list of the latest events in your stream.
|
||||
"""
|
||||
self.requires_authorization()
|
||||
query_params: Dict[str, str] = {}
|
||||
if q:
|
||||
query_params["q"] = q
|
||||
r = self._session.get(
|
||||
self.api.endpoints[ENDPOINT_STREAMS_LATEST], params=query_params
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def next_event(
|
||||
self, end_time: int, include_end: bool = True, q: str = ""
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Return the earliest event in your stream that occurred after the given end_time.
|
||||
|
||||
Arguments:
|
||||
- end_time - Time after which you want to retrieve the earliest event from your stream.
|
||||
- include_end - If True, the result is the first event that occurred in your stream strictly
|
||||
*after* the end time. If False, then you will get the first event that occurred in your
|
||||
stream *on* or *after* the end time.
|
||||
- q - Optional query to filter over your available subscriptions and subscription types.
|
||||
|
||||
Returns: None if no event has occurred after the given end time, else returns a dictionary
|
||||
representing that event.
|
||||
"""
|
||||
self.requires_authorization()
|
||||
query_params: Dict[str, Any] = {
|
||||
"end_time": end_time,
|
||||
"include_end": include_end,
|
||||
}
|
||||
if q:
|
||||
query_params["q"] = q
|
||||
r = self._session.get(
|
||||
self.api.endpoints[ENDPOINT_STREAMS_NEXT], params=query_params
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def previous_event(
|
||||
self, start_time: int, include_start: bool = True, q: str = ""
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Return the latest event in your stream that occurred before the given start_time.
|
||||
|
||||
Arguments:
|
||||
- start_time - Time before which you want to retrieve the latest event from your stream.
|
||||
- include_start - If True, the result is the last event that occurred in your stream strictly
|
||||
*before* the start time. If False, then you will get the last event that occurred in your
|
||||
stream *on* or *before* the start time.
|
||||
- q - Optional query to filter over your available subscriptions and subscription types.
|
||||
|
||||
Returns: None if no event has occurred before the given start time, else returns a dictionary
|
||||
representing that event.
|
||||
"""
|
||||
self.requires_authorization()
|
||||
query_params: Dict[str, Any] = {
|
||||
"start_time": start_time,
|
||||
"include_start": include_start,
|
||||
}
|
||||
if q:
|
||||
query_params["q"] = q
|
||||
r = self._session.get(
|
||||
self.api.endpoints[ENDPOINT_STREAMS_PREVIOUS], params=query_params
|
||||
)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
def events(
|
||||
self,
|
||||
start_time: int,
|
||||
end_time: int,
|
||||
include_start: bool = False,
|
||||
include_end: bool = False,
|
||||
q: str = "",
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Return all events in your stream that occurred between the given start and end times.
|
||||
|
||||
Arguments:
|
||||
- start_time - Time after which you want to query your stream.
|
||||
- include_start - Whether or not events that occurred exactly at the start_time should be included in the results.
|
||||
- end_time - Time before which you want to query your stream.
|
||||
- include_end - Whether or not events that occurred exactly at the end_time should be included in the results.
|
||||
- q - Optional query to filter over your available subscriptions and subscription types.
|
||||
|
||||
Returns: A dictionary representing the results of your query.
|
||||
"""
|
||||
self.requires_authorization()
|
||||
query_params: Dict[str, Any] = {
|
||||
"start_time": start_time,
|
||||
"include_start": include_start,
|
||||
"end_time": end_time,
|
||||
"include_end": include_end,
|
||||
}
|
||||
if q:
|
||||
query_params["q"] = q
|
||||
|
||||
r = self._session.get(self.api.endpoints[ENDPOINT_STREAMS], params=query_params)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
||||
|
||||
def client_from_env() -> Moonstream:
|
||||
"""
|
||||
Produces a Moonstream client instantiated using the following environment variables:
|
||||
- MOONSTREAM_API_URL: Specifies the url parameter on the Moonstream client
|
||||
- MOONSTREAM_TIMEOUT_SECONDS: Specifies the request timeout
|
||||
- MOONSTREAM_ACCESS_TOKEN: If this environment variable is defined, the client sets this token as
|
||||
the authorization header for all Moonstream API requests.
|
||||
"""
|
||||
kwargs: Dict[str, Any] = {}
|
||||
|
||||
url = os.environ.get("MOONSTREAM_API_URL")
|
||||
if url is not None:
|
||||
kwargs["url"] = url
|
||||
|
||||
raw_timeout = os.environ.get("MOONSTREAM_TIMEOUT_SECONDS")
|
||||
timeout: Optional[float] = None
|
||||
if raw_timeout is not None:
|
||||
try:
|
||||
timeout = float(raw_timeout)
|
||||
except:
|
||||
raise ValueError(
|
||||
f"Could not convert MOONSTREAM_TIMEOUT_SECONDS ({raw_timeout}) to float."
|
||||
)
|
||||
|
||||
kwargs["timeout"] = timeout
|
||||
|
||||
moonstream_client = Moonstream(**kwargs)
|
||||
|
||||
access_token = os.environ.get("MOONSTREAM_ACCESS_TOKEN")
|
||||
if access_token is not None:
|
||||
moonstream_client.authorize(access_token)
|
||||
|
||||
return moonstream_client
|
|
@ -0,0 +1,138 @@
|
|||
from dataclasses import FrozenInstanceError
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from . import client
|
||||
|
||||
|
||||
class TestMoonstreamClient(unittest.TestCase):
|
||||
def test_client_init(self):
|
||||
m = client.Moonstream()
|
||||
self.assertEqual(m.api.url, "https://api.moonstream.to")
|
||||
self.assertIsNone(m.timeout)
|
||||
self.assertGreater(len(m.api.endpoints), 0)
|
||||
|
||||
def test_client_init_with_timeout(self):
|
||||
timeout = 7
|
||||
m = client.Moonstream(timeout=timeout)
|
||||
self.assertEqual(m.api.url, "https://api.moonstream.to")
|
||||
self.assertEqual(m.timeout, timeout)
|
||||
self.assertGreater(len(m.api.endpoints), 0)
|
||||
|
||||
def test_client_with_custom_url_and_timeout(self):
|
||||
timeout = 9
|
||||
url = "https://my.custom.api.url"
|
||||
m = client.Moonstream(url=url, timeout=timeout)
|
||||
self.assertEqual(m.api.url, url)
|
||||
self.assertEqual(m.timeout, timeout)
|
||||
self.assertGreater(len(m.api.endpoints), 0)
|
||||
|
||||
def test_client_with_custom_messy_url_and_timeout(self):
|
||||
timeout = 3.5
|
||||
url = "https://my.custom.api.url/"
|
||||
m = client.Moonstream(url=url, timeout=timeout)
|
||||
self.assertEqual(m.api.url, url)
|
||||
self.assertEqual(m.timeout, timeout)
|
||||
self.assertGreater(len(m.api.endpoints), 0)
|
||||
|
||||
def test_client_with_custom_messy_url_no_protocol_and_timeout(self):
|
||||
timeout = 5.5
|
||||
url = "my.custom.api.url/"
|
||||
m = client.Moonstream(url=url, timeout=timeout)
|
||||
self.assertEqual(m.api.url, url)
|
||||
self.assertEqual(m.timeout, timeout)
|
||||
self.assertGreater(len(m.api.endpoints), 0)
|
||||
|
||||
def test_immutable_api_url(self):
|
||||
m = client.Moonstream()
|
||||
with self.assertRaises(FrozenInstanceError):
|
||||
m.api.url = "lol"
|
||||
|
||||
def test_immutable_api_endpoints(self):
|
||||
m = client.Moonstream()
|
||||
with self.assertRaises(FrozenInstanceError):
|
||||
m.api.endpoints = {}
|
||||
|
||||
def test_mutable_timeout(self):
|
||||
original_timeout = 5.0
|
||||
updated_timeout = 10.5
|
||||
m = client.Moonstream(timeout=original_timeout)
|
||||
self.assertEqual(m.timeout, original_timeout)
|
||||
m.timeout = updated_timeout
|
||||
self.assertEqual(m.timeout, updated_timeout)
|
||||
|
||||
|
||||
class TestMoonstreamClientFromEnv(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.old_moonstream_api_url = os.environ.get("MOONSTREAM_API_URL")
|
||||
self.old_moonstream_timeout_seconds = os.environ.get(
|
||||
"MOONSTREAM_TIMEOUT_SECONDS"
|
||||
)
|
||||
self.old_moonstream_access_token = os.environ.get("MOONSTREAM_ACCESS_TOKEN")
|
||||
|
||||
self.moonstream_api_url = "https://custom.example.com"
|
||||
self.moonstream_timeout_seconds = 15.333333
|
||||
self.moonstream_access_token = "1d431ca4-af9b-4c3a-b7b9-3cc79f3b0900"
|
||||
|
||||
os.environ["MOONSTREAM_API_URL"] = self.moonstream_api_url
|
||||
os.environ["MOONSTREAM_TIMEOUT_SECONDS"] = str(self.moonstream_timeout_seconds)
|
||||
os.environ["MOONSTREAM_ACCESS_TOKEN"] = self.moonstream_access_token
|
||||
|
||||
def tearDown(self) -> None:
|
||||
del os.environ["MOONSTREAM_API_URL"]
|
||||
del os.environ["MOONSTREAM_TIMEOUT_SECONDS"]
|
||||
del os.environ["MOONSTREAM_ACCESS_TOKEN"]
|
||||
|
||||
if self.old_moonstream_api_url is not None:
|
||||
os.environ["MOONSTREAM_API_URL"] = self.old_moonstream_api_url
|
||||
if self.old_moonstream_timeout_seconds is not None:
|
||||
os.environ[
|
||||
"MOONSTREAM_TIMEOUT_SECONDS"
|
||||
] = self.old_moonstream_timeout_seconds
|
||||
if self.old_moonstream_access_token is not None:
|
||||
os.environ["MOONSTREAM_ACCESS_TOKEN"] = self.old_moonstream_access_token
|
||||
|
||||
def test_client_from_env(self):
|
||||
m = client.client_from_env()
|
||||
self.assertEqual(m.api.url, self.moonstream_api_url)
|
||||
self.assertEqual(m.timeout, self.moonstream_timeout_seconds)
|
||||
self.assertIsNone(m.requires_authorization())
|
||||
|
||||
authorization_header = m._session.headers["Authorization"]
|
||||
self.assertEqual(authorization_header, f"Bearer {self.moonstream_access_token}")
|
||||
|
||||
|
||||
class TestMoonstreamEndpoints(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.url = "https://api.moonstream.to"
|
||||
self.normalized_url = "https://api.moonstream.to"
|
||||
|
||||
def test_moonstream_endpoints(self):
|
||||
endpoints = client.moonstream_endpoints(self.url)
|
||||
self.assertDictEqual(
|
||||
endpoints,
|
||||
{
|
||||
client.ENDPOINT_PING: f"{self.normalized_url}{client.ENDPOINT_PING}",
|
||||
client.ENDPOINT_VERSION: f"{self.normalized_url}{client.ENDPOINT_VERSION}",
|
||||
client.ENDPOINT_NOW: f"{self.normalized_url}{client.ENDPOINT_NOW}",
|
||||
client.ENDPOINT_TOKEN: f"{self.normalized_url}{client.ENDPOINT_TOKEN}",
|
||||
client.ENDPOINT_SUBSCRIPTION_TYPES: f"{self.normalized_url}{client.ENDPOINT_SUBSCRIPTION_TYPES}",
|
||||
client.ENDPOINT_SUBSCRIPTIONS: f"{self.normalized_url}{client.ENDPOINT_SUBSCRIPTIONS}",
|
||||
client.ENDPOINT_STREAMS: f"{self.normalized_url}{client.ENDPOINT_STREAMS}",
|
||||
client.ENDPOINT_STREAMS_LATEST: f"{self.normalized_url}{client.ENDPOINT_STREAMS_LATEST}",
|
||||
client.ENDPOINT_STREAMS_NEXT: f"{self.normalized_url}{client.ENDPOINT_STREAMS_NEXT}",
|
||||
client.ENDPOINT_STREAMS_PREVIOUS: f"{self.normalized_url}{client.ENDPOINT_STREAMS_PREVIOUS}",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class TestMoonstreamEndpointsMessyURL(TestMoonstreamEndpoints):
|
||||
def setUp(self):
|
||||
self.url = "https://api.moonstream.to/"
|
||||
self.normalized_url = "https://api.moonstream.to"
|
||||
|
||||
|
||||
class TestMoonstreamEndpointsMessyURLWithNoProtocol(TestMoonstreamEndpoints):
|
||||
def setUp(self):
|
||||
self.url = "api.moonstream.to/"
|
||||
self.normalized_url = "http://api.moonstream.to"
|
|
@ -0,0 +1,35 @@
|
|||
from setuptools import find_packages, setup
|
||||
|
||||
long_description = ""
|
||||
with open("README.md") as ifp:
|
||||
long_description = ifp.read()
|
||||
|
||||
setup(
|
||||
name="moonstream",
|
||||
version="0.0.2",
|
||||
packages=find_packages(),
|
||||
package_data={"moonstream": ["py.typed"]},
|
||||
install_requires=["requests", "dataclasses; python_version=='3.6'"],
|
||||
extras_require={
|
||||
"dev": [
|
||||
"black",
|
||||
"mypy",
|
||||
"wheel",
|
||||
"types-requests",
|
||||
"types-dataclasses",
|
||||
],
|
||||
"distribute": ["setuptools", "twine", "wheel"],
|
||||
},
|
||||
description="Moonstream: Open source blockchain analytics",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
author="Moonstream",
|
||||
author_email="engineering@moonstream.to",
|
||||
classifiers=[
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Programming Language :: Python",
|
||||
"License :: OSI Approved :: Apache Software License",
|
||||
"Topic :: Software Development :: Libraries",
|
||||
],
|
||||
url="https://github.com/bugout-dev/moonstream",
|
||||
)
|
|
@ -0,0 +1,12 @@
|
|||
#!/usr/bin/env bash
|
||||
set -e
|
||||
TAG="clients/python/v$(python setup.py --version)"
|
||||
read -r -p "Tag: $TAG -- tag and push (y/n)?" ACCEPT
|
||||
if [ "$ACCEPT" = "y" ]
|
||||
then
|
||||
echo "Tagging and pushing: $TAG..."
|
||||
git tag "$TAG"
|
||||
git push upstream "$TAG"
|
||||
else
|
||||
echo "noop"
|
||||
fi
|
|
@ -19,6 +19,7 @@ ETHEREUM_SYNCHRONIZE_SERVICE="ethereum-synchronize.service"
|
|||
ETHEREUM_TRENDING_SERVICE="ethereum-trending.service"
|
||||
ETHEREUM_TRENDING_TIMER="ethereum-trending.service"
|
||||
ETHEREUM_TXPOOL_SERVICE="ethereum-txpool.service"
|
||||
SERVICE_FILE="moonstreamcrawlers.service"
|
||||
|
||||
set -eu
|
||||
|
||||
|
@ -30,6 +31,14 @@ cd "${APP_CRAWLERS_DIR}/ethtxpool"
|
|||
HOME=/root /usr/local/go/bin/go build -o "${APP_CRAWLERS_DIR}/ethtxpool/ethtxpool" "${APP_CRAWLERS_DIR}/ethtxpool/main.go"
|
||||
cd "${EXEC_DIR}"
|
||||
|
||||
echo
|
||||
echo
|
||||
echo "Building executable server of moonstreamcrawlers with Go"
|
||||
EXEC_DIR=$(pwd)
|
||||
cd "${APP_CRAWLERS_DIR}/server"
|
||||
HOME=/root /usr/local/go/bin/go build -o "${APP_CRAWLERS_DIR}/server/moonstreamcrawlers" "${APP_CRAWLERS_DIR}/server/main.go"
|
||||
cd "${EXEC_DIR}"
|
||||
|
||||
echo
|
||||
echo
|
||||
echo "Updating Python dependencies"
|
||||
|
@ -82,3 +91,12 @@ chmod 644 "${SCRIPT_DIR}/${ETHEREUM_TXPOOL_SERVICE}"
|
|||
cp "${SCRIPT_DIR}/${ETHEREUM_TXPOOL_SERVICE}" "/etc/systemd/system/${ETHEREUM_TXPOOL_SERVICE}"
|
||||
systemctl daemon-reload
|
||||
systemctl restart "${ETHEREUM_TXPOOL_SERVICE}"
|
||||
|
||||
echo
|
||||
echo
|
||||
echo "Replacing existing moonstreamcrawlers service definition with ${SERVICE_FILE}"
|
||||
chmod 644 "${SCRIPT_DIR}/${SERVICE_FILE}"
|
||||
cp "${SCRIPT_DIR}/${SERVICE_FILE}" "/etc/systemd/system/${SERVICE_FILE}"
|
||||
systemctl daemon-reload
|
||||
systemctl restart "${SERVICE_FILE}"
|
||||
systemctl status "${SERVICE_FILE}"
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
[Unit]
|
||||
Description=moonstreamcrawlers-service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=ubuntu
|
||||
Group=www-data
|
||||
WorkingDirectory=/home/ubuntu/moonstream/crawlers/server
|
||||
EnvironmentFile=/home/ubuntu/moonstream-secrets/app.env
|
||||
ExecStart=/home/ubuntu/moonstream/crawlers/server/moonstreamcrawlers -host 0.0.0.0 -port "${MOONSTREAM_CRAWLERS_SERVER_PORT}"
|
||||
SyslogIdentifier=moonstreamcrawlers
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
|
@ -0,0 +1,9 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
# Expects access to Python environment with the requirements for this project installed.
|
||||
set -e
|
||||
|
||||
MOONSTREAM_CRAWLERS_SERVER_HOST="${MOONSTREAM_CRAWLERS_SERVER_HOST:-0.0.0.0}"
|
||||
MOONSTREAM_CRAWLERS_SERVER_PORT="${MOONSTREAM_CRAWLERS_SERVER_PORT:-8080}"
|
||||
|
||||
go run main.go -host "${MOONSTREAM_CRAWLERS_SERVER_HOST}" -port "${MOONSTREAM_CRAWLERS_SERVER_PORT}"
|
|
@ -0,0 +1,3 @@
|
|||
module moonstreamdb
|
||||
|
||||
go 1.17
|
|
@ -0,0 +1,118 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var MOONSTREAM_IPC_PATH = os.Getenv("MOONSTREAM_IPC_PATH")
|
||||
var MOONSTREAM_CORS_ALLOWED_ORIGINS = os.Getenv("MOONSTREAM_CORS_ALLOWED_ORIGINS")
|
||||
|
||||
type GethResponse struct {
|
||||
Result string `json:"result"`
|
||||
}
|
||||
|
||||
type PingGethResponse struct {
|
||||
CurrentBlock uint64 `json:"current_block"`
|
||||
}
|
||||
|
||||
type PingResponse struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// Extends handler with allowed CORS policies
|
||||
func setupCorsResponse(w *http.ResponseWriter, req *http.Request) {
|
||||
for _, allowedOrigin := range strings.Split(MOONSTREAM_CORS_ALLOWED_ORIGINS, ",") {
|
||||
for _, reqOrigin := range req.Header["Origin"] {
|
||||
if reqOrigin == allowedOrigin {
|
||||
(*w).Header().Set("Access-Control-Allow-Origin", allowedOrigin)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
(*w).Header().Set("Access-Control-Allow-Methods", "GET,OPTIONS")
|
||||
}
|
||||
|
||||
func ping(w http.ResponseWriter, req *http.Request) {
|
||||
setupCorsResponse(&w, req)
|
||||
log.Printf("%s, %s, %q", req.RemoteAddr, req.Method, req.URL.String())
|
||||
if (*req).Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
response := PingResponse{Status: "ok"}
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func pingGeth(w http.ResponseWriter, req *http.Request) {
|
||||
setupCorsResponse(&w, req)
|
||||
log.Printf("%s, %s, %q", req.RemoteAddr, req.Method, req.URL.String())
|
||||
if (*req).Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
postBody, err := json.Marshal(map[string]interface{}{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "eth_blockNumber",
|
||||
"params": []string{},
|
||||
"id": 1,
|
||||
})
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
http.Error(w, http.StatusText(500), 500)
|
||||
return
|
||||
}
|
||||
gethResponse, err := http.Post(MOONSTREAM_IPC_PATH, "application/json",
|
||||
bytes.NewBuffer(postBody))
|
||||
if err != nil {
|
||||
log.Printf("Unable to request geth, error: %v", err)
|
||||
http.Error(w, http.StatusText(500), 500)
|
||||
return
|
||||
}
|
||||
defer gethResponse.Body.Close()
|
||||
|
||||
gethResponseBody, err := ioutil.ReadAll(gethResponse.Body)
|
||||
if err != nil {
|
||||
log.Printf("Unable to read geth response, error: %v", err)
|
||||
http.Error(w, http.StatusText(500), 500)
|
||||
return
|
||||
}
|
||||
var obj GethResponse
|
||||
_ = json.Unmarshal(gethResponseBody, &obj)
|
||||
|
||||
blockNumberHex := strings.Replace(obj.Result, "0x", "", -1)
|
||||
blockNumberStr, err := strconv.ParseUint(blockNumberHex, 16, 64)
|
||||
if err != nil {
|
||||
log.Printf("Unable to parse block number from hex to string, error: %v", err)
|
||||
http.Error(w, http.StatusText(500), 500)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
response := PingGethResponse{CurrentBlock: blockNumberStr}
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func main() {
|
||||
var listenAddr string
|
||||
var listenPort string
|
||||
flag.StringVar(&listenAddr, "host", "127.0.0.1", "Server listen address")
|
||||
flag.StringVar(&listenPort, "port", "8080", "Server listen port")
|
||||
flag.Parse()
|
||||
|
||||
address := listenAddr + ":" + listenPort
|
||||
log.Printf("Starting server at %s\n", address)
|
||||
|
||||
http.HandleFunc("/ping", ping)
|
||||
http.HandleFunc("/status", pingGeth)
|
||||
|
||||
http.ListenAndServe(address, nil)
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export MOONSTREAM_CRAWLERS_SERVER_PORT="8080"
|
||||
export MOONSTREAM_IPC_PATH=null
|
||||
export MOONSTREAM_CORS_ALLOWED_ORIGINS="http://localhost:3000,https://moonstream.to,https://www.moonstream.to,https://alpha.moonstream.to"
|
|
@ -9,7 +9,8 @@ from moonstreamdb.db import yield_db_session_ctx
|
|||
|
||||
from .data import event_types, nft_event, BlockBounds
|
||||
from .datastore import setup_database, import_data
|
||||
from .derive import current_owners, current_market_values
|
||||
from .derive import current_owners, current_market_values, current_values_distribution
|
||||
|
||||
from .materialize import create_dataset, EthereumBatchloader
|
||||
|
||||
|
||||
|
@ -17,6 +18,13 @@ logging.basicConfig(level=logging.INFO)
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
derive_functions = {
|
||||
"current_owners": current_owners,
|
||||
"current_market_values": current_market_values,
|
||||
"current_values_distribution": current_values_distribution,
|
||||
}
|
||||
|
||||
|
||||
def handle_initdb(args: argparse.Namespace) -> None:
|
||||
with contextlib.closing(sqlite3.connect(args.datastore)) as conn:
|
||||
setup_database(conn)
|
||||
|
@ -58,8 +66,15 @@ def handle_materialize(args: argparse.Namespace) -> None:
|
|||
|
||||
def handle_derive(args: argparse.Namespace) -> None:
|
||||
with contextlib.closing(sqlite3.connect(args.datastore)) as moonstream_datastore:
|
||||
current_owners(moonstream_datastore)
|
||||
current_market_values(moonstream_datastore)
|
||||
calling_functions = []
|
||||
if not args.derive_functions:
|
||||
calling_functions.extend(derive_functions.keys())
|
||||
else:
|
||||
calling_functions.extend(args.derive_functions)
|
||||
|
||||
for function_name in calling_functions:
|
||||
if function_name in calling_functions:
|
||||
derive_functions[function_name](moonstream_datastore)
|
||||
logger.info("Done!")
|
||||
|
||||
|
||||
|
@ -138,6 +153,13 @@ def main() -> None:
|
|||
required=True,
|
||||
help="Path to SQLite database representing the dataset",
|
||||
)
|
||||
parser_derive.add_argument(
|
||||
"-f",
|
||||
"--derive_functions",
|
||||
required=False,
|
||||
nargs="+",
|
||||
help=f"Functions wich will call from derive module availabel {list(derive_functions.keys())}",
|
||||
)
|
||||
parser_derive.set_defaults(func=handle_derive)
|
||||
|
||||
parser_import_data = subcommands.add_parser(
|
||||
|
|
|
@ -109,4 +109,25 @@ def current_market_values(conn: sqlite3.Connection) -> None:
|
|||
except Exception as e:
|
||||
conn.rollback()
|
||||
logger.error("Could not create derived dataset: current_market_values")
|
||||
|
||||
|
||||
def current_values_distribution(conn: sqlite3.Connection) -> List[Tuple]:
|
||||
"""
|
||||
Requires a connection to a dataset in which current_market_values has already been loaded.
|
||||
"""
|
||||
ensure_custom_aggregate_functions(conn)
|
||||
drop_existing_values_distribution_query = (
|
||||
"DROP TABLE IF EXISTS market_values_distribution;"
|
||||
)
|
||||
current_values_distribution_query = """
|
||||
CREATE TABLE market_values_distribution AS
|
||||
select nft_address as address, market_value as value, CUME_DIST() over (PARTITION BY nft_address ORDER BY market_value) as cumulate_value from current_market_values;"""
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
cur.execute(drop_existing_values_distribution_query)
|
||||
cur.execute(current_values_distribution_query)
|
||||
conn.commit()
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logger.error("Could not create derived dataset: current_values_distribution")
|
||||
logger.error(e)
|
||||
|
|
|
@ -5,15 +5,32 @@
|
|||
# Main
|
||||
APP_DIR="${APP_DIR:-/home/ubuntu/moonstream}"
|
||||
APP_DB_SERVER_DIR="${APP_DIR}/db/server"
|
||||
SECRETS_DIR="${SECRETS_DIR:-/home/ubuntu/moonstream-secrets}"
|
||||
SCRIPT_DIR="$(realpath $(dirname $0))"
|
||||
SERVICE_FILE="${SCRIPT_DIR}/moonstreamdb.service"
|
||||
|
||||
set -eu
|
||||
|
||||
echo
|
||||
echo
|
||||
echo "Retrieving deployment parameters for GCL Secret Manager"
|
||||
mkdir -p "${SECRETS_DIR}"
|
||||
echo "" > "${SECRETS_DIR}/app.env"
|
||||
SECRET_NAMES=$(gcloud beta secrets list --filter="labels.product=moonstream" --format="get(name)")
|
||||
for secret in $SECRET_NAMES
|
||||
do
|
||||
secret_key=$(echo "${secret}" | awk -F'/' '{print $NF}')
|
||||
secret_val=$(gcloud secrets versions access latest --secret="${secret_key}")
|
||||
echo "${secret_key}=\"${secret_val}\"" >> "${SECRETS_DIR}/app.env"
|
||||
done
|
||||
|
||||
echo
|
||||
echo
|
||||
echo "Building executable database server script with Go"
|
||||
EXEC_DIR=$(pwd)
|
||||
cd "${APP_DB_SERVER_DIR}"
|
||||
HOME=/root /usr/local/go/bin/go build -o "${APP_DB_SERVER_DIR}/moonstreamdb" "${APP_DB_SERVER_DIR}/main.go"
|
||||
cd "${EXEC_DIR}"
|
||||
|
||||
echo
|
||||
echo
|
||||
|
|
|
@ -6,7 +6,8 @@ After=network.target
|
|||
User=ubuntu
|
||||
Group=www-data
|
||||
WorkingDirectory=/home/ubuntu/moonstream/db/server
|
||||
ExecStart=/home/ubuntu/moonstream/db/server/moonstreamdb
|
||||
EnvironmentFile=/home/ubuntu/moonstream-secrets/app.env
|
||||
ExecStart=/home/ubuntu/moonstream/db/server/moonstreamdb -host 0.0.0.0 -port "${MOONSTREAM_DB_SERVER_PORT}"
|
||||
SyslogIdentifier=moonstreamdb
|
||||
|
||||
[Install]
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
# Expects access to Python environment with the requirements for this project installed.
|
||||
set -e
|
||||
|
||||
MOONSTREAM_DB_SERVER_HOST="${MOONSTREAM_DB_SERVER_HOST:-0.0.0.0}"
|
||||
MOONSTREAM_DB_SERVER_PORT="${MOONSTREAM_DB_SERVER_PORT:-8080}"
|
||||
|
||||
go run main.go -host "${MOONSTREAM_DB_SERVER_HOST}" -port "${MOONSTREAM_DB_SERVER_PORT}"
|
|
@ -1,3 +1,23 @@
|
|||
module moonstreamdb
|
||||
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
gorm.io/driver/postgres v1.1.1
|
||||
gorm.io/gorm v1.21.15
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||
github.com/jackc/pgconn v1.10.0 // indirect
|
||||
github.com/jackc/pgio v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.1.1 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
||||
github.com/jackc/pgtype v1.8.1 // indirect
|
||||
github.com/jackc/pgx/v4 v4.13.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.2 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
)
|
||||
|
|
|
@ -0,0 +1,186 @@
|
|||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
|
||||
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
|
||||
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
|
||||
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
|
||||
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
|
||||
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
|
||||
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
|
||||
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
|
||||
github.com/jackc/pgconn v1.10.0 h1:4EYhlDVEMsJ30nNj0mmgwIUXoq7e9sMJrVC2ED6QlCU=
|
||||
github.com/jackc/pgconn v1.10.0/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
|
||||
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
||||
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
|
||||
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
|
||||
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
|
||||
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
|
||||
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgproto3/v2 v2.1.1 h1:7PQ/4gLoqnl87ZxL7xjO0DR5gYuviDCZxQJsUlFW1eI=
|
||||
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
|
||||
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
|
||||
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
|
||||
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
|
||||
github.com/jackc/pgtype v1.8.1 h1:9k0IXtdJXHJbyAWQgbWr1lU+MEhPXZz6RIXxfR5oxXs=
|
||||
github.com/jackc/pgtype v1.8.1/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
|
||||
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
|
||||
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
|
||||
github.com/jackc/pgx/v4 v4.13.0 h1:JCjhT5vmhMAf/YwBHLvrBn4OGdIQBiFG6ym8Zmdx570=
|
||||
github.com/jackc/pgx/v4 v4.13.0/go.mod h1:9P4X524sErlaxj0XSGZk7s+LD0eOyu1ZDUrrpznYDF0=
|
||||
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI=
|
||||
github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
|
||||
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
|
||||
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
|
||||
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
|
||||
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
|
||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/postgres v1.1.1 h1:tWLmqYCyaoh89fi7DhM6QggujrOnmfo3H98AzgNAAu0=
|
||||
gorm.io/driver/postgres v1.1.1/go.mod h1:tpe2xN7aCst1NUdYyWQyxPtnHC+Zfp6NEux9PXD1OU0=
|
||||
gorm.io/gorm v1.21.15 h1:gAyaDoPw0lCyrSFWhBlahbUA1U4P5RViC1uIqoB+1Rk=
|
||||
gorm.io/gorm v1.21.15/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
|
@ -2,25 +2,86 @@ package main
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var MOONSTREAM_DB_URI = os.Getenv("MOONSTREAM_DB_URI")
|
||||
var MOONSTREAM_CORS_ALLOWED_ORIGINS = os.Getenv("MOONSTREAM_CORS_ALLOWED_ORIGINS")
|
||||
|
||||
type PingResponse struct {
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type BlockResponse struct {
|
||||
BlockNumber uint64 `json:"block_number"`
|
||||
}
|
||||
|
||||
// Extends handler with allowed CORS policies
|
||||
func setupCorsResponse(w *http.ResponseWriter, req *http.Request) {
|
||||
for _, allowedOrigin := range strings.Split(MOONSTREAM_CORS_ALLOWED_ORIGINS, ",") {
|
||||
for _, reqOrigin := range req.Header["Origin"] {
|
||||
if reqOrigin == allowedOrigin {
|
||||
(*w).Header().Set("Access-Control-Allow-Origin", allowedOrigin)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
(*w).Header().Set("Access-Control-Allow-Methods", "GET,OPTIONS")
|
||||
}
|
||||
|
||||
func ping(w http.ResponseWriter, req *http.Request) {
|
||||
setupCorsResponse(&w, req)
|
||||
log.Printf("%s, %s, %q", req.RemoteAddr, req.Method, req.URL.String())
|
||||
if (*req).Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
response := PingResponse{Status: "ok"}
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func blockLatest(w http.ResponseWriter, req *http.Request) {
|
||||
setupCorsResponse(&w, req)
|
||||
log.Printf("%s, %s, %q", req.RemoteAddr, req.Method, req.URL.String())
|
||||
if (*req).Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
var latestBlock BlockResponse
|
||||
db, err := gorm.Open(postgres.Open(MOONSTREAM_DB_URI), &gorm.Config{})
|
||||
if err != nil {
|
||||
http.Error(w, http.StatusText(500), 500)
|
||||
return
|
||||
}
|
||||
|
||||
query := "SELECT block_number FROM ethereum_blocks ORDER BY block_number DESC LIMIT 1"
|
||||
db.Raw(query, 1).Scan(&latestBlock.BlockNumber)
|
||||
|
||||
json.NewEncoder(w).Encode(latestBlock)
|
||||
}
|
||||
|
||||
func main() {
|
||||
address := "0.0.0.0:8931"
|
||||
fmt.Printf("Starting server at %s\n", address)
|
||||
var listenAddr string
|
||||
var listenPort string
|
||||
flag.StringVar(&listenAddr, "host", "127.0.0.1", "Server listen address")
|
||||
flag.StringVar(&listenPort, "port", "8080", "Server listen port")
|
||||
flag.Parse()
|
||||
|
||||
address := listenAddr + ":" + listenPort
|
||||
log.Printf("Starting server at %s\n", address)
|
||||
|
||||
http.HandleFunc("/ping", ping)
|
||||
http.HandleFunc("/block/latest", blockLatest)
|
||||
|
||||
http.ListenAndServe(address, nil)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export MOONSTREAM_DB_SERVER_PORT="8080"
|
||||
export MOONSTREAM_DB_URI="postgresql://<username>:<password>@<db_host>:<db_port>/<db_name>"
|
||||
export MOONSTREAM_CORS_ALLOWED_ORIGINS="http://localhost:3000,https://moonstream.to,https://www.moonstream.to,https://alpha.moonstream.to"
|
|
@ -59,7 +59,11 @@ const RootLayout = (props) => {
|
|||
>
|
||||
Join early. Our first 1000 users get free lifetime access to
|
||||
blockchain analytics. Contact our team on{" "}
|
||||
<Link href={"https://discord.gg/V3tWaP36"} color="orange.900">
|
||||
<Link
|
||||
isExternal
|
||||
href={"https://discord.gg/K56VNUQGvA"}
|
||||
color="orange.900"
|
||||
>
|
||||
Discord
|
||||
</Link>
|
||||
</Text>
|
||||
|
|
Ładowanie…
Reference in New Issue