Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Adding Reservoir API Poller #307

Merged
merged 4 commits into from
Jul 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ GOOGLE_AUTH_DETAILS={}
METADATA_REFRESH_INTERVAL_MINUTES=60
OPENSEA_API_KEY=<contact maintainer for artbot-jr if token is desired>
RANDOM_ART_INTERVAL_MINUTES=20
RESERVOIR_API_KEY=<contact maintainer for artbot-jr if token is desired>
TOKEN=<contact maintainer for artbot-jr if token is desired>
6 changes: 5 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
// "editor.formatOnSave": false,
"editor.tabSize": 2,
"scss.validate": false,
"editor.quickSuggestions": true,
"editor.quickSuggestions": {
"comments": "on",
"strings": "on",
"other": "on"
},
"editor.autoClosingQuotes": "always",
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
Expand Down
17 changes: 17 additions & 0 deletions Classes/ApiPollBot.js → Classes/APIBots/ApiPollBot.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const fetch = require('node-fetch')
const { ensOrAddress } = require('./utils')

/** Abstract parent class for all API Poll Bots */
class APIPollBot {
Expand All @@ -14,6 +15,8 @@ class APIPollBot {
this.refreshRateMs = refreshRateMs
this.bot = bot
this.headers = headers
this.listColor = '#407FDB'
this.saleColor = '#62DE7C'

// Only send events that occur after this bot gets initialized
this.lastUpdatedTime = Date.now()
Expand Down Expand Up @@ -60,6 +63,20 @@ class APIPollBot {
async buildDiscordMessage(msg) {
console.warn('buildDiscordMessage function not implemented!')
}

async ensOrAddress(address) {
return await ensOrAddress(address)
}

buildOpenseaURL(contractAddr, tokenId) {
return `https://opensea.io/assets/ethereum/${contractAddr}/${tokenId}`
}
buildLooksRareURL(contractAddr, tokenId) {
return `https://looksrare.org/collections/${contractAddr}/${tokenId}`
}
buildX2Y2URL(contractAddr, tokenId) {
return `https://x2y2.io/eth/${contractAddr}/${tokenId}`
}
}

module.exports.APIPollBot = APIPollBot
16 changes: 11 additions & 5 deletions Classes/ArchipelagoBot.js → Classes/APIBots/ArchipelagoBot.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ const fetch = require('node-fetch')
const ReconnectingWebsocket = require('reconnecting-websocket')
const WS = require('ws')
const { MessageEmbed } = require('discord.js')
const CORE_CONTRACTS = require('../ProjectConfig/coreContracts.json')
const CORE_CONTRACTS = require('../../ProjectConfig/coreContracts.json')
const { ensOrAddress } = require('./utils')

const {
sendEmbedToSaleChannels,
sendEmbedToListChannels,
BAN_ADDRESSES,
} = require('../Utils/activityTriager')
} = require('../../Utils/activityTriager')

const WEB_SOCKET_URL = 'wss://api.archipelago.art/ws'
const COLLECTIONS_API = 'https://api.archipelago.art/v1/market/collections'
Expand Down Expand Up @@ -90,10 +91,11 @@ class ArchipelagoBot {
console.log(`Skipping banned seller ${seller} for ${slug} #${tokenIndex}`)
return
}
let sellerText = await ensOrAddress(seller)
const archipelagoUrl = `https://archipelago.art/collections/${slug}/${tokenIndex}`
const embed = new MessageEmbed()
const sellerUrl = `https://archipelago.art/address/${seller}`
embed.addField('Seller (Archipelago)', `[${seller}](${sellerUrl})`)
embed.addField('Seller (Archipelago)', `[${sellerText}](${sellerUrl})`)
embed.addField('List Price', priceToString(price) + ' ETH')
embed.setColor(ARCHIPELAGO_GOLD)
embed.setThumbnail(artBlocksData.image)
Expand Down Expand Up @@ -124,10 +126,14 @@ class ArchipelagoBot {
}
const archipelagoUrl = `https://archipelago.art/collections/${slug}/${tokenIndex}`
const embed = new MessageEmbed()

const sellerText = await ensOrAddress(seller)
const buyerText = await ensOrAddress(buyer)

const sellerUrl = `https://archipelago.art/address/${seller}`
embed.addField('Seller (Archipelago)', `[${seller}](${sellerUrl})`)
embed.addField('Seller (Archipelago)', `[${sellerText}](${sellerUrl})`)
const buyerUrl = `https://archipelago.art/address/${buyer}`
embed.addField('Buyer', `[${buyer}](${buyerUrl})`)
embed.addField('Buyer', `[${buyerText}](${buyerUrl})`)
embed.addField('Price', priceToString(price) + ' ETH')
embed.setColor(ARCHIPELAGO_GOLD)
embed.setThumbnail(artBlocksData.image)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const {
sendEmbedToSaleChannels,
sendEmbedToListChannels,
BAN_ADDRESSES,
} = require('../Utils/activityTriager')
} = require('../../Utils/activityTriager')

/** API Poller for LooksRare List and Sale events */
class LooksRareAPIPollBot extends APIPollBot {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const {
sendEmbedToSaleChannels,
sendEmbedToListChannels,
BAN_ADDRESSES,
} = require('../Utils/activityTriager')
} = require('../../Utils/activityTriager')

/** API Poller for Opensea List and Sale events */
class OpenseaAPIPollBot extends APIPollBot {
Expand All @@ -20,8 +20,6 @@ class OpenseaAPIPollBot extends APIPollBot {
apiEndpoint + '&occurred_after=' + (Date.now() / 1000).toFixed()
super(apiEndpoint, refreshRateMs, bot, headers)
this.contract = contract
this.listColor = '#407FDB'
this.saleColor = '#62DE7C'
}

/**
Expand Down Expand Up @@ -75,7 +73,7 @@ class OpenseaAPIPollBot extends APIPollBot {
let priceText, price, owner, ownerName, buyerText
if (eventType === 'successful') {
// Item sold, add 'Buyer' field
buyerText =
buyerText =
msg.winner_account.address +
(msg.winner_account.user && msg.winner_account.user.username
? ' (' + msg.winner_account.user.username + ')'
Expand Down
112 changes: 112 additions & 0 deletions Classes/APIBots/ReservoirListBot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
const { APIPollBot } = require('./ApiPollBot')
const { MessageEmbed } = require('discord.js')
const fetch = require('node-fetch')
const {
sendEmbedToListChannels,
BAN_ADDRESSES,
} = require('../../Utils/activityTriager')

/** API Poller for Reservoir Sale events */
class ReservoirListBot extends APIPollBot {
/** Constructor just calls super
* @param {string} apiEndpoint - Endpoint to be hitting
* @param {number} refreshRateMs - How often to poll the endpoint (in ms)
* @param {*} bot - Discord bot that will be sending messages
*/
constructor(apiEndpoint, refreshRateMs, bot, headers, contract = '') {
super(apiEndpoint, refreshRateMs, bot, headers)
this.contract = contract
this.listColor = '#407FDB'
this.saleColor = '#62DE7C'
this.lastUpdatedTime = (this.lastUpdatedTime / 1000).toFixed()
}

/**
* Parses and handles Reservoir API endpoint data
* Only sends events that are new
* Response spec: https://docs.reservoir.tools/reference/getordersasksv2
* @param {*} responseData - Dict parsed from API request json
*/
handleAPIResponse(responseData) {
let maxTime = 0
for (const data of responseData.orders) {
const eventTime = Date.parse(data.createdAt)
// Only deal with event if it is new
if (this.lastUpdatedTime < eventTime) {
this.buildDiscordMessage(data)
}

// Save the time of the latest event from this batch
if (maxTime < eventTime) {
maxTime = eventTime
}
}

// Update latest time vars if batch has new latest time
if (maxTime > this.lastUpdatedTime) {
this.lastUpdatedTime = maxTime
}
}

/**
* Handles constructing and sending Discord embed message
* Reservoir API Spec: https://docs.reservoir.tools/reference/getordersasksv2
* @param {*} msg - Dict of event data from API response
*/
async buildDiscordMessage(msg) {
// Create embed we will be sending
const embed = new MessageEmbed()

// Parsing message to get info
const tokenID = msg.tokenSetId.split(':')[2]

let priceText = 'List Price'
let price = msg.price
let owner = msg.maker
let platform = msg.source.name
let listingUrl = msg.source.url

embed.setColor(this.listColor)

if (BAN_ADDRESSES.has(owner)) {
console.log(`Skipping message propagation for ${owner}`)
return
}
const sellerText = await this.ensOrAddress(msg.maker)
const baseABProfile = 'https://www.artblocks.io/user/'
const sellerProfile = baseABProfile + owner
embed.addField(`Seller (${platform})`, `[${sellerText}](${sellerProfile})`)

embed.addField(priceText, price + 'ETH', true)

// Get Art Blocks metadata response for the item.
const tokenUrl =
this.contract === ''
? `https://token.artblocks.io/${tokenID}`
: `https://token.artblocks.io/${this.contract}/${tokenID}`
const artBlocksResponse = await fetch(tokenUrl)
const artBlocksData = await artBlocksResponse.json()

// Update thumbnail image to use larger variant from Art Blocks API.
embed.setThumbnail(artBlocksData.image)

// Add inline field for viewing live script on Art Blocks.
embed.addField(
'Live Script',
`[view on artblocks.io](${artBlocksData.external_url})`,
true
)

// Update to remove author name and to reflect this info in piece name
// rather than token number as the title and URL field..
embed.author = null
embed.setTitle(`${artBlocksData.name} - ${artBlocksData.artist}`)
embed.setURL(listingUrl)
if (artBlocksData.collection_name) {
console.log(artBlocksData.name + ' LIST')
sendEmbedToListChannels(this.bot, embed, artBlocksData)
}
}
}

module.exports.ReservoirListBot = ReservoirListBot
141 changes: 141 additions & 0 deletions Classes/APIBots/ReservoirSaleBot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
const { APIPollBot } = require('./ApiPollBot')
const { MessageEmbed } = require('discord.js')
const fetch = require('node-fetch')
const {
sendEmbedToSaleChannels,
BAN_ADDRESSES,
} = require('../../Utils/activityTriager')
const { getENSName } = require('./utils')
/** API Poller for Reservoir Sale events */
class ReservoirSaleBot extends APIPollBot {
/** Constructor just calls super
* @param {string} apiEndpoint - Endpoint to be hitting
* @param {number} refreshRateMs - How often to poll the endpoint (in ms)
* @param {*} bot - Discord bot that will be sending messages
*/
constructor(apiEndpoint, refreshRateMs, bot, headers, contract = '') {
apiEndpoint =
apiEndpoint + '&startTimestamp=' + (Date.now() / 1000).toFixed()
super(apiEndpoint, refreshRateMs, bot, headers)
this.contract = contract
this.lastUpdatedTime = (this.lastUpdatedTime / 1000).toFixed()
}

/**
* Parses and handles Opensea API endpoint data
* Only sends events that are new
* Response spec: https://docs.reservoir.tools/reference/getsalesbulkv1
* @param {*} responseData - Dict parsed from API request json
*/
handleAPIResponse(responseData) {
let maxTime = 0
for (const data of responseData.sales) {
const eventTime = data.timestamp
// Only deal with event if it is new
if (this.lastUpdatedTime < eventTime) {
this.buildDiscordMessage(data)
}

// Save the time of the latest event from this batch
if (maxTime < eventTime) {
maxTime = eventTime
}
}

// Update latest time vars if batch has new latest time
if (maxTime > this.lastUpdatedTime) {
this.lastUpdatedTime = maxTime

this.apiEndpoint.split('&startTimestamp=')[0] +
'&startTimestamp=' +
this.lastUpdatedTime
}
}

/**
* Handles constructing and sending Discord embed message
* OS API Spec: https://docs.opensea.io/reference/retrieving-asset-events
* @param {*} msg - Dict of event data from API response
*/
async buildDiscordMessage(msg) {
// Create embed we will be sending
const embed = new MessageEmbed()
// Parsing Opensea message to get info
const tokenID = msg.token.tokenId
// const openseaURL = msg.asset.permalink

// Event_type will either be SALE or LIST
const eventType = 'successful'

let priceText = 'Sale Price'
let price = msg.price
let owner = msg.from
let platform = msg.orderSource
embed.setColor(this.saleColor)

if (BAN_ADDRESSES.has(owner)) {
console.log(`Skipping message propagation for ${owner}`)
return
}

const sellerText = await this.ensOrAddress(msg.from)
const buyerText = await this.ensOrAddress(msg.to)
const baseABProfile = 'https://www.artblocks.io/user/'
const sellerProfile = baseABProfile + owner
const buyerProfile = baseABProfile + msg.to
embed.addField(`Seller (${platform})`, `[${sellerText}](${sellerProfile})`)
embed.addField('Buyer', `[${buyerText}](${buyerProfile})`)
embed.addField(priceText, price + 'ETH', true)

// Get Art Blocks metadata response for the item.
const tokenUrl =
this.contract === ''
? `https://token.artblocks.io/${tokenID}`
: `https://token.artblocks.io/${this.contract}/${tokenID}`
const artBlocksResponse = await fetch(tokenUrl)
const artBlocksData = await artBlocksResponse.json()

let platformUrl = ''
switch (platform.toLowerCase()) {
case 'opensea':
platformUrl = this.buildOpenseaURL(
msg.token.contract,
msg.token.tokenId
)
break
case 'looksrare':
platformUrl = this.buildLooksRareURL(
msg.token.contract,
msg.token.tokenId
)
break
case 'x2y2':
platformUrl = this.buildX2Y2URL(msg.token.contract, msg.token.tokenId)
break
default:
platformUrl = artBlocksData.external_url
break
}

// Update thumbnail image to use larger variant from Art Blocks API.
embed.setThumbnail(artBlocksData.image)

// Add inline field for viewing live script on Art Blocks.
embed.addField(
'Live Script',
`[view on artblocks.io](${artBlocksData.external_url})`,
true
)
// Update to remove author name and to reflect this info in piece name
// rather than token number as the title and URL field..
embed.author = null
embed.setTitle(`${artBlocksData.name} - ${artBlocksData.artist}`)
embed.setURL(platformUrl)
if (artBlocksData.collection_name) {
console.log(artBlocksData.name + ' SALE')
sendEmbedToSaleChannels(this.bot, embed, artBlocksData)
}
}
}

module.exports.ReservoirSaleBot = ReservoirSaleBot
Loading