copied from other repo

This commit is contained in:
Moon Man 2025-01-18 07:40:12 -05:00
commit dffa62de07
16 changed files with 14940 additions and 0 deletions

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
*settings.json
!example-settings.json
uploads
node_modules
*.swp
.idea
apidoc/
.env
.env.*

42
example-settings.json Normal file
View File

@ -0,0 +1,42 @@
{
"sentry": {
"api": "SENTRY_URL_1",
"scraper": "SENTRY_URL_2"
},
"baseUrl": "https://PUBLIC_DOMAIN",
"factory": {
"address": "FACTORY_ADDRESS",
"block": 9046391
},
"token": {
"address": "TOKEN_ADDRESS",
"decimals": 18,
"block": 7150030
},
"web3": {
"url": "wss://INFURA_URL",
"options": {
"reconnect": {
"auto": true,
"delay": 5000,
"maxAttempts": 5,
"onTimeout": false
}
}
},
"db": {
"user": "DB_USER",
"host": "127.0.0.1",
"database": "DB_NAME",
"password": "DB_PASS",
"port": 5432
},
"http": {
"port": 3031
},
"ipfs": {
"nodes": [
"http://localhost:5001/api/v0"
]
}
}

494
factory.abi.json Normal file
View File

@ -0,0 +1,494 @@
[
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "BridgeTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "ManagerTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "store",
"type": "address"
},
{
"indexed": true,
"internalType": "uint24",
"name": "id",
"type": "uint24"
}
],
"name": "NewStore",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "OracleTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "operator",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
}
],
"name": "PayoutTransferred",
"type": "event"
},
{
"inputs": [],
"name": "bridge",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"name": "diamondCut",
"outputs": [
{
"internalType": "address",
"name": "facetAddress",
"type": "address"
},
{
"internalType": "enum IDiamondCut.FacetCutAction",
"name": "action",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [],
"name": "initParams",
"outputs": [
{
"internalType": "address payable",
"name": "creator",
"type": "address"
},
{
"internalType": "contract IMotherShip",
"name": "mothership",
"type": "address"
},
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "bool",
"name": "isERC1363Token",
"type": "bool"
},
{
"internalType": "uint8",
"name": "fee",
"type": "uint8"
},
{
"internalType": "uint8",
"name": "royalty",
"type": "uint8"
},
{
"internalType": "string",
"name": "storeName",
"type": "string"
},
{
"internalType": "string",
"name": "storeSymbol",
"type": "string"
},
{
"internalType": "string",
"name": "baseURI",
"type": "string"
},
{
"internalType": "address",
"name": "proxyRegistryAddress",
"type": "address"
},
{
"internalType": "uint24",
"name": "storeId",
"type": "uint24"
},
{
"internalType": "bool",
"name": "sendRightAway",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [],
"name": "manager",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [],
"name": "oracle",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [],
"name": "payout",
"outputs": [
{
"internalType": "address payable",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function",
"constant": true
},
{
"inputs": [
{
"components": [
{
"internalType": "address",
"name": "facetAddress",
"type": "address"
},
{
"internalType": "enum IDiamondCut.FacetCutAction",
"name": "action",
"type": "uint8"
},
{
"internalType": "bytes4[]",
"name": "functionSelectors",
"type": "bytes4[]"
}
],
"internalType": "struct IDiamondCut.FacetCut[]",
"name": "_diamondCut",
"type": "tuple[]"
},
{
"components": [
{
"internalType": "address payable",
"name": "creator",
"type": "address"
},
{
"internalType": "contract IMotherShip",
"name": "mothership",
"type": "address"
},
{
"internalType": "address",
"name": "token",
"type": "address"
},
{
"internalType": "bool",
"name": "isERC1363Token",
"type": "bool"
},
{
"internalType": "uint8",
"name": "fee",
"type": "uint8"
},
{
"internalType": "uint8",
"name": "royalty",
"type": "uint8"
},
{
"internalType": "string",
"name": "storeName",
"type": "string"
},
{
"internalType": "string",
"name": "storeSymbol",
"type": "string"
},
{
"internalType": "string",
"name": "baseURI",
"type": "string"
},
{
"internalType": "address",
"name": "proxyRegistryAddress",
"type": "address"
},
{
"internalType": "uint24",
"name": "storeId",
"type": "uint24"
},
{
"internalType": "bool",
"name": "sendRightAway",
"type": "bool"
}
],
"internalType": "struct DiamondERC1155.InitializationParameters",
"name": "_initParams",
"type": "tuple"
}
],
"name": "initialize",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "_name",
"type": "string"
},
{
"internalType": "string",
"name": "_symbol",
"type": "string"
}
],
"name": "newStore",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "string",
"name": "_name",
"type": "string"
},
{
"internalType": "string",
"name": "_symbol",
"type": "string"
},
{
"internalType": "string",
"name": "_metadataURI",
"type": "string"
},
{
"internalType": "address",
"name": "_customToken",
"type": "address"
},
{
"internalType": "bool",
"name": "_isERC1363",
"type": "bool"
}
],
"name": "newStore",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_manager",
"type": "address"
}
],
"name": "setManager",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_bridge",
"type": "address"
}
],
"name": "setBridge",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "_oracle",
"type": "address"
}
],
"name": "setOracle",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address payable",
"name": "_payout",
"type": "address"
}
],
"name": "setPayout",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
}
]

7
forever-index.sh Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash
#set -eo pipefail
while true
do
node --max-old-space-size=8192 index.js ./test-settings.json || true
done

7
forever-scraper.sh Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -eo pipefail
while true
do
node scraper.js ./test-settings.json
done

10
generate-api-doc.sh Executable file
View File

@ -0,0 +1,10 @@
#!/bin/bash
# install apidoc:
# npm install -g apidoc
# use fswatch to invoke this command whever index.js updates:
# fswatch index.js | xargs -n1 -I{} ./generate-api-doc.sh
echo "Generating documentation..."
apidoc -i ./ -o apidoc/ -e node_modules -f index.js && echo success

2348
index.js Normal file

File diff suppressed because it is too large Load Diff

93
latest.sql Normal file
View File

@ -0,0 +1,93 @@
select y.nft_id,
y.store,
y.owner,
y.meta_id,
y.eth_price,
y.token_price,
y.metadata,
y.metadata_uri,
y.eth_for_sale,
y.token_for_sale,
x.dollar_price,
y.inserted,
z.cnt
from nft as y,
(
select coalesce(a1.store, b1.store) as store,
coalesce(a1.meta_id, b1.meta_id) as meta_id,
coalesce(a1.nft_id, b1.nft_id) as nft_id,
a1.dollar_price
from (
select coalesce(eth_nfts.store, token_nfts.store) as store,
coalesce(eth_nfts.meta_id, token_nfts.meta_id) as meta_id,
coalesce(eth_nfts.nft_id, token_nfts.nft_id) as nft_id,
eth_nfts.min_eth_price,
token_nfts.min_token_price,
coalesce(eth_nfts.dollar_price, token_nfts.dollar_price) as dollar_price
from (select distinct on (n1.store, n1.meta_id) n1.store,
n1.meta_id,
n1.nft_id,
min(n1.eth_price) as min_eth_price,
min(n1.eth_price) / (10 ^ 18) *
(select val from lookup where key = 'ubiqUsdRatio')::decimal as dollar_price
from nft n1
where n1.hidden = false
and n1.owner <> '0x0000000000000000000000000000000000000000'
and n1.metadata is not null
and n1.eth_for_sale = true
group by n1.store, n1.meta_id, n1.eth_price, n1.nft_id
) as eth_nfts
full join
(select distinct on (n1.store, n1.meta_id) n1.store,
n1.meta_id,
n1.nft_id,
min(n1.token_price) as min_token_price,
min(n1.token_price) / (10 ^ 18) *
(select val from lookup where key = 'ubiqUsdRatio')::decimal /
(select val from lookup where key = 'ubiqGransRatio')::decimal as dollar_price
from nft n1
where n1.hidden = false
and n1.owner <> '0x0000000000000000000000000000000000000000'
and n1.metadata is not null
and n1.token_for_sale = true
group by n1.store, n1.meta_id, n1.token_price, n1.nft_id
) as token_nfts
on eth_nfts.store = token_nfts.store and eth_nfts.meta_id = token_nfts.meta_id
) as a1
full join
(
select any_nfts.store, any_nfts.meta_id, min(any_nfts.nft_id) as nft_id
from nft as any_nfts
inner join (
select nosale_nfts.store,
nosale_nfts.meta_id,
array_agg(nosale_nfts.token_for_sale),
sum(case when nosale_nfts.token_for_sale = true then 1 else 0 end) as num_tokens_for_sale,
sum(case when nosale_nfts.eth_for_sale = true then 1 else 0 end) as eth_tokens_for_sale
from nft as nosale_nfts
where nosale_nfts.metadata is not null
group by nosale_nfts.store, nosale_nfts.meta_id
having sum(case when nosale_nfts.token_for_sale = true then 1 else 0 end) = 0
and sum(case when nosale_nfts.eth_for_sale = true then 1 else 0 end) = 0
order by nosale_nfts.store, nosale_nfts.meta_id
) as ag
on ag.store = any_nfts.store and ag.meta_id = any_nfts.meta_id
where any_nfts.metadata is not null
group by any_nfts.store, any_nfts.meta_id
) as b1
on a1.nft_id = b1.nft_id
) as x,
(
select i.meta_id, i.store, a.verified, count(*) as cnt
from nft as i, store as s, account as a
where i.metadata is not null
and i.hidden = false
and i.owner <> '0x0000000000000000000000000000000000000000'
and s.address = i.store
and a.id = s.creator
OWNER_CLAUSE
group by i.meta_id, i.store, a.verified
) as z
where x.nft_id = y.nft_id
and x.store = z.store
and x.meta_id = z.meta_id

21
nft-policy.json Normal file
View File

@ -0,0 +1,21 @@
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"s3:ListBucket"
],
"Effect": "Allow",
"Resource": "arn:aws:s3:::dev-erc1155"
},
{
"Action": [
"s3:DeleteObject",
"s3:GetObject",
"s3:PutObject"
],
"Effect": "Allow",
"Resource": "arn:aws:s3:::dev-erc1155/*"
}
]
}

9
notifications.sql Normal file
View File

@ -0,0 +1,9 @@
select inserted, name, return_values from
(select inserted, cast('Comment' as text) as name, jsonb_build_object('store', comment.store, 'meta_id', comment.meta_id, 'author', author, 'content', content) as return_values from (select store, meta_id, inserted, author, content from comment) comment join (select store, meta_id, owner from nft where owner = $1) nft on comment.store = nft.store and comment.meta_id = nft.meta_id and comment.author != $1
union
select inserted, name, return_values from event where name in ('BuySingleNft', 'TokenBuySingleNft') and return_values->>'from' = $1) notifications
union
select inserted, cast('Resale' as text) as name, return_values from event where name in ('BuySingleNft') and address in (select address from store where creator = $1) and return_values->>'from' != $1
union
select inserted, cast('TokenResale' as text) as name, return_values from event where name in ('TokenBuySingleNft') and address in (select address from store where creator = $1) and return_values->>'from' != $1
order by inserted desc limit $2 offset $3;

9514
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

37
package.json Normal file
View File

@ -0,0 +1,37 @@
{
"name": "token-gallery-backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"@sentry/browser": "^6.2.1",
"@sentry/node": "^5.24.2",
"@sentry/tracing": "^6.2.1",
"bignumber.js": "^9.0.1",
"ethereumjs-util": "5.1.5",
"express": "^4.17.1",
"feed": "^4.2.1",
"gm": "^1.23.1",
"ipfs-http-client": "^51.0.0",
"memory-streams": "^0.1.3",
"multer": "^1.4.2",
"node-fetch": "^2.6.0",
"pg": "^8.2.1",
"timed-cache": "^1.1.5",
"uuid-random": "^1.3.2",
"web3": "^1.3.4",
"web3-eth": "^1.3.4",
"web3-eth-contract": "^1.3.4",
"web3-utils": "^1.3.4"
},
"apidoc": {
"title": "token.gallery backend API",
"url" : "https://token.gallery",
"sampleUrl": "https://token.gallery"
}
}

494
scraper.js Normal file
View File

@ -0,0 +1,494 @@
const utils = require('ethereumjs-util');
const fs = require('fs');
const { Client } = require('pg');
const Web3 = require('web3');
const fetch = require("node-fetch");
const BigNumber = require("bignumber.js");
const BN = require('bn.js');
const { create } = require('ipfs-http-client');
const all = require('it-all')
const uint8ArrayConcat = require('uint8arrays/concat')
const uint8ArrayToString = require('uint8arrays/to-string')
const Sentry = require("@sentry/node");
const Tracing = require("@sentry/tracing");
const settings = require(process.argv[2]);
Sentry.init({
dsn: settings.sentry.scraper,
tracesSampleRate: 1.0
});
const ipfsNodes = settings.ipfs.nodes.map(n => create(n) );
async function fetchFromIpfs(hash) {
if (hash.startsWith("ipfs://")) {
hash = hash.substring(7);
}
const promises = ipfsNodes.map(n => all(n.cat(hash)));
const result = await Promise.any(promises);
const rawData = uint8ArrayConcat(result);
return uint8ArrayToString(rawData);
}
const provider = new Web3.providers.WebsocketProvider(settings.web3.url, settings.web3.options)
const web3 = new Web3(provider, null, { transactionConfirmationBlocks: 1 });
provider.on("error", e => {
Sentry.captureException(e);
console.error("Websocket error, exiting", e);
process.exit(1)
});
provider.on("connect", () => {
console.log("Websocket connected");
});
// "end" event is handled further down
async function fetchJson(url) { return await (await fetch(url)).json() }
// This is just here to keep the program from closing when it hits the end.
setInterval(() => {}, 1000 * 60 );
(async () => {
const client = new Client(settings.db);
await client.connect();
const factoryAbi = JSON.parse(await fs.promises.readFile("./factory.abi.json"));
const storeAbi = JSON.parse(await fs.promises.readFile("./store.abi.json"));
async function lookup(key) {
const result = await client.query("select val from lookup where key = $1 limit 1", [ key ]);
return result.rows.length == 0 ? null : result.rows[0].val;
}
async function setLatestBlock(blockNumber) {
const result = await client.query("update lookup set val = $1 where key = 'lastFactoryBlock' and cast(val as integer) < cast($1 as integer)", [ blockNumber ]);
if (result.rowCount == 0) console.log(`Ignoring last block update ${blockNumber} as was older than current`);
}
async function getLatestBlock() {
const result = await client.query("select cast(val as integer) as val from lookup where key = 'lastFactoryBlock' limit 1");
return result.rows[0].val;
}
async function insertEvent(event) {
const result = await client.query(
"insert into event( inserted, name, address, return_values, log_index, transaction_index, block) values ( now(), $1, $2, $3, $4, $5, $6 ) on conflict ( address, log_index, transaction_index, block ) do nothing returning *",
[ event.event, event.address, event.returnValues, event.logIndex, event.transactionIndex, event.blockNumber ]
);
if (result.rowCount == 0) {
return -1;
}
else {
return result.rows[0].id;
}
}
async function getNft(storeAddress, nftId) {
const result = await client.query("select * from nft where nft_id = $1 and store = $2 limit 1", [ nftId, storeAddress ]);
if (result.rowCount == 0) { return null; }
else {
const r = result.rows[0];
return {
id: r.id,
nftId: r.nft_id,
storeAddress: r.store,
ownerAddress: r.owner,
inserted: r.inserted,
base: r.base,
metadata: r.metadata,
metadataUri: r.metadata_uri,
hidden: r.hidden,
ethPrice: new BigNumber(r.eth_price),
tokenPrice: new BigNumber(r.token_price)
}
}
}
function registerStoreEventHandlers(contract) {
contract.events.TransferSingle({ fromBlock: lastSeenBlock }).on("data", handleNftTransferEvent);
contract.events.TransferBatch({ fromBlock: lastSeenBlock }).on("data", handleNftBatchTransferEvent);
contract.events.BuySingleNft({ fromBlock: lastSeenBlock }).on("data", handleBuySingleNft);
contract.events.TokenBuySingleNft({ fromBlock: lastSeenBlock }).on("data", handleTokenBuySingleNft);
contract.events.CreatorTransferred({ fromBlock: lastSeenBlock }).on("data", handleStoreTransferEvent);
contract.events.PriceChange({ fromBlock: lastSeenBlock }).on("data", handlePriceChangeEvent);
}
async function handleNewStoreEvent(event) {
const address = event.returnValues.store;
console.log(`New store: ${address}`);
try {
var contract = new web3.eth.Contract(storeAbi, address);
var name = await contract.methods.name().call();
var symbol = await contract.methods.symbol().call();
var creator = await contract.methods.creator().call();
}
catch (error) {
Sentry.captureException(e);
// This happens sometimes I think because nodes aren't in sync when we continue on a single confirmation.
console.error("There was an error handling new store, bailing and restarting to resume from where we left off.", error);
process.exit(1);
}
if (!stores[address]) {
stores[address] = {
address,
name,
symbol,
creator,
contract
}
}
const eventId = await insertEvent(event);
if (eventId == -1) {
console.log("Store already in database, not inserting.");
}
else {
const result = await client.query(
"insert into store(address, name, symbol, creator, inserted) values($1, $2, $3, $4, now()) on conflict(address) do nothing",
[address, name, symbol, creator]
);
if (result.rowCount == 0) console.error("Store not inserted for some reason.");
}
if (event.blockNumber > lastSeenBlock) {
lastSeenBlock = event.blockNumber;
await setLatestBlock(event.blockNumber);
}
registerStoreEventHandlers(contract);
}
async function handleStoreTransferEvent(event) {
console.log("Store transfer event");
const from = event.returnValues.from;
const to = event.returnValues.to;
const storeAddress = event.address;
const eventId = await insertEvent(event);
if (eventId == -1) {
console.log("Skipping transfer store event.");
}
else {
const result = await client.query("update store set creator = $1 where address = $2", [ to, storeAddress ]);
}
if (event.blockNumber > lastSeenBlock) {
lastSeenBlock = event.blockNumber;
await setLatestBlock(event.blockNumber);
}
}
// sometimes these come in before the mint so they have to be deferred.
const handlePriceChangeEvent = async function (event, deferred = false) {
console.log("Nft price change event");
async function updateNft(price, forSale, tokenPrice, tokenForSale, storeAddress, nftId) {
const params = [ price, forSale, tokenPrice, tokenForSale, storeAddress, nftId ];
const result = await client.query("update nft set eth_price = $1, eth_for_sale = $2, token_price = $3, token_for_sale = $4 where store = $5 and nft_id = $6", params);
if (result.rowCount != 1) {
console.error(`Updated ${result.rowCount} rows instead of 1 for some reason, deferring and trying again.`);
setTimeout(async function(){ await handlePriceChangeEvent(event, true); }, 5000);
}
}
const storeAddress = event.address;
const nftId = event.returnValues.id;
const price = event.returnValues.price;
const forSale = event.returnValues.forSale;
const tokenPrice = event.returnValues.tokenPrice;
const tokenForSale = event.returnValues.tokenForSale;
if (deferred) {
// We KNOW there's already an event so skip trying to insert it
await updateNft(price, forSale, tokenPrice, tokenForSale, storeAddress, nftId);
}
else {
const eventId = await insertEvent(event);
if (eventId == -1) {
console.log("Skipping nft price change event.");
}
else {
await updateNft(price, forSale, tokenPrice, tokenForSale, storeAddress, nftId);
}
}
if (event.blockNumber > lastSeenBlock) {
lastSeenBlock = event.blockNumber;
await setLatestBlock(event.blockNumber);
}
}
async function handleBuySingleNft(event, deferred = false, count = 5) {
console.log("NFT buy event (using Ubiq)");
if (!deferred) {
const eventId = await insertEvent(event);
}
// Don't need to show the transfer event because the buy event has same info.
const params = [
event.blockNumber,
event.transactionIndex,
event.returnValues.from,
event.returnValues.to,
event.returnValues.id
];
console.log(params);
const result = await client.query("update event set hidden=true where name = 'TransferSingle' and block = $1 and transaction_index = $2 and return_values->>'_from' = $3 and return_values->>'_to' = $4 and return_values->>'_id' = $5", params);
if (result.rowCount == 0) {
// Sometimes the events come out of order
if (count > 0) {
console.log("No transfer activity was found to hide, so waiting and trying again later.");
setTimeout(() => { handleBuySingleNft(event, true, count-1); }, 5000);
}
}
}
async function handleTokenBuySingleNft(event, deferred = false, count = 5) {
console.log("NFT buy event (using token)");
if (!deferred) {
const eventId = await insertEvent(event);
}
// Don't need to show the transfer event because the buy event has same info.
const params = [
event.blockNumber,
event.transactionIndex,
event.returnValuesfrom,
event.returnValues.to,
event.returnValues.id
];
const result = await client.query("update event set hidden=true where name = 'TransferSingle' and block = $1 and transaction_index = $2 and return_values->>'_from' = $3 and return_values->>'_to' = $4 and return_values->>'_id' = $5", params);
if (result.rowCount == 0) {
// Sometimes the events come out of order
if (count > 0) {
console.log("No transfer activity was found to hide, so waiting and trying again later.");
setTimeout(() => { handleTokenBuySingleNft(event, true, count-1); }, 5000);
}
}
}
async function handleNftBatchTransferEvent(event) {
console.log("NFT batch transfer event");
const eventId = await insertEvent(event); // Save it even if we're not using it.
const ids = event.returnValues.ids;
// remove aggregate fields
delete event.returnValues['3'];
delete event.returnValues['4'];
delete event.returnValues.ids;
delete event.returnValues.values;
event.event = 'TransferSingle';
event.returnValues.value = '1';
// Can't have same one for every derived event, so faking them, but predictably and recognizably.
let logIndex = event.logIndex + 10000;
for (const id of ids) {
event.returnValues.id = id;
event.logIndex = logIndex;
await handleNftTransferEvent(event);
++logIndex;
}
}
/**
* Handles token transfers and mints.
*/
async function handleNftTransferEvent(event) {
console.log("NFT transfer event");
const eventId = await insertEvent(event);
const operator = event.returnValues.operator;
const from = event.returnValues.from;
const to = event.returnValues.to;
const nftId = event.returnValues.id;
const isMintOperation = (from == "0x0000000000000000000000000000000000000000");
const isBurnOperation = (!isMintOperation && to == "0x0000000000000000000000000000000000000000");
const storeAddress = event.address;
const contract = stores[storeAddress].contract;
try {
// erc1155 can hold both fungible and nonfungible
var isNft = await contract.methods.isNonFungibleItem(nftId).call();
}
catch (error) {
console.error("Error getting if contract is nonfungible. bailing and starting from where we left off.", error);
process.exit(1);
}
if (isNft) {
if (isMintOperation) {
console.log("Mint operation");
try {
var metaId = await contract.methods.metaId(nftId).call();
}
catch (error) {
console.error("Error getting meta id. bailing and starting from where we left off.", error);
process.exit(1);
}
if (eventId != -1) {
const result = await client.query(
"insert into nft(nft_id, store, owner, inserted, base, meta_id) values($1, $2, $3, now(), false, $4) on conflict(nft_id, store) do nothing",
[ nftId, storeAddress, to, metaId ]
);
if (result.rowCount == 0) {
console.log("NFT wasn't inserted for some reason.");
}
}
const hasMetadata = (await client.query("select count(*) from nft where nft_id = $1 and store = $2 and metadata is not null", [ nftId, storeAddress ])) > 0;
if (!hasMetadata) {
// insert metadata if it exists.
const contract = stores[storeAddress].contract;
// this shouldn't ever fail because we call the contract earlier and it succeeded.
const uri = await contract.methods.uri(nftId).call();
try {
let metadata;
if (uri.startsWith("ipfs://")) {
metadata = await fetchFromIpfs(uri);
} else {
metadata = await fetchJson(uri);
}
const result = await client.query("update nft set metadata = $1, metadata_uri = $2 where nft_id = $3 and store = $4 and metadata is null", [ metadata, uri, nftId, storeAddress ]);
if (result.rowCount == 1) {
console.log("Inserted NFT metadata");
}
} catch (e) {
if (e instanceof AggregateError) {
console.error("Aggregate error fetching metadata: ", e.errors);
} else {
console.error("Error fetching metadata", e);
}
}
}
}
else if (isBurnOperation) {
console.log("Burn operation");
if (eventId != -1) {
console.log("Burning in database");
const result = await client.query(
"update nft set owner = $1, eth_price=0, eth_for_sale=false, token_price=0, token_for_sale=false where nft_id = $2 and store = $3",
[ to, nftId, storeAddress ]
);
if (result.rowCount == 0) console.log("Burn not updated in db for some reason.");
}
else {
console.log("Already recorded in db.");
}
}
else {
console.log("Normal transfer operation");
if (eventId != -1) {
const result = await client.query(
"update nft set owner = $1, eth_price=0, eth_for_sale=false, token_price=0, token_for_sale=false where nft_id = $2 and store = $3",
[ to, nftId, storeAddress ]
);
if (result.rowCount == 0) console.log("Skipping NFT transfer, probably because it was already processed");
}
}
}
else {
console.log("Not an NFT, ignoring")
}
if (event.blockNumber > lastSeenBlock) {
lastSeenBlock = event.blockNumber;
await setLatestBlock(event.blockNumber);
}
}
async function handleNewBlock(blockHeader) {
if (blockHeader.number == lastSeenBlock) {
console.log(`Ignoring duplicate block ${lastSeenBlock}`);
}
else if (blockHeader.number > lastSeenBlock) {
lastSeenBlock = blockHeader.number;
await setLatestBlock(blockHeader.number);
}
else {
console.log("Received block header out of order, assuming already processed and ignoring.");
}
}
const stores = {};
let lastSeenBlock = await getLatestBlock();
const storeResult = await client.query("select address, name, symbol, creator from store order by inserted asc");
console.log(`Number of stores queried: ${storeResult.rowCount}`);
for (const row of storeResult.rows) {
const contract = new web3.eth.Contract(storeAbi, row.address);
registerStoreEventHandlers(contract);
stores[row.address] = {
address: row.address,
name: row.name,
symbol: row.symbol,
creator: row.creator,
contract
}
}
const factory = new web3.eth.Contract(factoryAbi, settings.factory.address);
console.log(`Watching for new stores since block: ${lastSeenBlock}`);
factory.events.NewStore({ fromBlock: lastSeenBlock })
.on("data", handleNewStoreEvent);
// handle provider disconnects
provider.on("error", e => {
Sentry.captureException(e);
console.log("Connection ended, re-establishing connection")
web3.eth.clearSubscriptions();
web3.setProvider(provider);
web3.eth.subscribe('newBlockHeaders')
.on('data', handleNewBlock)
.on('error', e => {
Sentry.captureException(e);
});
console.log("Re-setting all event listeners");
factory.events.NewStore({ fromBlock: lastSeenBlock }).on("data", handleNewStoreEvent);
for (const address of Object.keys(stores)) {
const store = stores[address];
registerStoreEventHandlers(store.contract);
}
});
// this is also done in the reconnect above.
web3.eth.subscribe('newBlockHeaders')
.on('data', handleNewBlock)
.on('error', e => {
Sentry.captureException(e);
});
// for each store, add a watch, starting at last block number above,
// for transfer and mint events
})().catch(e => {
Sentry.captureException(e);
console.error("Top-level failure, bailing and starting over from the top.", e);
process.exit(1);
});

1460
store.abi.json Normal file

File diff suppressed because it is too large Load Diff

344
token.abi.json Normal file
View File

@ -0,0 +1,344 @@
[
{
"inputs": [],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "tokenOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokens",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "from",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "tokens",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
},
{
"stateMutability": "nonpayable",
"type": "fallback"
},
{
"inputs": [],
"name": "_totalSupply",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "contractOwner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "decimals",
"outputs": [
{
"internalType": "uint8",
"name": "",
"type": "uint8"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "name",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newContractOwner",
"type": "address"
}
],
"name": "setContractOwner",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "symbol",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "tokenOwner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "balance",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokens",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"internalType": "bool",
"name": "success",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokens",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"internalType": "bool",
"name": "success",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "from",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokens",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [
{
"internalType": "bool",
"name": "success",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "tokenOwner",
"type": "address"
},
{
"internalType": "address",
"name": "spender",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"internalType": "uint256",
"name": "remaining",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "spender",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokens",
"type": "uint256"
},
{
"internalType": "bytes",
"name": "data",
"type": "bytes"
}
],
"name": "approveAndCall",
"outputs": [
{
"internalType": "bool",
"name": "success",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "tokenAddress",
"type": "address"
},
{
"internalType": "uint256",
"name": "tokens",
"type": "uint256"
}
],
"name": "transferAnyERC20Token",
"outputs": [
{
"internalType": "bool",
"name": "success",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
}
]

51
url.js Normal file
View File

@ -0,0 +1,51 @@
const Web3Util = require('web3-utils');
const { Client } = require('pg');
const https = require('https');
const { argv } = require('process');
const account = Web3Util.toChecksumAddress(process.argv[2]);
const method = process.argv[3].toUpperCase();
const url = process.argv[4];
const path = new URL(url).pathname;
const settings = require('./test-settings.json');
const client = new Client(settings.db);
client.connect().then(async () => {
const result = await client.query("select token from account where id = $1", [ account ]);
const token = result.rows[0].token;
const now = new Date();
const hashString = `${now} ${account} ${token} ${path}`;
console.log(hashString);
const hash = Web3Util.keccak256(hashString);
const headers = {
'X-NftStore-Now': now,
'X-NftStore-Account': account,
'Authorization': `Bearer ${hash}`
}
if (argv.length > 5) {
var body = process.argv[5];
headers['Content-Type'] = 'application/json';
headers['Content-Length'] = body.length;
}
const req = https.request(url, { method, headers }, res => {
console.log(res.headers);
res.on('data', d => {
console.log(d.toString());
});
res.on('error', e => {
console.error(e);
process.exit(1);
});
res.on('end', () => {
process.exit(res.statusCode);
});
});
if (argv.length > 5) {
req.write(body);
}
req.end();
});