|
import { BigNumber } from "@ethersproject/bignumber"; |
|
import * as chalk from "chalk"; |
|
import { formatEther } from "ethers/lib/utils"; |
|
import * as fs from "fs"; |
|
import { forEach, groupBy, last } from "lodash"; |
|
import * as fetch from "node-fetch"; |
|
import * as ora from "ora"; |
|
import * as yargs from "yargs"; |
|
|
|
const argv = yargs |
|
.usage("$0") |
|
.option("slug", { |
|
describe: "open-sea slug", |
|
string: true, |
|
required: true, |
|
default: "forgottenruneswizardscult" |
|
}) |
|
.option("rpc", { |
|
describe: "The URL to your provider", |
|
string: true, |
|
required: true, |
|
default: "https://cloudflare-eth.com/" |
|
}) |
|
.help("help").argv; |
|
|
|
const runes = ["Φ", "Ψ", "λ", "ϐ", "ҩ", "Ӝ", "⊙", "⚧", "⚭", "Ω"]; |
|
|
|
async function fetchOrderPage({ |
|
slug, |
|
spinner, |
|
page = 0 |
|
}: { |
|
slug: string; |
|
spinner: any; |
|
page: number; |
|
}) { |
|
spinner.text = `Fetching page ${chalk.blue("#" + page)}`; |
|
const url = `https://api.opensea.io/wyvern/v1/orders?collection_slug=${slug}&bundled=false&include_bundled=false&include_invalid=false&side=1&limit=50&offset=${page}&order_by=created_date&order_direction=desc`; |
|
const response = await ( |
|
await fetch(url, { |
|
method: "GET", |
|
headers: { Accept: "application/json" } |
|
}) |
|
).json(); |
|
return response.orders; |
|
} |
|
|
|
const WETH = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"; |
|
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; |
|
|
|
function preprocessOrders({ orders }) { |
|
let lowestOrders = []; |
|
const ordersByToken = groupBy(orders, (o) => o.asset.token_id); |
|
forEach(ordersByToken, (tokensOrders, tokenId) => { |
|
const ethOrders = tokensOrders.filter( |
|
(o) => o.payment_token === WETH || o.payment_token === ZERO_ADDRESS |
|
); |
|
const sortedByPrice = ethOrders.sort((a, b) => |
|
BigNumber.from(a.base_price).gt(BigNumber.from(b.base_price)) ? 1 : -1 |
|
); |
|
lowestOrders.push(sortedByPrice[0]); |
|
}); |
|
return lowestOrders.sort((a, b) => |
|
BigNumber.from(a.base_price).gt(BigNumber.from(b.base_price)) ? 1 : -1 |
|
); |
|
} |
|
|
|
async function run(argv: any) { |
|
const spinner = ora({ |
|
text: "Downloading", |
|
spinner: { interval: 80, frames: runes }, |
|
prefixText: "🧙♀️" |
|
}).start(); |
|
|
|
let page = 0; |
|
let lastCount = 0; |
|
let orders = []; |
|
let maxPages = 5000; |
|
|
|
do { |
|
const newOrders = await fetchOrderPage({ slug: argv.slug, spinner, page }); |
|
lastCount = newOrders.length; |
|
orders = orders.concat(newOrders); |
|
page += 1; |
|
} while (lastCount > 0 && page <= maxPages); |
|
|
|
/** |
|
* the same user might have multiple open orders at different price points |
|
* because it costs gas to cancel an order so users often add a new order at a |
|
* lower price point. we're trying to calculate the lowest price that could |
|
* move the floor, so we want to ignore all but the lowest open sell order |
|
*/ |
|
const processedOrders = preprocessOrders({ orders }); |
|
|
|
fs.writeFileSync("depth.csv", ""); |
|
|
|
let sum = BigNumber.from(0); |
|
processedOrders.map((o) => { |
|
const row = [ |
|
o.asset.token_id, |
|
o.base_price, |
|
sum.toString(), |
|
formatEther(sum) |
|
]; |
|
console.log(...row); |
|
fs.appendFileSync("depth.csv", row.join(",") + "\n"); |
|
sum = sum.add(BigNumber.from(o.base_price)); |
|
}); |
|
|
|
spinner.color = "green"; |
|
spinner.text = "Complete"; |
|
spinner.succeed(); |
|
} |
|
|
|
run(argv); |