copied from other repo
This commit is contained in:
commit
dffa62de07
|
@ -0,0 +1,9 @@
|
||||||
|
*settings.json
|
||||||
|
!example-settings.json
|
||||||
|
uploads
|
||||||
|
node_modules
|
||||||
|
*.swp
|
||||||
|
.idea
|
||||||
|
apidoc/
|
||||||
|
.env
|
||||||
|
.env.*
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
|
@ -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
|
|
@ -0,0 +1,7 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -eo pipefail
|
||||||
|
|
||||||
|
while true
|
||||||
|
do
|
||||||
|
node scraper.js ./test-settings.json
|
||||||
|
done
|
|
@ -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
|
|
@ -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
|
|
@ -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/*"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -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;
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
});
|
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
|
@ -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();
|
||||||
|
});
|
Loading…
Reference in New Issue