· 4 years ago · Jul 11, 2021, 01:56 AM
1import os, tempfile, time, requests, asyncio, discord, sqlite3, hashlib
2from datetime import datetime, timedelta
3from dotmap import DotMap
4from contextlib import contextmanager
5
6CHECK_EVERY_X_SECONDS = 60
7
8async def get_discord_client():
9 client = discord.Client()
10 asyncio.ensure_future(client.start("DISCORD SECRET HERE"))
11 await client.wait_until_ready()
12
13 return client
14
15@contextmanager
16def get_db():
17 db = sqlite3.connect('data.db', isolation_level=None)
18 db.row_factory = sqlite3.Row
19 cur = db.cursor()
20 yield cur
21 cur.close()
22 db.close()
23
24
25def get_opensea_sales(from_timestamp):
26 data = {
27 "id": "EventHistoryPollQuery",
28 "query": "query EventHistoryPollQuery(\n $archetype: ArchetypeInputType\n $categories: [CollectionSlug!]\n $collections: [CollectionSlug!]\n $count: Int = 10\n $cursor: String\n $eventTimestamp_Gt: DateTime\n $eventTypes: [EventType!]\n $identity: IdentityInputType\n $showAll: Boolean = false\n) {\n assetEvents(after: $cursor, archetype: $archetype, categories: $categories, collections: $collections, eventTimestamp_Gt: $eventTimestamp_Gt, eventTypes: $eventTypes, first: $count, identity: $identity) {\n edges {\n node {\n assetBundle @include(if: $showAll) {\n ...AssetCell_assetBundle\n id\n }\n assetQuantity {\n asset @include(if: $showAll) {\n ...AssetCell_asset\n id\n }\n ...quantity_data\n id\n }\n relayId\n eventTimestamp\n eventType\n customEventName\n devFee {\n quantity\n ...AssetQuantity_data\n id\n }\n devFeePaymentEvent {\n ...EventTimestamp_data\n id\n }\n fromAccount {\n address\n ...AccountLink_data\n id\n }\n price {\n quantity\n ...AssetQuantity_data\n id\n }\n endingPrice {\n quantity\n ...AssetQuantity_data\n id\n }\n seller {\n
29 ...AccountLink_data\n id\n }\n toAccount {\n ...AccountLink_data\n id\n }\n winnerAccount {\n
30...AccountLink_data\n id\n }\n ...EventTimestamp_data\n id\n }\n }\n }\n}\n\nfragment AccountLink_data on AccountType {\n address\n chain {\n identifier\n id\n }\n user {\n username\n id\n }\n ...ProfileImage_data\n ...wallet_accountKey\n}\n\nfragment AssetCell_asset on AssetType {\n collection {\n name\n id\n }\n name\n ...AssetMedia_asset\n ...asset_url\n}\n\nfragment AssetCell_assetBundle on AssetBundleType {\n assetQuantities(first: 2) {\n edges {\n node {\n asset {\n collection {\n name\n id\n }\n name\n ...AssetMedia_asset\n ...asset_url\n id\n }\n relayId\n id\n }\n }\n }\n name\n slug\n}\n\nfragment AssetMedia_asset on AssetType {\n animationUrl\n backgroundColor\n collection {\n twitterUsername\n description\n displayData {\n cardDisplayStyle\n }\n imageUrl\n hidden\n name\n slug\n id\n }\n description\n name\n tokenId\n imageUrl\n}\n\nfragment AssetQuantity_data on AssetQuantityType {\n asset {\n ...Price_data\n id\n }\n quantity\n}\n\nfragment EventTimestamp_data on AssetEventType {\n eventTimestamp\n transaction {\n blockExplorerLink\n id\n }\n}\n\nfragment Price_data on AssetType {\n decimals\n imageUrl\n symbol\n usdSpotPrice\n assetContract {\n blockExplorerLink\n id\n }\n}\n\nfragment ProfileImage_data on AccountType {\n imageUrl\n address\n chain {\n identifier\n id\n }\n}\n\nfragment asset_url on AssetType {\n assetContract {\n account {\n address\n chain {\n identifier\n id\n }\n id\n }\n id\n }\n tokenId\n}\n\nfragment quantity_data on AssetQuantityType {\n asset {\n decimals\n id\n }\n quantity\n}\n\nfragment wallet_accountKey on AccountType {\n address\n chain {\n identifier\n id\n }\n}\n",
31 "variables": {
32 "archetype": None,
33 "categories": None,
34 "collections": ["COLLECTION NAME HERE"],
35 "count": 10,
36 "cursor": None,
37 "eventTimestamp_Gt": from_timestamp,
38 "eventTypes": ["AUCTION_SUCCESSFUL"],
39 "identity": None,
40 "showAll": True
41 }
42 }
43 r = requests.post("https://api.opensea.io/graphql/", json=data).json()
44 return DotMap(r).data.assetEvents.edges
45
46
47def get_opensea_rankings():
48 data = {
49 "id": "rankingsQuery",
50 "query": "query rankingsQuery( $chain: [ChainScalar!] $count: Int! $cursor: String $sortBy: CollectionSort $sortAscending: Boolean $parents: [CollectionSlug!] $createdAfter: DateTime) { ...rankings_collections}fragment rankings_collections on Query { collections(after: $cursor, chains: $chain, first: $count, sortBy: $sortBy, sortAscending: $sortAscending, parents: $parents, createdAfter: $createdAfter) { edges { node { createdDate imageUrl name slug stats { averagePrice marketCap numOwners sevenDayChange sevenDayVolume totalSupply totalVolume id } id __typename } cursor } pageInfo { endCursor hasNextPage } }}",
51 "variables": {
52 "count": 100,
53 "chain": None,
54 "createdAfter": None,
55 "cursor": None,
56 "parents": None,
57 "softAscending": None,
58 "sortBy": "SEVEN_DAY_VOLUME"
59 }
60 }
61
62 response = requests.post("https://api.opensea.io/graphql/", json=data).json()
63 edges = DotMap(response).data.collections.edges
64
65 for rank, edge in enumerate(edges, start=1):
66 if edge.node.slug == "topdogbeachclub":
67 edge.node.rank = rank
68 return edge.node
69
70def get_floor_price():
71 data = {
72 "id": "AssetSearchQuery",
73 "query": "query AssetSearchQuery($categories: [CollectionSlug!], $chains: [ChainScalar!], $collections: [CollectionSlug!], $count: Int, $cursor: String, $identity: IdentityInputType, $numericTraits: [TraitRangeType!], $paymentAssets: [PaymentAssetSymbol!], $priceFilter: PriceFilterType, $query: String, $resultModel: SearchResultModel, $sortAscending: Boolean, $sortBy: SearchSortBy, $stringTraits: [TraitInputType!], $toggles: [SearchToggle!], $creator: IdentityInputType, $isPrivate: Boolean, $safelistRequestStatuses: [SafelistRequestStatus!]) { query { ...AssetSearch_data_2hBjZ1 } } fragment AssetCardFooter_asset_fdERL on AssetType { orderData { bestBid { paymentAssetQuantity { quantity ...AssetQuantity_data id } } bestAsk { paymentAssetQuantity { quantity ...AssetQuantity_data id } } } } fragment AssetQuantity_data on AssetQuantityType { asset { ...Price_data id } quantity } fragment AssetSearchList_data_3Aax2O on SearchResultType { ...Asset_data_3Aax2O } fragment AssetSearch_data_2hBjZ1 on Query { search(after: $cursor, chains: $chains, categories: $categories, collections: $collections, first: $count, identity: $identity, numericTraits: $numericTraits, paymentAssets: $paymentAssets, priceFilter: $priceFilter, querystring: $query, resultType: $resultModel, sortAscending: $sortAscending, sortBy: $sortBy, stringTraits: $stringTraits, toggles: $toggles, creator: $creator, isPrivate: $isPrivate, safelistRequestStatuses: $safelistRequestStatuses) { edges { node { ...AssetSearchList_data_3Aax2O __typename } cursor } totalCount pageInfo { endCursor hasNextPage } } } fragment Asset_data_3Aax2O on SearchResultType { asset { ...AssetCardFooter_asset_fdERL id } } fragment Price_data on AssetType { decimals imageUrl symbol usdSpotPrice assetContract { blockExplorerLink account { chain { identifier id } id } id } }",
74 "variables": {
75 "categories": None,
76 "chains": None,
77 "collections": ["COLLECTION NAME HERE"],
78 "count": 1,
79 "creator": None,
80 "cursor": None,
81 "identity": None,
82 "isPrivate": None,
83 "numericTraits": None,
84 "paymentAssets": None,
85 "priceFilter": None,
86 "query": "",
87 "resultModel": "ASSETS",
88 "safelistRequestStatuses": None,
89 "sortAscending": True,
90 "sortBy": "PRICE",
91 "stringTraits": None,
92 "toggles": ["BUY_NOW"]
93 }
94 }
95 response = requests.post("https://api.opensea.io/graphql/", json=data).json()
96 price = response["data"]["query"]["search"]["edges"][0]["node"]["asset"]["orderData"]["bestAsk"]["paymentAssetQuantity"]
97 floor = int(price["quantity"]) / (10 ** int(price["asset"]["decimals"]))
98
99 with get_db() as db:
100 db.execute("INSERT INTO floors(floor, fetched) VALUES(?, ?)", (floor, datetime.utcnow().isoformat()))
101
102 floor_1hr = db.execute("SELECT floor FROM floors WHERE DATETIME(fetched) <= date('now', '-1 hour') ORDER BY fetched DESC LIMIT 1").fetchone()
103 floor_7d = db.execute("SELECT floor FROM floors WHERE DATETIME(fetched) <= date('now', '-7 days') ORDER BY fetched DESC LIMIT 1").fetchone()
104
105 return floor, floor_1hr["floor"] if floor_1hr else 0.00, floor_7d["floor"] if floor_7d else 0.00, get_change(floor_1hr["floor"], floor) if floor_1hr else 0.00, get_change(floor_7d["floor"], floor) if floor_7d else 0.00
106
107
108def truncate(str, width=20):
109 return (str[:width] + '..') if len(str) > width else str
110
111
112def process_asset(node):
113 if node.assetBundle:
114 _asset = node.assetBundle.assetQuantities.edges[0].node.asset
115 quantity = None
116 if node.assetBundle.name:
117 item = node.assetBundle.name
118 else:
119 item = _asset.collection.name
120 else:
121 _asset = node.assetQuantity.asset
122 quantity = node.assetQuantity.quantity
123 if _asset.name:
124 item = _asset.name
125 else:
126 item = _asset.collection.name
127
128 asset_contract = _asset.assetContract.account.address
129 token_id = _asset.tokenId
130 twitter_username = _asset.collection.twitterUsername
131 item_image_url = _asset.imageUrl
132
133 return item, item_image_url, quantity, asset_contract, token_id, twitter_username
134
135
136def current_timestamp():
137 return (datetime.now() - timedelta(minutes=2)).replace(microsecond=0).isoformat()
138
139
140def process_image(image_url):
141 image_stream = requests.get(image_url, stream=True)
142 image_path = os.path.join(tempfile.mkdtemp(), hashlib.md5("image_url".encode()).hexdigest() + ".png")
143
144 with open(image_path, 'wb') as image:
145 for chunk in image_stream:
146 image.write(chunk)
147
148 return image_path
149
150def get_change(current, previous):
151 if current == previous:
152 return 0
153
154 try:
155 return (abs(current - previous) / previous) * 100.0
156 except ZeroDivisionError:
157 return 0
158
159async def main():
160 run_tracked_hashes = set()
161 from_timestamp = current_timestamp()
162 discord_client = await get_discord_client()
163 doggo_market = await discord_client.fetch_channel(DISCORD CHANNEL ID HERE)
164
165 @discord_client.event
166 async def on_message(message):
167 if message.author == discord_client.user:
168 return
169
170 if message.content.startswith('#rank'):
171 collection = get_opensea_rankings()
172 embed = discord.Embed(title=f"**Top Dog Beach Club** is ranked **{collection.rank}** on OpenSea.", url="https://opensea.io/activity/topdogbeachclub")
173 embed.add_field(name="Market Cap", value=f"{collection.stats.marketCap:,.2f}㆔", inline=True)
174 embed.add_field(name="Average Price", value=f"{collection.stats.averagePrice:,.2f}㆔", inline=True)
175 embed.add_field(name="Doggo Owners", value=f"{collection.stats.numOwners:.0f}", inline=True)
176 embed.add_field(name="7 Day Change", value=f"+{(collection.stats.sevenDayChange * 1000):.0f}%", inline=True)
177 await message.channel.send(embed=embed)
178 elif message.content.startswith('#floor'):
179 floor, oneDayFloor, sevenDayFloor, oneDayChange, sevenDayChange = get_floor_price()
180 embed = discord.Embed(title=f"**Top Dog Beach Club** floor information", url="https://opensea.io/collection/topdogbeachclub")
181 embed.add_field(name="Current Floor", value=f"{floor:,.2f}㆔", inline=True)
182 embed.add_field(name="One Day Floor", value=f"{oneDayFloor:,.2f}㆔", inline=True)
183 embed.add_field(name="Seven Day Floor", value=f"{sevenDayFloor:.2f}㆔", inline=True)
184 embed.add_field(name="One Day Change", value=f"{oneDayChange:.2f}%", inline=True)
185 embed.add_field(name="Seven Day Floor", value=f"{sevenDayChange:.2f}%", inline=True)
186 await message.channel.send(embed=embed)
187
188 while True:
189 try:
190 edges = get_opensea_sales(from_timestamp)
191
192 if len(edges) > 0:
193 for edge in edges:
194 item, item_image_url, quantity, asset_contract, token_id, twitter_username = process_asset(edge.node)
195 opensea_url = f'https://opensea.io/assets/{asset_contract}/{token_id}'
196 the_hash = hashlib.md5((asset_contract + token_id).encode()).hexdigest()
197 image = process_image(item_image_url)
198
199 if the_hash not in run_tracked_hashes:
200 print(edge.node.eventType)
201 run_tracked_hashes.add(the_hash)
202
203 if edge.node.eventType == 'SUCCESSFUL':
204 asset_value = float(edge.node.price.quantity) / (10 ** edge.node.price.asset.decimals)
205 usd_value = edge.node.price.asset.usdSpotPrice * asset_value
206 sold_for = f'{asset_value:,.2f} {edge.node.price.asset.symbol}'
207
208 try:
209 seller = f'{edge.node.seller.address[:6]}...{edge.node.seller.address[-4:]}'
210 except:
211 seller = '(unknown)'
212 try:
213 buyer = f'{edge.node.winnerAccount.address[:6]}...{edge.node.winnerAccount.address[-4:]}'
214 except:
215 buyer = '(unknown)'
216
217 try:
218 embed = discord.Embed(title=f"SOLD: {truncate(item)}", description=item, url=opensea_url)
219 embed.add_field(name="Value", value=f"{sold_for} (${usd_value:,.2f})", inline=True)
220 embed.add_field(name="Quantity", value=f"x {str(quantity)}" if quantity else "x 1", inline=True)
221 embed.add_field(name="Seller", value=seller, inline=True)
222 embed.add_field(name="Buyer", value=buyer, inline=True)
223 embed.set_image(url=f"attachment://{os.path.basename(image)}")
224
225 await doggo_market.send(embed=embed, file=discord.File(image))
226 except Exception as e:
227 print(e)
228
229 from_timestamp = current_timestamp()
230 except Exception as e:
231 print(e)
232 pass
233
234 await asyncio.sleep(CHECK_EVERY_X_SECONDS)
235
236
237if __name__ == '__main__':
238 with get_db() as db:
239 db.execute('''
240 CREATE TABLE IF NOT EXISTS floors (
241 id INTEGER PRIMARY KEY AUTOINCREMENT,
242 floor REAL NOT NULL,
243 fetched TEXT NOT NULL
244 );
245 ''')
246 loop = asyncio.get_event_loop()
247 loop.run_until_complete(main())