├── config
├── plugin.js
└── config.default.js
├── app
├── middleware
│ ├── transaction.js
│ ├── ratelimit.js
│ ├── address.js
│ ├── pagination.js
│ ├── contract.js
│ └── block-filter.js
├── io
│ ├── middleware
│ │ └── connection.js
│ └── controller
│ │ └── default.js
├── extend
│ ├── application.js
│ └── helper.js
├── schedule
│ ├── update-price.js
│ └── update-feerate.js
├── controller
│ ├── qrc721.js
│ ├── info.js
│ ├── statistics.js
│ ├── misc.js
│ ├── transaction.js
│ ├── qrc20.js
│ ├── block.js
│ ├── contract.js
│ └── address.js
├── model
│ ├── contract-tag.js
│ ├── qrc20-statistics.js
│ ├── contract-code.js
│ ├── qrc721-token.js
│ ├── witness.js
│ ├── contract-spend.js
│ ├── rich-list.js
│ ├── block.js
│ ├── qrc20-balance.js
│ ├── transaction.js
│ ├── evm-receipt-log.js
│ ├── contract.js
│ ├── balance-change.js
│ ├── gas-refund.js
│ ├── address.js
│ ├── transaction-input.js
│ ├── qrc721.js
│ ├── transaction-output.js
│ ├── header.js
│ ├── qrc20.js
│ └── evm-receipt.js
├── service
│ ├── statistics.js
│ ├── qrc721.js
│ ├── info.js
│ ├── misc.js
│ ├── balance.js
│ ├── block.js
│ └── contract.js
└── router.js
├── package.json
├── doc
├── blockchain.md
├── block.md
├── qrc20.md
├── transaction.md
└── contract.md
├── .gitignore
├── agent.js
├── app.js
├── .eslintrc.json
└── README.md
/config/plugin.js:
--------------------------------------------------------------------------------
1 | exports.cors = {
2 | enable: true,
3 | package: 'egg-cors'
4 | }
5 |
6 | exports.io = {
7 | enable: true,
8 | package: 'egg-socket.io'
9 | }
10 |
11 | exports.redis = {
12 | enable: true,
13 | package: 'egg-redis'
14 | }
15 |
16 | exports.sequelize = {
17 | enable: true,
18 | package: 'egg-sequelize'
19 | }
20 |
--------------------------------------------------------------------------------
/app/middleware/transaction.js:
--------------------------------------------------------------------------------
1 | module.exports = () => async function transaction(ctx, next) {
2 | const db = ctx.model
3 | ctx.state.transaction = await db.transaction()
4 | try {
5 | await next()
6 | await ctx.state.transaction.commit()
7 | } catch (err) {
8 | await ctx.state.transaction.rollback()
9 | throw err
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/app/io/middleware/connection.js:
--------------------------------------------------------------------------------
1 | module.exports = () => async function connection(ctx, next) {
2 | const {app, socket} = ctx
3 | let interval = setInterval(() => {
4 | if (app.blockchainInfo.tip) {
5 | socket.emit('block-height', app.blockchainInfo.tip.height)
6 | clearInterval(interval)
7 | }
8 | }, 0)
9 | await next()
10 | }
11 |
--------------------------------------------------------------------------------
/app/middleware/ratelimit.js:
--------------------------------------------------------------------------------
1 | const ratelimit = require('koa-ratelimit')
2 |
3 | module.exports = (options, app) => ratelimit({
4 | ...options,
5 | id: ctx => `${app.name}-${ctx.get('cf-conecting-ip') || ctx.get('x-forwarded-for') || ctx.ip}`,
6 | whitelist: options.whitelist && (
7 | ctx => options.whitelist.includes(ctx.get('cf-connecting-ip') || ctx.get('x-forwarded-for') || ctx.ip)
8 | || options.whitelist.includes(ctx.get('application-id'))
9 | )
10 | })
11 |
--------------------------------------------------------------------------------
/app/extend/application.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | const CHAIN = Symbol('qtum.chain')
4 |
5 | module.exports = {
6 | get chain() {
7 | this[CHAIN] = this[CHAIN] || this.qtuminfo.lib.Chain.get(this.config.qtum.chain)
8 | return this[CHAIN]
9 | },
10 | get qtuminfo() {
11 | return {
12 | lib: require(path.resolve(this.config.qtuminfo.path, 'lib')),
13 | rpc: require(path.resolve(this.config.qtuminfo.path, 'rpc'))
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/app/io/controller/default.js:
--------------------------------------------------------------------------------
1 | const {Controller} = require('egg')
2 |
3 | class DefaultController extends Controller {
4 | async subscribe() {
5 | const {ctx} = this
6 | let rooms = ctx.args
7 | if (rooms.length) {
8 | ctx.socket.join(...rooms)
9 | }
10 | }
11 |
12 | async unsubscribe() {
13 | const {ctx} = this
14 | let rooms = ctx.args
15 | if (rooms.length) {
16 | ctx.socket.leave(...rooms)
17 | }
18 | }
19 | }
20 |
21 | module.exports = DefaultController
22 |
--------------------------------------------------------------------------------
/app/schedule/update-price.js:
--------------------------------------------------------------------------------
1 | const {Subscription} = require('egg')
2 |
3 | class UpdatePriceSubscription extends Subscription {
4 | static get schedule() {
5 | return {
6 | cron: '0 * * * *',
7 | type: 'worker'
8 | }
9 | }
10 |
11 | async subscribe() {
12 | let price = await this.ctx.service.misc.getPrices()
13 | await this.app.redis.hset(this.app.name, 'qtum-price', JSON.stringify(price))
14 | this.app.io.of('/').to('coin')
15 | .emit('qtum-price', price)
16 | }
17 | }
18 |
19 | module.exports = UpdatePriceSubscription
20 |
--------------------------------------------------------------------------------
/app/schedule/update-feerate.js:
--------------------------------------------------------------------------------
1 | const {Subscription} = require('egg')
2 |
3 | class UpdateFeerateSubscription extends Subscription {
4 | static get schedule() {
5 | return {
6 | cron: '0 * * * *',
7 | type: 'worker'
8 | }
9 | }
10 |
11 | async subscribe() {
12 | let feeRate = await this.ctx.service.info.getFeeRates()
13 | if (feeRate) {
14 | await this.app.redis.hset(this.app.name, 'feerate', JSON.stringify(feeRate))
15 | this.app.io.of('/').to('blockchain')
16 | .emit('feerate', feeRate.find(item => item.blocks === 10).feeRate)
17 | }
18 | }
19 | }
20 |
21 | module.exports = UpdateFeerateSubscription
22 |
--------------------------------------------------------------------------------
/app/controller/qrc721.js:
--------------------------------------------------------------------------------
1 | const {Controller} = require('egg')
2 |
3 | class QRC721Controller extends Controller {
4 | async list() {
5 | const {ctx} = this
6 | let {totalCount, tokens} = await ctx.service.qrc721.listQRC721Tokens()
7 | ctx.body = {
8 | totalCount,
9 | tokens: tokens.map(item => ({
10 | address: item.addressHex.toString('hex'),
11 | addressHex: item.addressHex.toString('hex'),
12 | name: item.name,
13 | symbol: item.symbol,
14 | totalSupply: item.totalSupply.toString(),
15 | holders: item.holders
16 | }))
17 | }
18 | }
19 | }
20 |
21 | module.exports = QRC721Controller
22 |
--------------------------------------------------------------------------------
/app/model/contract-tag.js:
--------------------------------------------------------------------------------
1 | module.exports = app => {
2 | const {BIGINT, CHAR, STRING} = app.Sequelize
3 |
4 | let ContractTag = app.model.define('contract_tag', {
5 | _id: {
6 | type: BIGINT.UNSIGNED,
7 | field: '_id',
8 | primaryKey: true
9 | },
10 | contractAddress: CHAR(20).BINARY,
11 | tag: STRING(32)
12 | }, {freezeTableName: true, underscored: true, timestamps: false})
13 |
14 | ContractTag.associate = () => {
15 | const {Contract} = app.model
16 | Contract.hasMany(ContractTag, {as: 'tags', foreignKey: 'contractAddress'})
17 | ContractTag.belongsTo(Contract, {as: 'contract', foreignKey: 'contractAddress'})
18 | }
19 |
20 | return ContractTag
21 | }
22 |
--------------------------------------------------------------------------------
/app/model/qrc20-statistics.js:
--------------------------------------------------------------------------------
1 | module.exports = app => {
2 | const {INTEGER, CHAR} = app.Sequelize
3 |
4 | let QRC20Statistics = app.model.define('qrc20_statistics', {
5 | contractAddress: {
6 | type: CHAR(20).BINARY,
7 | primaryKey: true
8 | },
9 | holders: INTEGER.UNSIGNED,
10 | transactions: INTEGER.UNSIGNED
11 | }, {freezeTableName: true, underscored: true, timestamps: false})
12 |
13 | QRC20Statistics.associate = () => {
14 | const {Qrc20: QRC20} = app.model
15 | QRC20Statistics.belongsTo(QRC20, {as: 'qrc20', foreignKey: 'contractAddress'})
16 | QRC20.hasOne(QRC20Statistics, {as: 'statistics', foreignKey: 'contractAddress'})
17 | }
18 |
19 | return QRC20Statistics
20 | }
21 |
--------------------------------------------------------------------------------
/app/model/contract-code.js:
--------------------------------------------------------------------------------
1 | module.exports = app => {
2 | const {CHAR, BLOB, TEXT} = app.Sequelize
3 |
4 | let ContractCode = app.model.define('contract_code', {
5 | contractAddress: {
6 | type: CHAR(20).BINARY,
7 | primaryKey: true
8 | },
9 | code: BLOB,
10 | source: {
11 | type: TEXT('long'),
12 | allowNull: true
13 | }
14 | }, {freezeTableName: true, underscored: true, timestamps: false})
15 |
16 | ContractCode.associate = () => {
17 | const {Contract} = app.model
18 | Contract.hasOne(ContractCode, {as: 'code', foreignKey: 'contractAddress'})
19 | ContractCode.belongsTo(Contract, {as: 'contract', foreignKey: 'contractAddress'})
20 | }
21 |
22 | return ContractCode
23 | }
24 |
--------------------------------------------------------------------------------
/app/model/qrc721-token.js:
--------------------------------------------------------------------------------
1 | module.exports = app => {
2 | const {CHAR} = app.Sequelize
3 |
4 | let QRC721Token = app.model.define('qrc721_token', {
5 | contractAddress: {
6 | type: CHAR(20).BINARY,
7 | primaryKey: true
8 | },
9 | tokenId: {
10 | type: CHAR(32).BINARY,
11 | primaryKey: true
12 | },
13 | holder: CHAR(20).BINARY
14 | }, {freezeTableName: true, underscored: true, timestamps: false})
15 |
16 | QRC721Token.associate = () => {
17 | const {Contract} = app.model
18 | Contract.hasMany(QRC721Token, {as: 'qrc721Tokens', foreignKey: 'contractAddress'})
19 | QRC721Token.belongsTo(Contract, {as: 'contract', foreignKey: 'contractAddress'})
20 | }
21 |
22 | return QRC721Token
23 | }
24 |
--------------------------------------------------------------------------------
/app/controller/info.js:
--------------------------------------------------------------------------------
1 | const {Controller} = require('egg')
2 |
3 | class InfoController extends Controller {
4 | async index() {
5 | this.ctx.body = await this.ctx.service.info.getInfo()
6 | }
7 |
8 | async supply() {
9 | this.ctx.body = this.ctx.service.info.getTotalSupply()
10 | }
11 |
12 | async totalMaxSupply() {
13 | this.ctx.body = this.ctx.service.info.getTotalMaxSupply()
14 | }
15 |
16 | async circulatingSupply() {
17 | this.ctx.body = this.ctx.service.info.getCirculatingSupply()
18 | }
19 |
20 | async feeRates() {
21 | this.ctx.body = JSON.parse(await this.app.redis.hget(this.app.name, 'feerate')).filter(item => [2, 4, 6, 12, 24].includes(item.blocks))
22 | }
23 | }
24 |
25 | module.exports = InfoController
26 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "qtuminfo-api",
3 | "private": true,
4 | "scripts": {
5 | "start": "egg-scripts start --title=qtuminfo-api --sticky --daemon",
6 | "stop": "egg-scripts stop --title=qtuminfo-api",
7 | "dev": "egg-bin dev",
8 | "lint": "eslint --ext .js app config"
9 | },
10 | "dependencies": {
11 | "egg": "^2.23.0",
12 | "egg-cors": "^2.2.0",
13 | "egg-redis": "^2.4.0",
14 | "egg-scripts": "^2.11.0",
15 | "egg-sequelize": "^5.2.0",
16 | "egg-socket.io": "^4.1.5",
17 | "ioredis": "^4.14.1",
18 | "koa-ratelimit": "^4.2.0",
19 | "mysql2": "^1.7.0",
20 | "socket.io-client": "^2.3.0"
21 | },
22 | "devDependencies": {
23 | "babel-eslint": "^10.0.3",
24 | "egg-bin": "^4.13.1",
25 | "eslint": "^6.4.0"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/extend/helper.js:
--------------------------------------------------------------------------------
1 | function transformSQLArg(arg) {
2 | if (typeof arg === 'string') {
3 | return `'${arg}'`
4 | } else if (['number', 'bigint'].includes(typeof arg)) {
5 | return arg.toString()
6 | } else if (Buffer.isBuffer(arg)) {
7 | return `X'${arg.toString('hex')}'`
8 | } else if (Array.isArray(arg)) {
9 | return arg.length === 0 ? '(NULL)' : `(${arg.map(transformSQLArg).join(', ')})`
10 | } else if (arg && 'raw' in arg) {
11 | return arg.raw
12 | }
13 | return arg.toString()
14 | }
15 |
16 | exports.sql = function(strings, ...args) {
17 | let buffer = []
18 | for (let i = 0; i < args.length; ++i) {
19 | buffer.push(strings[i].replace(/\s+/g, ' '), transformSQLArg(args[i]))
20 | }
21 | buffer.push(strings[args.length].replace(/\s+/g, ' '))
22 | return buffer.join('')
23 | }
24 |
--------------------------------------------------------------------------------
/app/model/witness.js:
--------------------------------------------------------------------------------
1 | module.exports = app => {
2 | const {INTEGER, CHAR, BLOB} = app.Sequelize
3 |
4 | let Witness = app.model.define('witness', {
5 | transactionId: {
6 | type: CHAR(32).BINARY,
7 | primaryKey: true
8 | },
9 | inputIndex: {
10 | type: INTEGER.UNSIGNED,
11 | primaryKey: true
12 | },
13 | witnessIndex: {
14 | type: INTEGER.UNSIGNED,
15 | primaryKey: true
16 | },
17 | script: BLOB
18 | }, {freezeTableName: true, underscored: true, timestamps: false})
19 |
20 | Witness.associate = () => {
21 | const {Transaction} = app.model
22 | Transaction.hasMany(Witness, {as: 'witnesses', foreignKey: 'transactionId', sourceKey: 'id'})
23 | Witness.belongsTo(Transaction, {foreignKey: 'transactionId', targetKey: 'id'})
24 | }
25 |
26 | return Witness
27 | }
28 |
--------------------------------------------------------------------------------
/app/model/contract-spend.js:
--------------------------------------------------------------------------------
1 | module.exports = app => {
2 | const {BIGINT} = app.Sequelize
3 |
4 | let ContractSpend = app.model.define('contract_spend', {
5 | sourceId: {
6 | type: BIGINT.UNSIGNED,
7 | primaryKey: true
8 | },
9 | destId: BIGINT.UNSIGNED
10 | }, {freezeTableName: true, underscored: true, timestamps: false})
11 |
12 | ContractSpend.associate = () => {
13 | const {Transaction} = app.model
14 | Transaction.hasOne(ContractSpend, {as: 'contractSpendSource', foreignKey: 'sourceId'})
15 | ContractSpend.belongsTo(Transaction, {as: 'sourceTransaction', foreignKey: 'sourceId'})
16 | Transaction.hasMany(ContractSpend, {as: 'contractSpendDests', foreignKey: 'destId'})
17 | ContractSpend.belongsTo(Transaction, {as: 'destTransaction', foreignKey: 'destId'})
18 | }
19 |
20 | return ContractSpend
21 | }
22 |
--------------------------------------------------------------------------------
/app/model/rich-list.js:
--------------------------------------------------------------------------------
1 | module.exports = app => {
2 | const {BIGINT} = app.Sequelize
3 |
4 | let RichList = app.model.define('rich_list', {
5 | addressId: {
6 | type: BIGINT.UNSIGNED,
7 | primaryKey: true
8 | },
9 | balance: {
10 | type: BIGINT,
11 | get() {
12 | let balance = this.getDataValue('balance')
13 | return balance == null ? null : BigInt(balance)
14 | },
15 | set(balance) {
16 | this.setDataValue('balance', balance.toString())
17 | }
18 | }
19 | }, {freezeTableName: true, underscored: true, timestamps: false})
20 |
21 | RichList.associate = () => {
22 | const {Address} = app.model
23 | Address.hasOne(RichList, {as: 'balance', foreignKey: 'addressId'})
24 | RichList.belongsTo(Address, {as: 'address', foreignKey: 'addressId'})
25 | }
26 |
27 | return RichList
28 | }
29 |
--------------------------------------------------------------------------------
/doc/blockchain.md:
--------------------------------------------------------------------------------
1 | # Blockchain API
2 |
3 | - [Blockchain API](#Blockchain-API)
4 | - [Blockchain Information](#Blockchain-Information)
5 | - [Supply](#Supply)
6 | - [Total Max Supply](#Total-Max-Supply)
7 |
8 |
9 | ## Blockchain Information
10 |
11 | **Request**
12 | ```
13 | GET /info
14 | ```
15 |
16 | **Response**
17 | ```json
18 | {
19 | "height": 405961,
20 | "supply": 101603844,
21 | "circulatingSupply": 95853844,
22 | "netStakeWeight": 1095728543244388,
23 | "feeRate": 0.00401787,
24 | "dgpInfo": {
25 | "maxBlockSize": 2000000,
26 | "minGasPrice": 40,
27 | "blockGasLimit": 40000000
28 | }
29 | }
30 | ```
31 |
32 |
33 | ## Supply
34 |
35 | **Request**
36 | ```
37 | GET /supply
38 | ```
39 |
40 | **Response**
41 | ```json
42 | 101603852
43 | ```
44 |
45 |
46 | ## Total Max Supply
47 |
48 | **Request**
49 | ```
50 | GET /total-max-supply
51 | ```
52 |
53 | **Response**
54 | ```json
55 | 107822406.25
56 | ```
57 |
--------------------------------------------------------------------------------
/app/model/block.js:
--------------------------------------------------------------------------------
1 | module.exports = app => {
2 | const {INTEGER, BIGINT, CHAR} = app.Sequelize
3 |
4 | let Block = app.model.define('block', {
5 | hash: {
6 | type: CHAR(32).BINARY,
7 | unique: true
8 | },
9 | height: {
10 | type: INTEGER.UNSIGNED,
11 | primaryKey: true
12 | },
13 | size: INTEGER.UNSIGNED,
14 | weight: INTEGER.UNSIGNED,
15 | minerId: BIGINT.UNSIGNED,
16 | transactionsCount: INTEGER.UNSIGNED,
17 | contractTransactionsCount: INTEGER.UNSIGNED
18 | }, {freezeTableName: true, underscored: true, timestamps: false})
19 |
20 | Block.associate = () => {
21 | const {Header, Address} = app.model
22 | Header.hasOne(Block, {as: 'block', foreignKey: 'height'})
23 | Block.belongsTo(Header, {as: 'header', foreignKey: 'height'})
24 | Address.hasOne(Block, {as: 'minedBlocks', foreignKey: 'minerId'})
25 | Block.belongsTo(Address, {as: 'miner', foreignKey: 'minerId'})
26 | }
27 |
28 | return Block
29 | }
30 |
--------------------------------------------------------------------------------
/app/middleware/address.js:
--------------------------------------------------------------------------------
1 | module.exports = () => async function address(ctx, next) {
2 | ctx.assert(ctx.params.address, 404)
3 | const {Address: RawAddress} = ctx.app.qtuminfo.lib
4 | const chain = ctx.app.chain
5 | const {Address} = ctx.model
6 | const {in: $in} = ctx.app.Sequelize.Op
7 |
8 | let addresses = ctx.params.address.split(',')
9 | let rawAddresses = []
10 | for (let address of addresses) {
11 | try {
12 | rawAddresses.push(RawAddress.fromString(address, chain))
13 | } catch (err) {
14 | ctx.throw(400)
15 | }
16 | }
17 | let result = await Address.findAll({
18 | where: {string: {[$in]: addresses}},
19 | attributes: ['_id', 'type', 'data'],
20 | transaction: ctx.state.transaction
21 | })
22 | ctx.state.address = {
23 | rawAddresses,
24 | addressIds: result.map(address => address._id),
25 | p2pkhAddressIds: result.filter(address => address.type === RawAddress.PAY_TO_PUBLIC_KEY_HASH).map(address => address._id),
26 | }
27 | await next()
28 | }
29 |
--------------------------------------------------------------------------------
/app/model/qrc20-balance.js:
--------------------------------------------------------------------------------
1 | module.exports = app => {
2 | const {CHAR} = app.Sequelize
3 |
4 | let QRC20Balance = app.model.define('qrc20_balance', {
5 | contractAddress: {
6 | type: CHAR(20).BINARY,
7 | primaryKey: true
8 | },
9 | address: {
10 | type: CHAR(20).BINARY,
11 | primaryKey: true
12 | },
13 | balance: {
14 | type: CHAR(32).BINARY,
15 | get() {
16 | let balance = this.getDataValue('balance')
17 | return balance == null ? null : BigInt(`0x${balance.toString('hex')}`)
18 | },
19 | set(balance) {
20 | this.setDataValue(
21 | 'balance',
22 | Buffer.from(balance.toString(16).padStart(64, '0'), 'hex')
23 | )
24 | }
25 | }
26 | }, {freezeTableName: true, underscored: true, timestamps: false})
27 |
28 | QRC20Balance.associate = () => {
29 | const {Contract} = app.model
30 | Contract.hasMany(QRC20Balance, {as: 'qrc20Balances', foreignKey: 'contractAddress'})
31 | QRC20Balance.belongsTo(Contract, {as: 'contract', foreignKey: 'contractAddress'})
32 | }
33 |
34 | return QRC20Balance
35 | }
36 |
--------------------------------------------------------------------------------
/app/controller/statistics.js:
--------------------------------------------------------------------------------
1 | const {Controller} = require('egg')
2 |
3 | class StatisticsController extends Controller {
4 | async dailyTransactions() {
5 | const {app, ctx} = this
6 | let dailyTransactions = JSON.parse(await app.redis.hget(app.name, 'daily-transactions') || '[]')
7 | ctx.body = dailyTransactions.map(({timestamp, transactionsCount, contractTransactionsCount}) => ({
8 | time: new Date(timestamp * 1000),
9 | transactionCount: transactionsCount,
10 | contractTransactionCount: contractTransactionsCount
11 | }))
12 | }
13 |
14 | async blockInterval() {
15 | const {app, ctx} = this
16 | let blockInterval = JSON.parse(await app.redis.hget(app.name, 'block-interval') || '[]')
17 | ctx.body = blockInterval
18 | }
19 |
20 | async addressGrowth() {
21 | const {app, ctx} = this
22 | let addressGrowth = JSON.parse(await app.redis.hget(app.name, 'address-growth') || '[]')
23 | ctx.body = addressGrowth.map(({timestamp, count}) => ({
24 | time: new Date(timestamp * 1000),
25 | addresses: count
26 | }))
27 | }
28 | }
29 |
30 | module.exports = StatisticsController
31 |
--------------------------------------------------------------------------------
/app/model/transaction.js:
--------------------------------------------------------------------------------
1 | module.exports = app => {
2 | const {INTEGER, BIGINT, CHAR} = app.Sequelize
3 |
4 | let Transaction = app.model.define('transaction', {
5 | _id: {
6 | type: BIGINT.UNSIGNED,
7 | field: '_id',
8 | primaryKey: true,
9 | autoIncrement: true
10 | },
11 | id: {
12 | type: CHAR(32).BINARY,
13 | unique: true
14 | },
15 | hash: CHAR(32).BINARY,
16 | version: INTEGER,
17 | flag: INTEGER(3).UNSIGNED,
18 | lockTime: INTEGER.UNSIGNED,
19 | blockHeight: INTEGER.UNSIGNED,
20 | indexInBlock: INTEGER.UNSIGNED,
21 | size: INTEGER.UNSIGNED,
22 | weight: INTEGER.UNSIGNED
23 | }, {freezeTableName: true, underscored: true, timestamps: false})
24 |
25 | Transaction.associate = () => {
26 | const {Header, Block} = app.model
27 | Header.hasMany(Transaction, {as: 'transactions', foreignKey: 'blockHeight'})
28 | Transaction.belongsTo(Header, {as: 'header', foreignKey: 'blockHeight'})
29 | Block.hasMany(Transaction, {as: 'transactions', foreignKey: 'blockHeight'})
30 | Transaction.belongsTo(Block, {as: 'block', foreignKey: 'blockHeight'})
31 | }
32 |
33 | return Transaction
34 | }
35 |
--------------------------------------------------------------------------------
/app/model/evm-receipt-log.js:
--------------------------------------------------------------------------------
1 | module.exports = app => {
2 | const {INTEGER, BIGINT, CHAR, STRING, BLOB} = app.Sequelize
3 |
4 | let EVMReceiptLog = app.model.define('evm_receipt_log', {
5 | _id: {
6 | type: BIGINT.UNSIGNED,
7 | field: '_id',
8 | primaryKey: true,
9 | autoIncrement: true
10 | },
11 | receiptId: BIGINT.UNSIGNED,
12 | logIndex: INTEGER.UNSIGNED,
13 | blockHeight: INTEGER.UNSIGNED,
14 | address: CHAR(20).BINARY,
15 | topic1: {
16 | type: STRING(32).BINARY,
17 | allowNull: true
18 | },
19 | topic2: {
20 | type: STRING(32).BINARY,
21 | allowNull: true
22 | },
23 | topic3: {
24 | type: STRING(32).BINARY,
25 | allowNull: true
26 | },
27 | topic4: {
28 | type: STRING(32).BINARY,
29 | allowNull: true
30 | },
31 | data: BLOB
32 | }, {freezeTableName: true, underscored: true, timestamps: false})
33 |
34 | EVMReceiptLog.associate = () => {
35 | const {EvmReceipt: EVMReceipt} = app.model
36 | EVMReceipt.hasMany(EVMReceiptLog, {as: 'logs', foreignKey: 'receiptId'})
37 | EVMReceiptLog.belongsTo(EVMReceipt, {as: 'receipt', foreignKey: 'receiptId'})
38 | }
39 |
40 | return EVMReceiptLog
41 | }
42 |
--------------------------------------------------------------------------------
/app/model/contract.js:
--------------------------------------------------------------------------------
1 | module.exports = app => {
2 | const {CHAR, TEXT, ENUM} = app.Sequelize
3 |
4 | let Contract = app.model.define('contract', {
5 | address: {
6 | type: CHAR(20).BINARY,
7 | primaryKey: true
8 | },
9 | addressString: CHAR(34),
10 | vm: {
11 | type: ENUM,
12 | values: ['evm', 'x86']
13 | },
14 | type: {
15 | type: ENUM,
16 | values: ['dgp', 'qrc20', 'qrc721'],
17 | allowNull: true
18 | },
19 | description: {
20 | type: TEXT,
21 | defaultValue: ''
22 | }
23 | }, {freezeTableName: true, underscored: true, timestamps: false})
24 |
25 | Contract.associate = () => {
26 | const {Address, EvmReceipt: EVMReceipt, EvmReceiptLog: EVMReceiptLog} = app.model
27 | Contract.hasOne(Address, {as: 'originalAddress', foreignKey: 'data'})
28 | Address.belongsTo(Contract, {as: 'contract', foreignKey: 'data'})
29 | EVMReceipt.belongsTo(Contract, {as: 'contract', foreignKey: 'contractAddress'})
30 | Contract.hasMany(EVMReceipt, {as: 'evmReceipts', foreignKey: 'contractAddress'})
31 | EVMReceiptLog.belongsTo(Contract, {as: 'contract', foreignKey: 'address'})
32 | Contract.hasMany(EVMReceiptLog, {as: 'evmLogs', foreignKey: 'address'})
33 | }
34 |
35 | return Contract
36 | }
37 |
--------------------------------------------------------------------------------
/app/model/balance-change.js:
--------------------------------------------------------------------------------
1 | module.exports = app => {
2 | const {INTEGER, BIGINT} = app.Sequelize
3 |
4 | let BalanceChange = app.model.define('balance_change', {
5 | transactionId: {
6 | type: BIGINT.UNSIGNED,
7 | primaryKey: true
8 | },
9 | blockHeight: INTEGER.UNSIGNED,
10 | indexInBlock: INTEGER.UNSIGNED,
11 | addressId: {
12 | type: BIGINT.UNSIGNED,
13 | primaryKey: true
14 | },
15 | value: {
16 | type: BIGINT,
17 | get() {
18 | let value = this.getDataValue('value')
19 | return value == null ? null : BigInt(value)
20 | },
21 | set(value) {
22 | this.setDataValue('value', value.toString())
23 | }
24 | }
25 | }, {freezeTableName: true, underscored: true, timestamps: false})
26 |
27 | BalanceChange.associate = () => {
28 | const {Header, Address, Transaction} = app.model
29 | Transaction.hasMany(BalanceChange, {as: 'balanceChanges', foreignKey: 'transactionId'})
30 | BalanceChange.belongsTo(Transaction, {as: 'transaction', foreignKey: 'transactionId'})
31 | Address.hasOne(BalanceChange, {as: 'balanceChanges', foreignKey: 'addressId'})
32 | BalanceChange.belongsTo(Address, {as: 'address', foreignKey: 'addressId'})
33 | BalanceChange.belongsTo(Header, {as: 'header', foreignKey: 'blockHeight'})
34 | }
35 |
36 | return BalanceChange
37 | }
38 |
--------------------------------------------------------------------------------
/app/controller/misc.js:
--------------------------------------------------------------------------------
1 | const {Controller} = require('egg')
2 |
3 | class MiscController extends Controller {
4 | async classify() {
5 | let {ctx} = this
6 | ctx.body = await ctx.service.misc.classify(ctx.query.query)
7 | }
8 |
9 | async richList() {
10 | let {ctx} = this
11 | let {totalCount, list} = await ctx.service.balance.getRichList()
12 | ctx.body = {
13 | totalCount,
14 | list: list.map(item => ({
15 | address: item.addressHex ? item.addressHex.toString('hex') : item.address,
16 | addressHex: item.addressHex && item.addressHex.toString('hex'),
17 | balance: item.balance.toString()
18 | }))
19 | }
20 | }
21 |
22 | async biggestMiners() {
23 | let {ctx} = this
24 | let lastNBlocks = null
25 | if (ctx.query.blocks && /^[1-9]\d*$/.test(ctx.query.blocks)) {
26 | lastNBlocks = Number.parseInt(ctx.query.blocks)
27 | }
28 | let {totalCount, list} = await ctx.service.block.getBiggestMiners(lastNBlocks)
29 | ctx.body = {
30 | totalCount,
31 | list: list.map(item => ({
32 | address: item.address,
33 | blocks: item.blocks,
34 | balance: item.balance.toString()
35 | }))
36 | }
37 | }
38 |
39 | async prices() {
40 | this.ctx.body = JSON.parse(await this.app.redis.hget(this.app.name, 'qtum-price'))
41 | }
42 | }
43 |
44 | module.exports = MiscController
45 |
--------------------------------------------------------------------------------
/app/middleware/pagination.js:
--------------------------------------------------------------------------------
1 | module.exports = ({defaultPageSize = 100} = {}) => async function pagination(ctx, next) {
2 | if (!['GET', 'POST'].includes(ctx.method)) {
3 | return await next()
4 | }
5 | let object = {GET: ctx.query, POST: ctx.request.body}[ctx.method]
6 | let limit = defaultPageSize
7 | let offset = 0
8 | let reversed
9 | if ('limit' in object && 'offset' in object) {
10 | limit = Number.parseInt(object.limit)
11 | offset = Number.parseInt(object.offset)
12 | }
13 | if ('pageSize' in object && 'pageIndex' in object) {
14 | let pageSize = Number.parseInt(object.pageSize)
15 | let pageIndex = Number.parseInt(object.pageIndex)
16 | limit = pageSize
17 | offset = pageSize * pageIndex
18 | }
19 | if ('pageSize' in object && 'page' in object) {
20 | let pageSize = Number.parseInt(object.pageSize)
21 | let pageIndex = Number.parseInt(object.page)
22 | limit = pageSize
23 | offset = pageSize * pageIndex
24 | }
25 | if ('from' in object && 'to' in object) {
26 | let from = Number.parseInt(object.from)
27 | let to = Number.parseInt(object.to)
28 | limit = to - from + 1
29 | offset = from
30 | }
31 | ctx.assert(limit > 0 && offset >= 0, 400)
32 | if ('reversed' in object) {
33 | reversed = ![false, 'false', 0, '0'].includes(object.reversed)
34 | }
35 | ctx.state.pagination = {limit, offset, reversed}
36 | await next()
37 | }
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # parcel-bundler cache (https://parceljs.org/)
61 | .cache
62 |
63 | # next.js build output
64 | .next
65 |
66 | # nuxt.js build output
67 | .nuxt
68 |
69 | # vuepress build output
70 | .vuepress/dist
71 |
72 | # Serverless directories
73 | .serverless
74 |
75 | /run
76 | config/config.local.js
77 |
--------------------------------------------------------------------------------
/app/model/gas-refund.js:
--------------------------------------------------------------------------------
1 | module.exports = app => {
2 | const {INTEGER, BIGINT} = app.Sequelize
3 |
4 | let GasRefund = app.model.define('gas_refund', {
5 | transactionId: {
6 | type: BIGINT.UNSIGNED,
7 | primaryKey: true
8 | },
9 | outputIndex: {
10 | type: INTEGER.UNSIGNED,
11 | primaryKey: true
12 | },
13 | refundId: {
14 | type: BIGINT.UNSIGNED,
15 | unique: 'refund'
16 | },
17 | refundIndex: {
18 | type: INTEGER.UNSIGNED,
19 | unique: 'refund'
20 | }
21 | }, {freezeTableName: true, underscored: true, timestamps: false})
22 |
23 | GasRefund.associate = () => {
24 | const {Transaction, TransactionOutput} = app.model
25 | Transaction.hasMany(GasRefund, {as: 'refunds', foreignKey: 'transactionId'})
26 | GasRefund.belongsTo(Transaction, {as: 'transaction', foreignKey: 'transactionId'})
27 | TransactionOutput.hasOne(GasRefund, {as: 'refund', foreignKey: 'transactionId'})
28 | GasRefund.belongsTo(TransactionOutput, {as: 'refund', foreignKey: 'transactionId'})
29 | Transaction.hasOne(GasRefund, {as: 'refundToTransaction', foreignKey: 'refundId'})
30 | GasRefund.belongsTo(Transaction, {as: 'refundToTransaction', foreignKey: 'refundId'})
31 | TransactionOutput.hasOne(GasRefund, {as: 'refundTo', foreignKey: 'refundId'})
32 | GasRefund.belongsTo(TransactionOutput, {as: 'refundTo', foreignKey: 'refundId'})
33 | }
34 |
35 | return GasRefund
36 | }
37 |
--------------------------------------------------------------------------------
/config/config.default.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const Redis = require('ioredis')
3 |
4 | const redisConfig = {
5 | host: 'localhost',
6 | port: 6379,
7 | password: '',
8 | db: 0
9 | }
10 |
11 | exports.keys = 'qtuminfo-api'
12 |
13 | exports.security = {
14 | csrf: {enable: false}
15 | }
16 |
17 | exports.middleware = ['ratelimit']
18 |
19 | exports.redis = {
20 | client: redisConfig
21 | }
22 |
23 | exports.ratelimit = {
24 | db: new Redis(redisConfig),
25 | headers: {
26 | remaining: 'Rate-Limit-Remaining',
27 | reset: 'Rate-Limit-Reset',
28 | total: 'Rate-Limit-Total',
29 | },
30 | disableHeader: false,
31 | errorMessage: 'Rate Limit Exceeded',
32 | duration: 10 * 60 * 1000,
33 | max: 10 * 60
34 | }
35 |
36 | exports.io = {
37 | redis: {
38 | ...redisConfig,
39 | key: 'qtuminfo-api-socket.io'
40 | },
41 | namespace: {
42 | '/': {connectionMiddleware: ['connection']}
43 | }
44 | }
45 |
46 | exports.sequelize = {
47 | dialect: 'mysql',
48 | database: 'qtum_mainnet',
49 | host: 'localhost',
50 | port: 3306,
51 | username: 'qtum',
52 | password: ''
53 | }
54 |
55 | exports.qtum = {
56 | chain: 'mainnet'
57 | }
58 |
59 | exports.qtuminfo = {
60 | path: path.resolve('..', 'qtuminfo'),
61 | port: 3001,
62 | rpc: {
63 | protocol: 'http',
64 | host: 'localhost',
65 | port: 3889,
66 | user: 'user',
67 | password: 'password'
68 | }
69 | }
70 |
71 | exports.cmcAPIKey = null
72 |
--------------------------------------------------------------------------------
/app/middleware/contract.js:
--------------------------------------------------------------------------------
1 | module.exports = (paramName = 'contract') => async function contract(ctx, next) {
2 | ctx.assert(ctx.params[paramName], 404)
3 | const {Address: RawAddress} = ctx.app.qtuminfo.lib
4 | const chain = ctx.app.chain
5 | const {Address, Contract} = ctx.model
6 | const {gte: $gte} = ctx.app.Sequelize.Op
7 |
8 | let contract = {}
9 | let rawAddress
10 | try {
11 | rawAddress = RawAddress.fromString(ctx.params[paramName], chain)
12 | } catch (err) {
13 | ctx.throw(400)
14 | }
15 | let filter
16 | if (rawAddress.type === RawAddress.CONTRACT) {
17 | filter = {address: Buffer.from(ctx.params[paramName], 'hex')}
18 | } else if (rawAddress.type === RawAddress.EVM_CONTRACT) {
19 | filter = {addressString: ctx.params[paramName]}
20 | } else {
21 | ctx.throw(400)
22 | }
23 | let contractResult = await Contract.findOne({
24 | where: filter,
25 | attributes: ['address', 'addressString', 'vm', 'type'],
26 | transaction: ctx.state.transaction
27 | })
28 | ctx.assert(contractResult, 404)
29 | contract.contractAddress = contractResult.address
30 | contract.address = contractResult.addressString
31 | contract.vm = contractResult.vm
32 | contract.type = contractResult.type
33 |
34 | let addressList = await Address.findAll({
35 | where: {
36 | type: {[$gte]: Address.parseType('contract')},
37 | data: contract.contractAddress
38 | },
39 | attributes: ['_id'],
40 | transaction: ctx.state.transaction
41 | })
42 | contract.addressIds = addressList.map(address => address._id)
43 | ctx.state[paramName] = contract
44 | await next()
45 | }
46 |
--------------------------------------------------------------------------------
/app/model/address.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 | const addressTypes = {
3 | pubkeyhash: 1,
4 | scripthash: 2,
5 | witness_v0_keyhash: 3,
6 | witness_v0_scripthash: 4,
7 | contract: 0x80,
8 | evm_contract: 0x81,
9 | x86_contract: 0x82
10 | }
11 | /* eslint-enable camelcase*/
12 | const addressTypeMap = {
13 | 1: 'pubkeyhash',
14 | 2: 'scripthash',
15 | 3: 'witness_v0_keyhash',
16 | 4: 'witness_v0_scripthash',
17 | 0x80: 'contract',
18 | 0x81: 'evm_contract',
19 | 0x82: 'x86_contract'
20 | }
21 |
22 | module.exports = app => {
23 | const {INTEGER, BIGINT, STRING} = app.Sequelize
24 |
25 | const Address = app.model.define('address', {
26 | _id: {
27 | type: BIGINT.UNSIGNED,
28 | field: '_id',
29 | primaryKey: true,
30 | autoIncrement: true
31 | },
32 | type: {
33 | type: INTEGER(3).UNSIGNED,
34 | get() {
35 | let type = this.getDataValue('type')
36 | return addressTypeMap[type] || null
37 | },
38 | set(type) {
39 | if (type != null) {
40 | this.setDataValue('type', addressTypes[type] || 0)
41 | }
42 | },
43 | unique: 'address'
44 | },
45 | data: {
46 | type: STRING(32).BINARY,
47 | unique: 'address'
48 | },
49 | string: STRING(64),
50 | createHeight: INTEGER.UNSIGNED,
51 | createIndex: INTEGER.UNSIGNED
52 | }, {freezeTableName: true, underscored: true, timestamps: false})
53 |
54 | Address.getType = function(type) {
55 | return addressTypeMap[type] || null
56 | }
57 | Address.parseType = function(type) {
58 | return addressTypes[type] || 0
59 | }
60 |
61 | return Address
62 | }
63 |
--------------------------------------------------------------------------------
/app/model/transaction-input.js:
--------------------------------------------------------------------------------
1 | module.exports = app => {
2 | const {INTEGER, BIGINT, BLOB} = app.Sequelize
3 |
4 | let TransactionInput = app.model.define('transaction_input', {
5 | transactionId: {
6 | type: BIGINT.UNSIGNED,
7 | primaryKey: true
8 | },
9 | inputIndex: {
10 | type: INTEGER.UNSIGNED,
11 | primaryKey: true
12 | },
13 | scriptSig: {
14 | type: BLOB('medium'),
15 | field: 'scriptsig'
16 | },
17 | sequence: INTEGER.UNSIGNED,
18 | blockHeight: INTEGER.UNSIGNED,
19 | value: {
20 | type: BIGINT,
21 | get() {
22 | let value = this.getDataValue('value')
23 | return value == null ? null : BigInt(value)
24 | },
25 | set(value) {
26 | this.setDataValue('value', value.toString())
27 | }
28 | },
29 | addressId: BIGINT.UNSIGNED,
30 | outputId: BIGINT.UNSIGNED,
31 | outputIndex: INTEGER.UNSIGNED
32 | }, {freezeTableName: true, underscored: true, timestamps: false})
33 |
34 | TransactionInput.associate = () => {
35 | const {Address, Transaction, TransactionOutput} = app.model
36 | Transaction.hasMany(TransactionInput, {as: 'inputs', foreignKey: 'transactionId'})
37 | TransactionInput.belongsTo(Transaction, {as: 'inputTransaction', foreignKey: 'transactionId'})
38 | TransactionInput.belongsTo(Transaction, {as: 'outputTransaction', foreignKey: 'outputId'})
39 | TransactionInput.belongsTo(TransactionOutput, {as: 'output', foreignKey: 'outputId'})
40 | Address.hasMany(TransactionInput, {as: 'inputTxos', foreignKey: 'addressId'})
41 | TransactionInput.belongsTo(Address, {as: 'address', foreignKey: 'addressId'})
42 | }
43 |
44 | return TransactionInput
45 | }
46 |
--------------------------------------------------------------------------------
/app/model/qrc721.js:
--------------------------------------------------------------------------------
1 | module.exports = app => {
2 | const {CHAR, BLOB} = app.Sequelize
3 |
4 | let QRC721 = app.model.define('qrc721', {
5 | contractAddress: {
6 | type: CHAR(20).BINARY,
7 | primaryKey: true
8 | },
9 | name: {
10 | type: BLOB,
11 | get() {
12 | return this.getDataValue('name').toString()
13 | },
14 | set(name) {
15 | this.setDataValue('name', Buffer.from(name))
16 | }
17 | },
18 | symbol: {
19 | type: BLOB,
20 | get() {
21 | return this.getDataValue('symbol').toString()
22 | },
23 | set(symbol) {
24 | this.setDataValue('symbol', Buffer.from(symbol))
25 | }
26 | },
27 | totalSupply: {
28 | type: CHAR(32).BINARY,
29 | get() {
30 | let totalSupply = this.getDataValue('totalSupply')
31 | return totalSupply == null ? null : BigInt(`0x${totalSupply.toString('hex')}`)
32 | },
33 | set(totalSupply) {
34 | this.setDataValue(
35 | 'totalSupply',
36 | Buffer.from(totalSupply.toString(16).padStart(64, '0'), 'hex')
37 | )
38 | }
39 | }
40 | }, {freezeTableName: true, underscored: true, timestamps: false})
41 |
42 | QRC721.associate = () => {
43 | const {EvmReceiptLog: EVMReceiptLog, Contract} = app.model
44 | EVMReceiptLog.belongsTo(QRC721, {as: 'qrc721', foreignKey: 'address', sourceKey: 'contractAddress'})
45 | QRC721.hasOne(EVMReceiptLog, {as: 'logs', foreignKey: 'address', sourceKey: 'contractAddress'})
46 | Contract.hasOne(QRC721, {as: 'qrc721', foreignKey: 'contractAddress'})
47 | QRC721.belongsTo(Contract, {as: 'contract', foreignKey: 'contractAddress'})
48 | }
49 |
50 | return QRC721
51 | }
52 |
--------------------------------------------------------------------------------
/app/model/transaction-output.js:
--------------------------------------------------------------------------------
1 | module.exports = app => {
2 | const {INTEGER, BIGINT, BOOLEAN, BLOB} = app.Sequelize
3 |
4 | let TransactionOutput = app.model.define('transaction_output', {
5 | transactionId: {
6 | type: BIGINT.UNSIGNED,
7 | primaryKey: true
8 | },
9 | outputIndex: {
10 | type: INTEGER.UNSIGNED,
11 | primaryKey: true
12 | },
13 | scriptPubKey: {
14 | type: BLOB('medium'),
15 | field: 'scriptpubkey'
16 | },
17 | blockHeight: INTEGER.UNSIGNED,
18 | value: {
19 | type: BIGINT,
20 | get() {
21 | let value = this.getDataValue('value')
22 | return value == null ? null : BigInt(value)
23 | },
24 | set(value) {
25 | this.setDataValue('value', value.toString())
26 | }
27 | },
28 | addressId: BIGINT.UNSIGNED,
29 | isStake: BOOLEAN,
30 | inputId: BIGINT.UNSIGNED,
31 | inputIndex: INTEGER.UNSIGNED,
32 | inputHeight: {
33 | type: INTEGER.UNSIGNED,
34 | allowNull: true
35 | }
36 | }, {freezeTableName: true, underscored: true, timestamps: false})
37 |
38 | TransactionOutput.associate = () => {
39 | const {Address, Transaction, TransactionInput} = app.model
40 | Transaction.hasMany(TransactionOutput, {as: 'outputs', foreignKey: 'transactionId'})
41 | TransactionOutput.belongsTo(Transaction, {as: 'outputTransaction', foreignKey: 'transactionId'})
42 | TransactionOutput.belongsTo(Transaction, {as: 'inputTransaction', foreignKey: 'inputId'})
43 | TransactionOutput.belongsTo(TransactionInput, {as: 'input', foreignKey: 'inputId'})
44 | Address.hasMany(TransactionOutput, {as: 'outputTxos', foreignKey: 'addressId'})
45 | TransactionOutput.belongsTo(Address, {as: 'address', foreignKey: 'addressId'})
46 | }
47 |
48 | return TransactionOutput
49 | }
50 |
--------------------------------------------------------------------------------
/app/middleware/block-filter.js:
--------------------------------------------------------------------------------
1 | module.exports = () => async function pagination(ctx, next) {
2 | const {Header} = ctx.model
3 | const {gte: $gte, lte: $lte} = ctx.app.Sequelize.Op
4 |
5 | if (!['GET', 'POST'].includes(ctx.method)) {
6 | return await next()
7 | }
8 | let fromBlock = 1
9 | let toBlock = null
10 | let object = {GET: ctx.query, POST: ctx.request.body}[ctx.method]
11 | if ('fromBlock' in object) {
12 | let height = Number.parseInt(object.fromBlock)
13 | ctx.assert(height >= 0 && height <= 0xffffffff, 400)
14 | if (height > fromBlock) {
15 | fromBlock = height
16 | }
17 | }
18 | if ('toBlock' in object) {
19 | let height = Number.parseInt(object.toBlock)
20 | ctx.assert(height >= 0 && height <= 0xffffffff, 400)
21 | if (toBlock == null || height < toBlock) {
22 | toBlock = height
23 | }
24 | }
25 | if ('fromTime' in object) {
26 | let timestamp = Math.floor(Date.parse(object.fromTime) / 1000)
27 | ctx.assert(timestamp >= 0 && timestamp <= 0xffffffff, 400)
28 | let header = await Header.findOne({
29 | where: {timestamp: {[$gte]: timestamp}},
30 | attributes: ['height'],
31 | order: [['timestamp', 'ASC']],
32 | transaction: ctx.state.transaction
33 | })
34 | if (header && header.height > fromBlock) {
35 | fromBlock = header.height
36 | }
37 | }
38 | if ('toTime' in object) {
39 | let timestamp = Math.floor(Date.parse(object.toTime) / 1000)
40 | ctx.assert(timestamp >= 0 && timestamp <= 0xffffffff, 400)
41 | let header = await Header.findOne({
42 | where: {timestamp: {[$lte]: timestamp}},
43 | attributes: ['height'],
44 | order: [['timestamp', 'DESC']],
45 | transaction: ctx.state.transaction
46 | })
47 | if (header && (toBlock == null || header.height < toBlock)) {
48 | toBlock = header.height
49 | }
50 | }
51 | ctx.state.fromBlock = fromBlock
52 | ctx.state.toBlock = toBlock
53 | await next()
54 | }
55 |
--------------------------------------------------------------------------------
/app/model/header.js:
--------------------------------------------------------------------------------
1 | module.exports = app => {
2 | const {INTEGER, CHAR, BLOB} = app.Sequelize
3 |
4 | let Header = app.model.define('header', {
5 | hash: {
6 | type: CHAR(32).BINARY,
7 | unique: true
8 | },
9 | height: {
10 | type: INTEGER.UNSIGNED,
11 | primaryKey: true
12 | },
13 | version: INTEGER,
14 | prevHash: {
15 | type: CHAR(32).BINARY,
16 | defaultValue: Buffer.alloc(32)
17 | },
18 | merkleRoot: CHAR(32).BINARY,
19 | timestamp: INTEGER.UNSIGNED,
20 | bits: INTEGER.UNSIGNED,
21 | nonce: INTEGER.UNSIGNED,
22 | hashStateRoot: CHAR(32).BINARY,
23 | hashUTXORoot: {type: CHAR(32).BINARY, field: 'hash_utxo_root'},
24 | stakePrevTxId: {type: CHAR(32).BINARY, field: 'stake_prev_transaction_id'},
25 | stakeOutputIndex: INTEGER.UNSIGNED,
26 | signature: BLOB,
27 | chainwork: {
28 | type: CHAR(32).BINARY,
29 | get() {
30 | let chainwork = this.getDataValue('chainwork')
31 | return chainwork == null ? null : BigInt(`0x${chainwork.toString('hex')}`)
32 | },
33 | set(chainwork) {
34 | this.setDataValue(
35 | 'chainwork',
36 | Buffer.from(chainwork.toString(16).padStart(64, '0'), 'hex')
37 | )
38 | }
39 | }
40 | }, {
41 | freezeTableName: true, underscored: true, timestamps: false,
42 | getterMethods: {
43 | difficulty() {
44 | function getTargetDifficulty(bits) {
45 | return (bits & 0xffffff) * 2 ** ((bits >>> 24) - 3 << 3)
46 | }
47 | return getTargetDifficulty(0x1d00ffff) / getTargetDifficulty(this.bits)
48 | }
49 | }
50 | })
51 |
52 | Header.prototype.isProofOfStake = function isProofOfStake() {
53 | return Buffer.compare(this.stakePrevTxId, Buffer.alloc(32)) !== 0 && this.stakeOutputIndex !== 0xffffffff
54 | }
55 |
56 | Header.associate = () => {
57 | Header.hasOne(Header, {as: 'prevHeader', foreignKey: 'height'})
58 | Header.hasOne(Header, {as: 'nextHeader', foreignKey: 'height'})
59 | }
60 |
61 | return Header
62 | }
63 |
--------------------------------------------------------------------------------
/app/model/qrc20.js:
--------------------------------------------------------------------------------
1 | module.exports = app => {
2 | const {INTEGER, CHAR, BLOB} = app.Sequelize
3 |
4 | let QRC20 = app.model.define('qrc20', {
5 | contractAddress: {
6 | type: CHAR(20).BINARY,
7 | primaryKey: true
8 | },
9 | name: {
10 | type: BLOB,
11 | get() {
12 | let name = this.getDataValue('name')
13 | return name == null ? null : name.toString()
14 | },
15 | set(name) {
16 | this.setDataValue('name', Buffer.from(name))
17 | }
18 | },
19 | symbol: {
20 | type: BLOB,
21 | get() {
22 | let symbol = this.getDataValue('symbol')
23 | return symbol == null ? null : symbol.toString()
24 | },
25 | set(symbol) {
26 | this.setDataValue('symbol', Buffer.from(symbol))
27 | }
28 | },
29 | decimals: INTEGER(3).UNSIGNED,
30 | totalSupply: {
31 | type: CHAR(32).BINARY,
32 | get() {
33 | let totalSupply = this.getDataValue('totalSupply')
34 | return totalSupply == null ? null : BigInt(`0x${totalSupply.toString('hex')}`)
35 | },
36 | set(totalSupply) {
37 | this.setDataValue(
38 | 'totalSupply',
39 | Buffer.from(totalSupply.toString(16).padStart(64, '0'), 'hex')
40 | )
41 | }
42 | },
43 | version: {
44 | type: BLOB,
45 | allowNull: true,
46 | get() {
47 | let version = this.getDataValue('version')
48 | return version == null ? null : version.toString()
49 | },
50 | set(version) {
51 | this.setDataValue('version', Buffer.from(version))
52 | }
53 | }
54 | }, {freezeTableName: true, underscored: true, timestamps: false})
55 |
56 | QRC20.associate = () => {
57 | const {EvmReceiptLog: EVMReceiptLog, Contract} = app.model
58 | EVMReceiptLog.belongsTo(QRC20, {as: 'qrc20', foreignKey: 'address', sourceKey: 'contractAddress'})
59 | QRC20.hasMany(EVMReceiptLog, {as: 'logs', foreignKey: 'address', sourceKey: 'contractAddress'})
60 | Contract.hasOne(QRC20, {as: 'qrc20', foreignKey: 'contractAddress'})
61 | QRC20.belongsTo(Contract, {as: 'contract', foreignKey: 'contractAddress'})
62 | }
63 |
64 | return QRC20
65 | }
66 |
--------------------------------------------------------------------------------
/agent.js:
--------------------------------------------------------------------------------
1 | const SocketClient = require('socket.io-client')
2 |
3 | module.exports = function(agent) {
4 | let tip = null
5 |
6 | agent.messenger.on('egg-ready', () => {
7 | let io = SocketClient(`http://localhost:${agent.config.qtuminfo.port}`)
8 | io.on('tip', newTip => {
9 | tip = newTip
10 | agent.messenger.sendToApp('block-tip', tip)
11 | agent.messenger.sendRandom('socket/block-tip', tip)
12 | })
13 | io.on('block', block => {
14 | tip = block
15 | agent.messenger.sendToApp('new-block', block)
16 | agent.messenger.sendRandom('update-stakeweight')
17 | agent.messenger.sendRandom('update-dgpinfo')
18 | agent.messenger.sendRandom('socket/block-tip', block)
19 | })
20 | io.on('reorg', block => {
21 | tip = block
22 | agent.messenger.sendToApp('reorg-to-block', block)
23 | agent.messenger.sendRandom('socket/reorg/block-tip', block)
24 | })
25 | io.on('mempool-transaction', id => {
26 | if (id) {
27 | agent.messenger.sendRandom('socket/mempool-transaction', id)
28 | }
29 | })
30 | })
31 |
32 | let lastTipHash = Buffer.alloc(0)
33 | function updateStatistics() {
34 | if (tip && Buffer.compare(lastTipHash, tip.hash) !== 0) {
35 | agent.messenger.sendRandom('update-richlist')
36 | agent.messenger.sendRandom('update-qrc20-statistics')
37 | agent.messenger.sendRandom('update-daily-transactions')
38 | agent.messenger.sendRandom('update-block-interval')
39 | agent.messenger.sendRandom('update-address-growth')
40 | lastTipHash = tip.hash
41 | }
42 | }
43 |
44 | setInterval(updateStatistics, 2 * 60 * 1000).unref()
45 |
46 | agent.messenger.on('blockchain-info', () => {
47 | agent.messenger.sendToApp('blockchain-info', {tip})
48 | })
49 |
50 | agent.messenger.on('egg-ready', () => {
51 | let interval = setInterval(() => {
52 | if (tip) {
53 | agent.messenger.sendToApp('blockchain-info', {tip})
54 | clearInterval(interval)
55 | updateStatistics()
56 | }
57 | }, 0)
58 | agent.messenger.sendRandom('update-stakeweight')
59 | agent.messenger.sendRandom('update-feerate')
60 | agent.messenger.sendRandom('update-dgpinfo')
61 | })
62 | }
63 |
--------------------------------------------------------------------------------
/app/model/evm-receipt.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 | const addressTypes = {
3 | pubkeyhash: 1,
4 | scripthash: 2,
5 | witness_v0_keyhash: 3,
6 | witness_v0_scripthash: 4,
7 | contract: 0x80,
8 | evm_contract: 0x81,
9 | x86_contract: 0x82
10 | }
11 | /* eslint-enable camelcase*/
12 | const addressTypeMap = {
13 | 1: 'pubkeyhash',
14 | 2: 'scripthash',
15 | 3: 'witness_v0_keyhash',
16 | 4: 'witness_v0_scripthash',
17 | 0x80: 'contract',
18 | 0x81: 'evm_contract',
19 | 0x82: 'x86_contract'
20 | }
21 |
22 | module.exports = app => {
23 | const {INTEGER, BIGINT, CHAR, STRING, TEXT} = app.Sequelize
24 |
25 | let EVMReceipt = app.model.define('evm_receipt', {
26 | _id: {
27 | type: BIGINT.UNSIGNED,
28 | field: '_id',
29 | primaryKey: true,
30 | autoIncrement: true
31 | },
32 | transactionId: {
33 | type: BIGINT.UNSIGNED,
34 | unique: 'transaction'
35 | },
36 | outputIndex: {
37 | type: INTEGER.UNSIGNED,
38 | unique: 'transaction'
39 | },
40 | blockHeight: INTEGER.UNSIGNED,
41 | indexInBlock: INTEGER.UNSIGNED,
42 | senderType: {
43 | type: INTEGER(3).UNSIGNED,
44 | get() {
45 | let senderType = this.getDataValue('senderType')
46 | return addressTypeMap[senderType] || null
47 | },
48 | set(senderType) {
49 | if (senderType != null) {
50 | this.setDataValue('senderType', addressTypes[senderType] || 0)
51 | }
52 | }
53 | },
54 | senderData: STRING(32).BINARY,
55 | gasUsed: INTEGER.UNSIGNED,
56 | contractAddress: CHAR(20).BINARY,
57 | excepted: STRING(32),
58 | exceptedMessage: TEXT
59 | }, {freezeTableName: true, underscored: true, timestamps: false})
60 |
61 | EVMReceipt.associate = () => {
62 | const {Header, Transaction, TransactionOutput} = app.model
63 | EVMReceipt.belongsTo(Header, {as: 'header', foreignKey: 'blockHeight'})
64 | Transaction.hasMany(EVMReceipt, {as: 'evmReceipts', foreignKey: 'transactionId'})
65 | EVMReceipt.belongsTo(Transaction, {as: 'transaction', foreignKey: 'transactionId'})
66 | TransactionOutput.hasOne(EVMReceipt, {as: 'evmReceipt', foreignKey: 'transactionId'})
67 | EVMReceipt.belongsTo(TransactionOutput, {as: 'output', foreignKey: 'transactionId'})
68 | }
69 |
70 | return EVMReceipt
71 | }
72 |
--------------------------------------------------------------------------------
/app/service/statistics.js:
--------------------------------------------------------------------------------
1 | const {Service} = require('egg')
2 |
3 | class StatisticsService extends Service {
4 | async getDailyTransactions() {
5 | const db = this.ctx.model
6 | const {sql} = this.ctx.helper
7 | let result = await db.query(sql`
8 | SELECT
9 | FLOOR(header.timestamp / 86400) AS date,
10 | SUM(block.transactions_count) AS transactionsCount,
11 | SUM(block.contract_transactions_count) AS contractTransactionsCount
12 | FROM header, block
13 | WHERE header.height = block.height
14 | GROUP BY date
15 | ORDER BY date ASC
16 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction})
17 | return result.map(({date, transactionsCount, contractTransactionsCount}) => ({
18 | timestamp: date * 86400,
19 | transactionsCount,
20 | contractTransactionsCount
21 | }))
22 | }
23 |
24 | async getBlockIntervalStatistics() {
25 | const db = this.ctx.model
26 | const {sql} = this.ctx.helper
27 | let result = await db.query(sql`
28 | SELECT header.timestamp - prev_header.timestamp AS blockInterval, COUNT(*) AS count FROM header
29 | INNER JOIN header prev_header ON prev_header.height = header.height - 1
30 | WHERE header.height > 5001
31 | GROUP BY blockInterval
32 | ORDER BY blockInterval ASC
33 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction})
34 | let total = this.app.blockchainInfo.tip.height - 5001
35 | return result.map(({blockInterval, count}) => ({interval: blockInterval, count, percentage: count / total}))
36 | }
37 |
38 | async getAddressGrowth() {
39 | const db = this.ctx.model
40 | const {Address} = db
41 | const {sql} = this.ctx.helper
42 | let result = await db.query(sql`
43 | SELECT FLOOR(header.timestamp / 86400) AS date, COUNT(*) AS count FROM address, header
44 | WHERE address.create_height = header.height AND address.type < ${Address.parseType('contract')}
45 | GROUP BY date
46 | ORDER BY date ASC
47 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction})
48 | let sum = 0
49 | return result.map(({date, count}) => {
50 | sum += count
51 | return {
52 | timestamp: date * 86400,
53 | count: sum
54 | }
55 | })
56 | }
57 | }
58 |
59 | module.exports = StatisticsService
60 |
--------------------------------------------------------------------------------
/app/service/qrc721.js:
--------------------------------------------------------------------------------
1 | const {Service} = require('egg')
2 |
3 | class QRC721Service extends Service {
4 | async listQRC721Tokens() {
5 | const db = this.ctx.model
6 | const {sql} = this.ctx.helper
7 | let {limit, offset} = this.ctx.state.pagination
8 |
9 | let [{totalCount}] = await db.query(sql`
10 | SELECT COUNT(DISTINCT(qrc721_token.contract_address)) AS count FROM qrc721_token
11 | INNER JOIN qrc721 USING (contract_address)
12 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction})
13 | let list = await db.query(sql`
14 | SELECT
15 | contract.address_string AS address, contract.address AS addressHex,
16 | qrc721.name AS name, qrc721.symbol AS symbol, qrc721.total_supply AS totalSupply,
17 | list.holders AS holders
18 | FROM (
19 | SELECT contract_address, COUNT(*) AS holders FROM qrc721_token
20 | INNER JOIN qrc721 USING (contract_address)
21 | GROUP BY contract_address
22 | ORDER BY holders DESC
23 | LIMIT ${offset}, ${limit}
24 | ) list
25 | INNER JOIN qrc721 USING (contract_address)
26 | INNER JOIN contract ON contract.address = list.contract_address
27 | ORDER BY holders DESC
28 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction})
29 |
30 | return {
31 | totalCount,
32 | tokens: list.map(item => ({
33 | address: item.addressHex.toString('hex'),
34 | addressHex: item.addressHex,
35 | name: item.name.toString(),
36 | symbol: item.symbol.toString(),
37 | totalSupply: BigInt(`0x${item.totalSupply.toString('hex')}`),
38 | holders: item.holders
39 | }))
40 | }
41 | }
42 |
43 | async getAllQRC721Balances(hexAddresses) {
44 | if (hexAddresses.length === 0) {
45 | return []
46 | }
47 | const db = this.ctx.model
48 | const {sql} = this.ctx.helper
49 | let list = await db.query(sql`
50 | SELECT
51 | contract.address AS addressHex, contract.address_string AS address,
52 | qrc721.name AS name,
53 | qrc721.symbol AS symbol,
54 | qrc721_token.count AS count
55 | FROM (
56 | SELECT contract_address, COUNT(*) AS count FROM qrc721_token
57 | WHERE holder IN ${hexAddresses}
58 | GROUP BY contract_address
59 | ) qrc721_token
60 | INNER JOIN contract ON contract.address = qrc721_token.contract_address
61 | INNER JOIN qrc721 ON qrc721.contract_address = qrc721_token.contract_address
62 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction})
63 | return list.map(item => ({
64 | address: item.addressHex.toString('hex'),
65 | addressHex: item.addressHex,
66 | name: item.name.toString(),
67 | symbol: item.symbol.toString(),
68 | count: item.count
69 | }))
70 | }
71 | }
72 |
73 | module.exports = QRC721Service
74 |
--------------------------------------------------------------------------------
/app/controller/transaction.js:
--------------------------------------------------------------------------------
1 | const {Controller} = require('egg')
2 |
3 | class TransactionController extends Controller {
4 | async transaction() {
5 | const {ctx} = this
6 | ctx.assert(ctx.params.id && /^[0-9a-f]{64}$/i.test(ctx.params.id), 404)
7 | let brief = 'brief' in ctx.query
8 | let id = Buffer.from(ctx.params.id, 'hex')
9 | let transaction = await ctx.service.transaction.getTransaction(id)
10 | ctx.assert(transaction, 404)
11 | ctx.body = await ctx.service.transaction.transformTransaction(transaction, {brief})
12 | }
13 |
14 | async transactions() {
15 | const {ctx} = this
16 | ctx.assert(ctx.params.ids, 404)
17 | let ids = ctx.params.ids.split(',')
18 | ctx.assert(ids.length <= 100 && ids.every(id => /^[0-9a-f]{64}$/i.test(id)), 404)
19 | let brief = 'brief' in ctx.query
20 | let transactions = await Promise.all(ids.map(
21 | id => ctx.service.transaction.getTransaction(Buffer.from(id, 'hex'))
22 | ))
23 | ctx.assert(transactions.every(Boolean), 404)
24 | ctx.body = await Promise.all(transactions.map(
25 | tx => ctx.service.transaction.transformTransaction(tx, {brief})
26 | ))
27 | }
28 |
29 | async rawTransaction() {
30 | const {ctx} = this
31 | ctx.assert(/^[0-9a-f]{64}$/.test(ctx.params.id), 404)
32 | let id = Buffer.from(ctx.params.id, 'hex')
33 | let transaction = await ctx.service.transaction.getRawTransaction(id)
34 | ctx.assert(transaction, 404)
35 | ctx.body = transaction.toBuffer().toString('hex')
36 | }
37 |
38 | async recent() {
39 | const {ctx} = this
40 | let count = Number.parseInt(ctx.query.count || 10)
41 | let ids = await ctx.service.transaction.getRecentTransactions(count)
42 | let transactions = await Promise.all(ids.map(
43 | id => ctx.service.transaction.getTransaction(Buffer.from(id, 'hex'))
44 | ))
45 | ctx.body = await Promise.all(transactions.map(
46 | tx => ctx.service.transaction.transformTransaction(tx, {brief: true})
47 | ))
48 | }
49 |
50 | async list() {
51 | const {ctx} = this
52 | let {totalCount, ids} = await ctx.service.transaction.getAllTransactions()
53 | let transactions = await Promise.all(ids.map(id => ctx.service.transaction.getTransaction(id)))
54 | ctx.body = {
55 | totalCount,
56 | transactions: await Promise.all(transactions.map(tx => ctx.service.transaction.transformTransaction(tx)))
57 | }
58 | }
59 |
60 | async send() {
61 | const {ctx} = this
62 | let {rawtx: data} = ctx.request.body
63 | if (!/^([0-9a-f][0-9a-f])+$/i.test(data)) {
64 | ctx.body = {status: 1, message: 'TX decode failed'}
65 | }
66 | try {
67 | let id = await ctx.service.transaction.sendRawTransaction(Buffer.from(data, 'hex'))
68 | ctx.body = {status: 0, id: id.toString('hex'), txid: id.toString('hex')}
69 | } catch (err) {
70 | ctx.body = {status: 1, message: err.message}
71 | }
72 | }
73 | }
74 |
75 | module.exports = TransactionController
76 |
--------------------------------------------------------------------------------
/app/controller/qrc20.js:
--------------------------------------------------------------------------------
1 | const {Controller} = require('egg')
2 |
3 | class QRC20Controller extends Controller {
4 | async list() {
5 | const {ctx} = this
6 | let {totalCount, tokens} = await ctx.service.qrc20.listQRC20Tokens()
7 | ctx.body = {
8 | totalCount,
9 | tokens: tokens.map(item => ({
10 | address: item.addressHex.toString('hex'),
11 | addressHex: item.addressHex.toString('hex'),
12 | name: item.name,
13 | symbol: item.symbol,
14 | decimals: item.decimals,
15 | totalSupply: item.totalSupply.toString(),
16 | version: item.version,
17 | holders: item.holders,
18 | transactions: item.transactions
19 | }))
20 | }
21 | }
22 |
23 | async allTransactions() {
24 | const {ctx} = this
25 | let {totalCount, transactions} = await ctx.service.qrc20.getAllQRC20TokenTransactions()
26 | ctx.body = {
27 | totalCount,
28 | transactions: transactions.map(transaction => ({
29 | transactionId: transaction.transactionId.toString('hex'),
30 | outputIndex: transaction.outputIndex,
31 | blockHeight: transaction.blockHeight,
32 | blockHash: transaction.blockHash.toString('hex'),
33 | timestamp: transaction.timestamp,
34 | token: {
35 | name: transaction.token.name,
36 | symbol: transaction.token.symbol,
37 | decimals: transaction.token.decimals
38 | },
39 | from: transaction.from,
40 | fromHex: transaction.fromHex && transaction.fromHex.toString('hex'),
41 | to: transaction.to,
42 | toHex: transaction.toHex && transaction.toHex.toString('hex'),
43 | value: transaction.value.toString()
44 | }))
45 | }
46 | }
47 |
48 | async transactions() {
49 | const {ctx} = this
50 | ctx.assert(ctx.state.token.type === 'qrc20', 404)
51 | let {totalCount, transactions} = await ctx.service.qrc20.getQRC20TokenTransactions(ctx.state.token.contractAddress)
52 | ctx.body = {
53 | totalCount,
54 | transactions: transactions.map(transaction => ({
55 | transactionId: transaction.transactionId.toString('hex'),
56 | outputIndex: transaction.outputIndex,
57 | blockHeight: transaction.blockHeight,
58 | blockHash: transaction.blockHash.toString('hex'),
59 | timestamp: transaction.timestamp,
60 | from: transaction.from,
61 | fromHex: transaction.fromHex && transaction.fromHex.toString('hex'),
62 | to: transaction.to,
63 | toHex: transaction.toHex && transaction.toHex.toString('hex'),
64 | value: transaction.value.toString()
65 | }))
66 | }
67 | }
68 |
69 | async richList() {
70 | const {ctx} = this
71 | ctx.assert(ctx.state.token.type === 'qrc20', 404)
72 | let {totalCount, list} = await ctx.service.qrc20.getQRC20TokenRichList(ctx.state.token.contractAddress)
73 | ctx.body = {
74 | totalCount,
75 | list: list.map(item => ({
76 | address: item.address,
77 | addressHex: item.addressHex,
78 | balance: item.balance.toString()
79 | }))
80 | }
81 | }
82 | }
83 |
84 | module.exports = QRC20Controller
85 |
--------------------------------------------------------------------------------
/app/service/info.js:
--------------------------------------------------------------------------------
1 | const {Service} = require('egg')
2 |
3 | class InfoService extends Service {
4 | async getInfo() {
5 | let height = this.app.blockchainInfo.tip.height
6 | let stakeWeight = JSON.parse(await this.app.redis.hget(this.app.name, 'stakeweight')) || 0
7 | let feeRate = JSON.parse(await this.app.redis.hget(this.app.name, 'feerate')).find(item => item.blocks === 10).feeRate || 0.004
8 | let dgpInfo = JSON.parse(await this.app.redis.hget(this.app.name, 'dgpinfo')) || {}
9 | return {
10 | height,
11 | supply: this.getTotalSupply(),
12 | ...this.app.chain.name === 'mainnet' ? {circulatingSupply: this.getCirculatingSupply()} : {},
13 | netStakeWeight: Math.round(stakeWeight),
14 | feeRate,
15 | dgpInfo
16 | }
17 | }
18 |
19 | getTotalSupply() {
20 | let height = this.app.blockchainInfo.tip.height
21 | if (height <= this.app.chain.lastPoWBlockHeight) {
22 | return height * 20000
23 | } else {
24 | let supply = 1e8
25 | let reward = 4
26 | let interval = 985500
27 | let stakeHeight = height - this.app.chain.lastPoWBlockHeight
28 | let halvings = 0
29 | while (halvings < 7 && stakeHeight > interval) {
30 | supply += interval * reward / (1 << halvings++)
31 | stakeHeight -= interval
32 | }
33 | supply += stakeHeight * reward / (1 << halvings)
34 | return supply
35 | }
36 | }
37 |
38 | getTotalMaxSupply() {
39 | return 1e8 + 985500 * 4 * (1 - 1 / 2 ** 7) / (1 - 1 / 2)
40 | }
41 |
42 | getCirculatingSupply() {
43 | let height = this.app.blockchainInfo.tip.height
44 | let totalSupply = this.getTotalSupply(height)
45 | if (this.app.chain.name === 'mainnet') {
46 | return totalSupply - 575e4
47 | } else {
48 | return totalSupply
49 | }
50 | }
51 |
52 | async getStakeWeight() {
53 | const {Header} = this.ctx.model
54 | const {gte: $gte} = this.app.Sequelize.Op
55 | let height = await Header.aggregate('height', 'max', {transaction: this.ctx.state.transaction})
56 | let list = await Header.findAll({
57 | where: {height: {[$gte]: height - 500}},
58 | attributes: ['timestamp', 'bits'],
59 | order: [['height', 'ASC']],
60 | transaction: this.ctx.state.transaction
61 | })
62 | let interval = list[list.length - 1].timestamp - list[0].timestamp
63 | let sum = list.slice(1)
64 | .map(x => x.difficulty)
65 | .reduce((x, y) => x + y)
66 | return sum * 2 ** 32 * 16 / interval
67 | }
68 |
69 | async getFeeRates() {
70 | let client = new this.app.qtuminfo.rpc(this.app.config.qtuminfo.rpc)
71 | let results = await Promise.all([2, 4, 6, 10, 12, 24].map(blocks => client.estimatesmartfee(blocks)))
72 | return [
73 | {blocks: 2, feeRate: results[0].feerate || 0.004},
74 | {blocks: 4, feeRate: results[1].feerate || 0.004},
75 | {blocks: 6, feeRate: results[2].feerate || 0.004},
76 | {blocks: 10, feeRate: results[3].feerate || 0.004},
77 | {blocks: 12, feeRate: results[4].feerate || 0.004},
78 | {blocks: 24, feeRate: results[5].feerate || 0.004}
79 | ]
80 | }
81 |
82 | async getDGPInfo() {
83 | let client = new this.app.qtuminfo.rpc(this.app.config.qtuminfo.rpc)
84 | let info = await client.getdgpinfo()
85 | return {
86 | maxBlockSize: info.maxblocksize,
87 | minGasPrice: info.mingasprice,
88 | blockGasLimit: info.blockgaslimit
89 | }
90 | }
91 | }
92 |
93 | module.exports = InfoService
94 |
--------------------------------------------------------------------------------
/app.js:
--------------------------------------------------------------------------------
1 | module.exports = app => {
2 | app.blockchainInfo = {
3 | tip: null
4 | }
5 | const namespace = app.io.of('/')
6 |
7 | app.messenger.on('egg-ready', () => {
8 | app.messenger.sendToAgent('blockchain-info')
9 | })
10 |
11 | app.messenger.on('update-richlist', async () => {
12 | let ctx = app.createAnonymousContext()
13 | await ctx.service.balance.updateRichList()
14 | })
15 |
16 | app.messenger.on('update-qrc20-statistics', async () => {
17 | let ctx = app.createAnonymousContext()
18 | await ctx.service.qrc20.updateQRC20Statistics()
19 | })
20 |
21 | app.messenger.on('update-daily-transactions', async () => {
22 | let ctx = app.createAnonymousContext()
23 | let dailyTransactions = await ctx.service.statistics.getDailyTransactions()
24 | await app.redis.hset(app.name, 'daily-transactions', JSON.stringify(dailyTransactions))
25 | })
26 |
27 | app.messenger.on('update-block-interval', async () => {
28 | let ctx = app.createAnonymousContext()
29 | let blockInterval = await ctx.service.statistics.getBlockIntervalStatistics()
30 | await app.redis.hset(app.name, 'block-interval', JSON.stringify(blockInterval))
31 | })
32 |
33 | app.messenger.on('update-address-growth', async () => {
34 | let ctx = app.createAnonymousContext()
35 | let addressGrowth = await ctx.service.statistics.getAddressGrowth()
36 | await app.redis.hset(app.name, 'address-growth', JSON.stringify(addressGrowth))
37 | })
38 |
39 | app.messenger.on('update-stakeweight', async () => {
40 | let ctx = app.createAnonymousContext()
41 | let stakeWeight = await ctx.service.info.getStakeWeight()
42 | await app.redis.hset(app.name, 'stakeweight', JSON.stringify(stakeWeight))
43 | namespace.to('blockchain').emit('stakeweight', stakeWeight)
44 | })
45 |
46 | app.messenger.on('update-feerate', async () => {
47 | await app.runSchedule('update-feerate')
48 | })
49 |
50 | app.messenger.on('update-dgpinfo', async () => {
51 | let ctx = app.createAnonymousContext()
52 | let dgpInfo = await ctx.service.info.getDGPInfo()
53 | await app.redis.hset(app.name, 'dgpinfo', JSON.stringify(dgpInfo))
54 | namespace.to('blockchain').emit('dgpinfo', dgpInfo)
55 | })
56 |
57 | app.messenger.on('blockchain-info', info => {
58 | app.blockchainInfo = info
59 | })
60 | app.messenger.on('block-tip', tip => {
61 | app.blockchainInfo.tip = tip
62 | })
63 | app.messenger.on('new-block', block => {
64 | app.blockchainInfo.tip = block
65 | })
66 | app.messenger.on('reorg-to-block', block => {
67 | app.blockchainInfo.tip = block
68 | })
69 |
70 | app.messenger.on('socket/block-tip', async tip => {
71 | app.blockchainInfo.tip = tip
72 | namespace.emit('tip', tip)
73 | let ctx = app.createAnonymousContext()
74 | let transactions = (await ctx.service.block.getBlockTransactions(tip.height)).map(id => id.toString('hex'))
75 | for (let id of transactions) {
76 | namespace.to(`transaction/${id}`).emit('transaction/confirm', id)
77 | }
78 | let list = await ctx.service.block.getBlockAddressTransactions(tip.height)
79 | for (let i = 0; i < transactions.length; ++i) {
80 | for (let address of list[i] || []) {
81 | namespace.to(`address/${address}`).emit('address/transaction', {address, id: transactions[i]})
82 | }
83 | }
84 | })
85 |
86 | app.messenger.on('socket/reorg/block-tip', tip => {
87 | app.blockchainInfo.tip = tip
88 | namespace.emit('reorg', tip)
89 | })
90 |
91 | app.messenger.on('socket/mempool-transaction', async id => {
92 | id = Buffer.from(id)
93 | let ctx = app.createAnonymousContext()
94 | let transaction = await ctx.service.transaction.getTransaction(id)
95 | if (!transaction) {
96 | return
97 | }
98 | namespace.to('mempool').emit('mempool/transaction', await ctx.service.transaction.transformTransaction(transaction, {brief: true}))
99 | let addresses = await ctx.service.transaction.getMempoolTransactionAddresses(id)
100 | for (let address of addresses) {
101 | namespace.to(`address/${address}`).emit('address/transaction', {address, id: id.toString('hex')})
102 | }
103 | })
104 | }
105 |
--------------------------------------------------------------------------------
/app/service/misc.js:
--------------------------------------------------------------------------------
1 | const {Service} = require('egg')
2 |
3 | class MiscService extends Service {
4 | async classify(id) {
5 | const db = this.ctx.model
6 | const {Block, Transaction, Contract, Qrc20: QRC20, where, fn, literal} = db
7 | const {or: $or, like: $like} = this.app.Sequelize.Op
8 | const {Address} = this.app.qtuminfo.lib
9 | const {sql} = this.ctx.helper
10 | const transaction = this.ctx.state.transaction
11 |
12 | if (/^(0|[1-9]\d{0,9})$/.test(id)) {
13 | let height = Number.parseInt(id)
14 | if (height <= this.app.blockchainInfo.tip.height) {
15 | return {type: 'block'}
16 | }
17 | }
18 | if (/^[0-9a-f]{64}$/i.test(id)) {
19 | if (await Block.findOne({
20 | where: {hash: Buffer.from(id, 'hex')},
21 | attributes: ['height']
22 | })) {
23 | return {type: 'block'}
24 | } else if (await Transaction.findOne({
25 | where: {id: Buffer.from(id, 'hex')},
26 | attributes: ['_id'],
27 | transaction
28 | })) {
29 | return {type: 'transaction'}
30 | }
31 | }
32 |
33 | try {
34 | let address = Address.fromString(id, this.app.chain)
35 | if ([Address.CONTRACT, Address.EVM_CONTRACT].includes(address.type)) {
36 | let contract = await Contract.findOne({
37 | where: {address: address.data},
38 | attributes: ['address'],
39 | transaction
40 | })
41 | if (contract) {
42 | return {type: 'contract'}
43 | }
44 | } else {
45 | return {type: 'address'}
46 | }
47 | } catch (err) {}
48 |
49 | let qrc20Results = (await QRC20.findAll({
50 | where: {
51 | [$or]: [
52 | where(fn('LOWER', fn('CONVERT', literal('name USING utf8mb4'))), id.toLowerCase()),
53 | where(fn('LOWER', fn('CONVERT', literal('symbol USING utf8mb4'))), id.toLowerCase())
54 | ]
55 | },
56 | attributes: ['contractAddress'],
57 | transaction
58 | })).map(qrc20 => qrc20.contractAddress)
59 | if (qrc20Results.length === 0) {
60 | qrc20Results = (await QRC20.findAll({
61 | where: {
62 | [$or]: [
63 | where(fn('LOWER', fn('CONVERT', literal('name USING utf8mb4'))), {[$like]: ['', ...id.toLowerCase(), ''].join('%')}),
64 | where(fn('LOWER', fn('CONVERT', literal('name USING utf8mb4'))), {[$like]: `%${id.toLowerCase()}%`}),
65 | where(fn('LOWER', fn('CONVERT', literal('symbol USING utf8mb4'))), {[$like]: ['', ...id.toLowerCase(), ''].join('%')}),
66 | where(fn('LOWER', fn('CONVERT', literal('symbol USING utf8mb4'))), {[$like]: `%${id.toLowerCase()}%`})
67 | ]
68 | },
69 | attributes: ['contractAddress'],
70 | transaction
71 | })).map(qrc20 => qrc20.contractAddress)
72 | }
73 | if (qrc20Results.length) {
74 | let [{addressHex}] = await db.query(sql`
75 | SELECT contract.address_string AS address, contract.address AS addressHex FROM (
76 | SELECT contract_address FROM qrc20_statistics
77 | WHERE contract_address IN ${qrc20Results}
78 | ORDER BY transactions DESC LIMIT 1
79 | ) qrc20_balance
80 | INNER JOIN contract ON contract.address = qrc20_balance.contract_address
81 | `, {type: db.QueryTypes.SELECT, transaction})
82 | return {type: 'contract', address: addressHex.toString('hex'), addressHex: addressHex.toString('hex')}
83 | }
84 |
85 | return {}
86 | }
87 |
88 | async getPrices() {
89 | let apiKey = this.app.config.cmcAPIKey
90 | if (!apiKey) {
91 | return {}
92 | }
93 | const coinId = 1684
94 | let [USDResult, CNYResult] = await Promise.all([
95 | this.ctx.curl('https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest', {
96 | headers: {
97 | 'X-CMC_PRO_API_KEY': apiKey,
98 | Accept: 'application/json'
99 | },
100 | data: {
101 | id: coinId,
102 | convert: 'USD'
103 | },
104 | dataType: 'json'
105 | }),
106 | this.ctx.curl('https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest', {
107 | headers: {
108 | 'X-CMC_PRO_API_KEY': apiKey,
109 | Accept: 'application/json'
110 | },
111 | data: {
112 | id: coinId,
113 | convert: 'CNY'
114 | },
115 | dataType: 'json'
116 | })
117 | ])
118 | return {
119 | USD: USDResult.data.data[coinId].quote.USD.price,
120 | CNY: CNYResult.data.data[coinId].quote.CNY.price
121 | }
122 | }
123 | }
124 |
125 | module.exports = MiscService
126 |
--------------------------------------------------------------------------------
/app/controller/block.js:
--------------------------------------------------------------------------------
1 | const {Controller} = require('egg')
2 |
3 | class BlockController extends Controller {
4 | async block() {
5 | const {ctx} = this
6 | let arg = ctx.params.block
7 | ctx.assert(arg, 404)
8 | if (/^(0|[1-9]\d{0,9})$/.test(arg)) {
9 | arg = Number.parseInt(arg)
10 | } else if (/^[0-9a-f]{64}$/i.test(arg)) {
11 | arg = Buffer.from(arg, 'hex')
12 | } else {
13 | ctx.throw(400)
14 | }
15 | let block = await ctx.service.block.getBlock(arg)
16 | ctx.assert(block, 404)
17 | ctx.body = {
18 | hash: block.hash.toString('hex'),
19 | height: block.height,
20 | version: block.version,
21 | prevHash: block.prevHash.toString('hex'),
22 | ...block.nextHash ? {nextHash: block.nextHash.toString('hex')} : {},
23 | merkleRoot: block.merkleRoot.toString('hex'),
24 | timestamp: block.timestamp,
25 | bits: block.bits.toString(16),
26 | nonce: block.nonce,
27 | hashStateRoot: block.hashStateRoot.toString('hex'),
28 | hashUTXORoot: block.hashUTXORoot.toString('hex'),
29 | prevOutStakeHash: block.stakePrevTxId.toString('hex'),
30 | prevOutStakeN: block.stakeOutputIndex,
31 | signature: block.signature.toString('hex'),
32 | chainwork: block.chainwork.toString(16).padStart(64, '0'),
33 | flags: block.proofOfStake ? 'proof-of-stake' : 'proof-of-work',
34 | ...block.height > 0 ? {interval: block.interval} : {},
35 | size: block.size,
36 | weight: block.weight,
37 | transactions: block.transactions.map(id => id.toString('hex')),
38 | miner: block.miner,
39 | difficulty: block.difficulty,
40 | reward: block.reward.toString(),
41 | confirmations: this.app.blockchainInfo.tip.height - block.height + 1
42 | }
43 | }
44 |
45 | async rawBlock() {
46 | const {ctx} = this
47 | let arg = ctx.params.block
48 | ctx.assert(arg, 404)
49 | if (/^(0|[1-9]\d{0,9})$/.test(arg)) {
50 | arg = Number.parseInt(arg)
51 | } else if (/^[0-9a-f]{64}$/i.test(arg)) {
52 | arg = Buffer.from(arg, 'hex')
53 | } else {
54 | ctx.throw(400)
55 | }
56 | let block = await ctx.service.block.getRawBlock(arg)
57 | ctx.assert(block, 404)
58 | ctx.body = block.toBuffer().toString('hex')
59 | }
60 |
61 | async list() {
62 | const {ctx} = this
63 | let date = ctx.query.date
64 | if (!date) {
65 | let d = new Date()
66 | let yyyy = d.getUTCFullYear().toString()
67 | let mm = (d.getUTCMonth() + 1).toString()
68 | let dd = d.getUTCDate().toString()
69 | date = `${yyyy}-${mm.padStart(2, '0')}-${dd.padStart(2, '0')}`
70 | }
71 | let min = Math.floor(Date.parse(date) / 1000)
72 | let max = min + 24 * 60 * 60
73 | let {blocks} = await ctx.service.block.listBlocks({min, max})
74 | ctx.body = blocks.map(block => ({
75 | hash: block.hash.toString('hex'),
76 | height: block.height,
77 | timestamp: block.timestamp,
78 | ...block.height > 0 ? {interval: block.interval} : {},
79 | size: block.size,
80 | transactionCount: block.transactionsCount,
81 | miner: block.miner,
82 | reward: block.reward.toString()
83 | }))
84 | }
85 |
86 | async blockList() {
87 | const {ctx} = this
88 | let dateFilter = null
89 | let date = ctx.query.date
90 | if (date) {
91 | let min = Math.floor(Date.parse(date) / 1000)
92 | let max = min + 24 * 60 * 60
93 | dateFilter = {min, max}
94 | }
95 | let result = await ctx.service.block.listBlocks(dateFilter)
96 | ctx.body = {
97 | totalCount: result.totalCount,
98 | blocks: result.blocks.map(block => ({
99 | hash: block.hash.toString('hex'),
100 | height: block.height,
101 | timestamp: block.timestamp,
102 | ...block.height > 0 ? {interval: block.interval} : {},
103 | size: block.size,
104 | transactionCount: block.transactionsCount,
105 | miner: block.miner,
106 | reward: block.reward.toString()
107 | }))
108 | }
109 | }
110 |
111 | async recent() {
112 | const {ctx} = this
113 | let count = Number.parseInt(ctx.query.count || 10)
114 | let blocks = await ctx.service.block.getRecentBlocks(count)
115 | ctx.body = blocks.map(block => ({
116 | hash: block.hash.toString('hex'),
117 | height: block.height,
118 | timestamp: block.timestamp,
119 | ...block.height > 0 ? {interval: block.interval} : {},
120 | size: block.size,
121 | transactionCount: block.transactionsCount,
122 | miner: block.miner,
123 | reward: block.reward.toString()
124 | }))
125 | }
126 | }
127 |
128 | module.exports = BlockController
129 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "parserOptions": {
4 | "ecmaVersion": 2018
5 | },
6 | "env": {
7 | "es6": true,
8 | "node": true
9 | },
10 | "globals": {
11 | "BigInt": true
12 | },
13 | "extends": "eslint:recommended",
14 | "rules": {
15 | "for-direction": "error",
16 | "getter-return": "warn",
17 | "no-console": "off",
18 | "no-constant-condition": ["warn", {"checkLoops": false}],
19 | "no-empty": ["warn", {"allowEmptyCatch": true}],
20 | "no-extra-parens": "warn",
21 | "require-atomic-updates": "off",
22 | "curly": ["error", "multi-line"],
23 | "dot-notation": "warn",
24 | "eqeqeq": ["error", "smart"],
25 | "guard-for-in": "error",
26 | "no-caller": "error",
27 | "no-empty-pattern": "error",
28 | "no-eval": "error",
29 | "no-extend-native": "error",
30 | "no-extra-label": "error",
31 | "no-floating-decimal": "warn",
32 | "no-implicit-coercion": "error",
33 | "no-implied-eval": "error",
34 | "no-labels": ["error", {"allowLoop": true}],
35 | "no-lone-blocks": "warn",
36 | "no-multi-spaces": "warn",
37 | "no-multi-str": "warn",
38 | "no-new-func": "error",
39 | "no-new-wrappers": "error",
40 | "no-octal-escape": "error",
41 | "no-return-assign": "error",
42 | "no-self-compare": "error",
43 | "no-sequences": "error",
44 | "no-unmodified-loop-condition": "warn",
45 | "no-useless-call": "error",
46 | "no-useless-concat": "warn",
47 | "no-useless-return": "warn",
48 | "no-void": "error",
49 | "no-with": "error",
50 | "wrap-iife": "error",
51 | "yoda": "error",
52 | "no-label-var": "error",
53 | "no-shadow-restricted-names": "error",
54 | "no-undef-init": "error",
55 | "no-unused-vars": "warn",
56 | "no-use-before-define": ["error", "nofunc"],
57 | "no-buffer-constructor": "error",
58 | "no-mixed-requires": "warn",
59 | "no-new-require": "error",
60 | "no-path-concat": "warn",
61 | "array-bracket-newline": ["warn", "consistent"],
62 | "array-bracket-spacing": "warn",
63 | "brace-style": ["warn", "1tbs", {"allowSingleLine": true}],
64 | "camelcase": "warn",
65 | "comma-spacing": "warn",
66 | "comma-style": "warn",
67 | "computed-property-spacing": "warn",
68 | "consistent-this": "warn",
69 | "eol-last": "error",
70 | "func-call-spacing": "warn",
71 | "func-name-matching": "warn",
72 | "func-style": ["warn", "declaration", {"allowArrowFunctions": true}],
73 | "implicit-arrow-linebreak": "warn",
74 | "indent": ["warn", 2],
75 | "key-spacing": "warn",
76 | "keyword-spacing": "warn",
77 | "linebreak-style": "error",
78 | "lines-between-class-members": "warn",
79 | "newline-per-chained-call": "warn",
80 | "no-array-constructor": "error",
81 | "no-lonely-if": "warn",
82 | "no-multiple-empty-lines": ["error", {"max": 2, "maxEOF": 1, "maxBOF": 0}],
83 | "no-negated-condition": "warn",
84 | "no-nested-ternary": "error",
85 | "no-new-object": "error",
86 | "no-tabs": "error",
87 | "no-trailing-spaces": "warn",
88 | "no-unneeded-ternary": "warn",
89 | "no-whitespace-before-property": "warn",
90 | "nonblock-statement-body-position": "error",
91 | "object-curly-newline": "warn",
92 | "object-curly-spacing": "warn",
93 | "one-var": ["warn", "never"],
94 | "operator-assignment": "warn",
95 | "operator-linebreak": ["warn", "before"],
96 | "padded-blocks": ["warn", "never"],
97 | "quote-props": ["warn", "as-needed"],
98 | "quotes": ["warn", "single", {"avoidEscape": true, "allowTemplateLiterals": true}],
99 | "semi": ["warn", "never"],
100 | "semi-spacing": "warn",
101 | "semi-style": "warn",
102 | "space-before-blocks": "warn",
103 | "space-before-function-paren": ["warn", {
104 | "anonymous": "never",
105 | "named": "never",
106 | "asyncArrow": "always"
107 | }],
108 | "space-in-parens": "warn",
109 | "space-infix-ops": "warn",
110 | "space-unary-ops": ["warn", {"words": true, "nonwords": false}],
111 | "switch-colon-spacing": "warn",
112 | "template-curly-spacing": "warn",
113 | "unicode-bom": "error",
114 | "arrow-body-style": ["warn", "as-needed"],
115 | "arrow-parens": ["warn", "as-needed"],
116 | "arrow-spacing": "warn",
117 | "generator-star-spacing": ["warn", {"before": false, "after": true}],
118 | "no-duplicate-imports": "warn",
119 | "no-useless-computed-key": "warn",
120 | "no-useless-constructor": "warn",
121 | "no-useless-rename": "warn",
122 | "no-var": "error",
123 | "object-shorthand": "warn",
124 | "prefer-arrow-callback": "warn",
125 | "prefer-numeric-literals": "error",
126 | "prefer-rest-params": "error",
127 | "prefer-spread": "warn",
128 | "prefer-template": "warn",
129 | "rest-spread-spacing": "warn",
130 | "template-tag-spacing": "warn",
131 | "yield-star-spacing": "warn"
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # qtuminfo API Documentation
2 |
3 | * [Pagination Parameters](#pagination-parameters)
4 | * [Block / Timestamp Filter Parameters](#block--timestamp-filter-parameters)
5 | * [Blockchain](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/blockchain.md)
6 | * [Blockchain Information](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/blockchain.md#Blockchain-Information)
7 | * [Supply](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/blockchain.md#Supply)
8 | * [Total Max Supply](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/blockchain.md#Total-Max-Supply)
9 | * [Block](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/block.md)
10 | * [Block Information](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/block.md#Block-Information)
11 | * [Blocks of Date](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/block.md#Blocks-of-Date)
12 | * [Recent Blocks](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/block.md#Recent-Blocks)
13 | * [Transaction](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/transaction.md)
14 | * [Transaction Information](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/transaction.md#Transaction-Information)
15 | * [Raw Transaction](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/transaction.md#Raw-Transaction)
16 | * [Send Raw Transaction](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/transaction.md#Send-Raw-Transaction)
17 | * [Address](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/address.md)
18 | * [Address Information](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/address.md#Address-Information)
19 | * [Address Balance](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/address.md#Address-Balance)
20 | * [Address Transactions](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/address.md#Address-Transactions)
21 | * [Address Basic Transactions](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/address.md#Address-Basic-Transactions)
22 | * [Address Contract Transactions](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/address.md#Address-Contract-Transactions)
23 | * [Address QRC20 Token Transactions](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/address.md#Address-QRC20-Token-Transactions)
24 | * [Address UTXO List](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/address.md#Address-UTXO-List)
25 | * [Address Balance History](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/address.md#Address-Balance-History)
26 | * [Address QRC20 Balance History](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/address.md#Address-QRC20-Balance-History)
27 | * [Contract](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/contract.md)
28 | * [Contract Information](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/contract.md#Contract-Information)
29 | * [Contract Transactions](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/contract.md#Contract-Transactions)
30 | * [Contract Basic Transactions](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/contract.md#Contract-Basic-Transactions)
31 | * [Call Contract](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/contract.md#Call-Contract)
32 | * [Search Logs](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/contract.md#Search-Logs)
33 | * [QRC20](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/contract.md)
34 | * [QRC20 list](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/contract.md#QRC20-list)
35 | * [QRC20 Transaction list](https://github.com/qtumproject/qtuminfo-api/blob/master/doc/contract.md#QRC20-Transaction-list)
36 |
37 |
38 | ## API Endpoint
39 | * `https://qtum.info/api/` for mainnet
40 | * `https://testnet.qtum.info/api/` for testnet
41 |
42 |
43 | ## Pagination Parameters
44 |
45 | You may use one of 3 forms below, all indices count from 0, maximum 100 records per page:
46 | * `limit=20&offset=40`
47 | * `from=40&to=59`
48 | * `pageSize=20&page=2`
49 |
50 |
51 | ## Block / Timestamp Filter Parameters
52 |
53 | These params are available in some transaction list queries,
54 | records are picked only when `fromBlock <= blockHeight <= toBlock`, `fromTime <= blockTimestamp <= toTime`.
55 |
56 |
57 |
58 |
59 | | Name |
60 | Type |
61 | Description |
62 |
63 |
64 |
65 |
66 | fromBlock |
67 | Number (optional) |
68 | Search blocks from height |
69 |
70 |
71 | toBlock |
72 | Number (optional) |
73 | Search blocks until height |
74 |
75 |
76 | fromTime |
77 | ISO 8601 Date String (optional) |
78 | Search blocks from timestamp |
79 |
80 |
81 | toTime |
82 | ISO 8601 Date String (optional) |
83 | Search blocks until timestamp |
84 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/app/router.js:
--------------------------------------------------------------------------------
1 | module.exports = app => {
2 | const {router, controller, io, middleware} = app
3 | const addressMiddleware = middleware.address()
4 | const blockFilterMiddleware = middleware.blockFilter()
5 | const contractMiddleware = middleware.contract()
6 | const paginationMiddleware = middleware.pagination()
7 |
8 | router.get('/info', controller.info.index)
9 | router.get('/supply', controller.info.supply)
10 | router.get('/total-max-supply', controller.info.totalMaxSupply)
11 | router.get('/circulating-supply', controller.info.circulatingSupply)
12 | router.get('/feerates', controller.info.feeRates)
13 |
14 | router.get('/blocks', controller.block.list)
15 | router.get(
16 | '/block/list',
17 | paginationMiddleware,
18 | controller.block.blockList
19 | )
20 | router.get('/block/:block', controller.block.block)
21 | router.get('/raw-block/:block', controller.block.rawBlock)
22 | router.get('/recent-blocks', controller.block.recent)
23 |
24 | router.get(
25 | '/tx/list',
26 | paginationMiddleware,
27 | controller.transaction.list
28 | )
29 | router.get('/tx/:id', controller.transaction.transaction)
30 | router.get('/txs/:ids', controller.transaction.transactions)
31 | router.get('/raw-tx/:id', controller.transaction.rawTransaction)
32 | router.get('/recent-txs', controller.transaction.recent)
33 | router.post('/tx/send', controller.transaction.send)
34 |
35 | router.get(
36 | '/address/:address',
37 | addressMiddleware,
38 | controller.address.summary
39 | )
40 | router.get(
41 | '/address/:address/balance',
42 | addressMiddleware,
43 | controller.address.balance
44 | )
45 | router.get(
46 | '/address/:address/balance/total-received',
47 | addressMiddleware,
48 | controller.address.totalReceived
49 | )
50 | router.get(
51 | '/address/:address/balance/total-sent',
52 | addressMiddleware,
53 | controller.address.totalSent
54 | )
55 | router.get(
56 | '/address/:address/balance/unconfirmed',
57 | addressMiddleware,
58 | controller.address.unconfirmedBalance
59 | )
60 | router.get(
61 | '/address/:address/balance/staking',
62 | addressMiddleware,
63 | controller.address.stakingBalance
64 | )
65 | router.get(
66 | '/address/:address/balance/mature',
67 | addressMiddleware,
68 | controller.address.matureBalance
69 | )
70 | router.get(
71 | '/address/:address/qrc20-balance/:token',
72 | addressMiddleware, middleware.contract('token'),
73 | controller.address.qrc20TokenBalance
74 | )
75 | router.get(
76 | '/address/:address/txs',
77 | addressMiddleware, paginationMiddleware, blockFilterMiddleware,
78 | controller.address.transactions
79 | )
80 | router.get(
81 | '/address/:address/basic-txs',
82 | addressMiddleware, paginationMiddleware, blockFilterMiddleware,
83 | controller.address.basicTransactions
84 | )
85 | router.get(
86 | '/address/:address/contract-txs',
87 | addressMiddleware, paginationMiddleware, blockFilterMiddleware,
88 | controller.address.contractTransactions
89 | )
90 | router.get(
91 | '/address/:address/contract-txs/:contract',
92 | addressMiddleware, contractMiddleware, paginationMiddleware,
93 | controller.address.contractTransactions
94 | )
95 | router.get(
96 | '/address/:address/qrc20-txs/:token',
97 | addressMiddleware, middleware.contract('token'), paginationMiddleware,
98 | controller.address.qrc20TokenTransactions
99 | )
100 | router.get(
101 | '/address/:address/qrc20-mempool-txs/:token',
102 | addressMiddleware, middleware.contract('token'),
103 | controller.address.qrc20TokenMempoolTransactions
104 | )
105 | router.get(
106 | '/address/:address/utxo',
107 | addressMiddleware,
108 | controller.address.utxo
109 | )
110 | router.get(
111 | '/address/:address/balance-history',
112 | addressMiddleware, paginationMiddleware,
113 | controller.address.balanceHistory
114 | )
115 | router.get(
116 | '/address/:address/qrc20-balance-history',
117 | addressMiddleware, paginationMiddleware,
118 | controller.address.qrc20BalanceHistory
119 | )
120 | router.get(
121 | '/address/:address/qrc20-balance-history/:token',
122 | addressMiddleware, middleware.contract('token'), paginationMiddleware,
123 | controller.address.qrc20BalanceHistory
124 | )
125 |
126 | router.get(
127 | '/contract/:contract',
128 | contractMiddleware,
129 | controller.contract.summary
130 | )
131 | router.get(
132 | '/contract/:contract/txs',
133 | contractMiddleware, paginationMiddleware, blockFilterMiddleware,
134 | controller.contract.transactions
135 | )
136 | router.get(
137 | '/contract/:contract/basic-txs',
138 | contractMiddleware, paginationMiddleware, blockFilterMiddleware,
139 | controller.contract.basicTransactions
140 | )
141 | router.get(
142 | '/contract/:contract/balance-history',
143 | contractMiddleware, paginationMiddleware,
144 | controller.contract.balanceHistory
145 | )
146 | router.get(
147 | '/contract/:contract/qrc20-balance-history',
148 | contractMiddleware, paginationMiddleware,
149 | controller.contract.qrc20BalanceHistory
150 | )
151 | router.get(
152 | '/contract/:contract/qrc20-balance-history/:token',
153 | contractMiddleware, middleware.contract('token'), paginationMiddleware,
154 | controller.contract.qrc20BalanceHistory
155 | )
156 | router.get(
157 | '/contract/:contract/call',
158 | contractMiddleware,
159 | controller.contract.callContract
160 | )
161 | router.get(
162 | '/searchlogs',
163 | paginationMiddleware, blockFilterMiddleware,
164 | controller.contract.searchLogs
165 | )
166 | router.get(
167 | '/qrc20',
168 | paginationMiddleware,
169 | controller.qrc20.list
170 | )
171 | router.get(
172 | '/qrc20/txs',
173 | paginationMiddleware,
174 | controller.qrc20.allTransactions
175 | )
176 | router.get(
177 | '/qrc20/:token/txs',
178 | middleware.contract('token'), paginationMiddleware, blockFilterMiddleware,
179 | controller.qrc20.transactions
180 | )
181 | router.get(
182 | '/qrc20/:token/rich-list',
183 | middleware.contract('token'), paginationMiddleware,
184 | controller.qrc20.richList
185 | )
186 | router.get(
187 | '/qrc721',
188 | paginationMiddleware,
189 | controller.qrc721.list
190 | )
191 |
192 | router.get(`/search`, controller.misc.classify)
193 | router.get(
194 | '/misc/rich-list',
195 | paginationMiddleware,
196 | controller.misc.richList
197 | )
198 | router.get(
199 | '/misc/biggest-miners',
200 | paginationMiddleware,
201 | controller.misc.biggestMiners
202 | )
203 | router.get('/misc/prices', controller.misc.prices)
204 |
205 | router.get('/stats/daily-transactions', controller.statistics.dailyTransactions)
206 | router.get('/stats/block-interval', controller.statistics.blockInterval)
207 | router.get('/stats/address-growth', controller.statistics.addressGrowth)
208 |
209 | io.route('subscribe', io.controller.default.subscribe)
210 | io.route('unsubscribe', io.controller.default.unsubscribe)
211 | }
212 |
--------------------------------------------------------------------------------
/doc/block.md:
--------------------------------------------------------------------------------
1 | # Block API
2 |
3 | - [Block API](#Block-API)
4 | - [Block Information](#Block-Information)
5 | - [Blocks of Date](#Blocks-of-Date)
6 | - [Recent Blocks](#Recent-Blocks)
7 |
8 |
9 | ## Block Information
10 |
11 | **Request URL**
12 | ```
13 | GET /block/:height
14 | GET /block/:hash
15 | ```
16 |
17 | **Request**
18 | ```
19 | GET /block/400000
20 | GET /block/14f9d58d8f96d3a685808a8be3e5f2743dd71cb1af54fb8a134b9a1bc8bc20b8
21 | ```
22 |
23 | **Response**
24 | ```json
25 | {
26 | "hash": "14f9d58d8f96d3a685808a8be3e5f2743dd71cb1af54fb8a134b9a1bc8bc20b8",
27 | "height": 400000,
28 | "version": 536870912,
29 | "prevHash": "186d8797fc3280174c0dec83457c8a1e8c1bbd003baf930efce6f23d6cf2ac5a",
30 | "nextHash": "69e8f78ded5eb2f53f28b7bcde1a4299a61d9cde89d2a8a6c8ff2793fc777119",
31 | "merkleRoot": "93ce673230dbdd33c90fceca4a1504a8a40aa8ae51464f6f00848d239f08e0b8",
32 | "timestamp": 1561809392,
33 | "bits": "1a0aa152",
34 | "nonce": 0,
35 | "hashStateRoot": "ff18f1a8d8e6584f8f2c2e921257fd245668de5fb37c8d0800de675eaf673d21",
36 | "hashUTXORoot": "9e729950c184acd011471252a0c1a4bc279cd4c1e86d543bead4af6df787b2dd",
37 | "prevOutStakeHash": "9ed785ab666b13becffa83dc2aecc17a80cb434d4ba9670be455e15e730a2d2d",
38 | "prevOutStakeN": 1,
39 | "signature": "3044022027bb84b9e75477f3ca81c2bd12612593aa91daeafdb99feef7bbd2b560fb16ba022049e9af798574c8cfc70cd145d9cb223beff9ebf3576eada1c508e21c4edade46",
40 | "chainwork": "0000000000000000000000000000000000000000000001130f72ecb5f976a847",
41 | "flags": "proof-of-stake",
42 | "interval": 16,
43 | "size": 926,
44 | "weight": 3596,
45 | "transactions": [
46 | "50c7c324f59729da9b30f72cda43de64ef69fa7b617428c69cd1946931ff40b2",
47 | "89a16f4ce26db8029a835df02f46e0416fe729eee1da8b95225cb35a30fc78f9"
48 | ],
49 | "miner": "Qhyek8Kn36vCKSdkcmbSFdR1XfKRKGX6hP",
50 | "difficulty": 1578241.9071624815,
51 | "reward": "400000000",
52 | "confirmations": 5970
53 | }
54 | ```
55 |
56 |
57 | ## Blocks of Date
58 |
59 | **Request URL**
60 | ```
61 | GET /blocks
62 | ```
63 | **Request Params**
64 |
65 |
66 |
67 | | Name |
68 | Type |
69 | Default |
70 |
71 |
72 |
73 |
74 |
75 | date
76 | |
77 |
78 | ISO 8601 Date String (optional)
79 | |
80 | Today |
81 |
82 |
83 |
84 |
85 | **Request**
86 | ```
87 | GET /blocks?date=2019-01-01
88 | ```
89 |
90 | **Response**
91 | ```json
92 | [
93 | {
94 | "hash": "7c22bfc2192145e1db6d9c1492a0697ef7d23893798c454433fab3bcc2c2b35d",
95 | "height": 292738,
96 | "timestamp": 1546387072,
97 | "interval": 208,
98 | "size": 1668,
99 | "transactionCount": 5,
100 | "miner": "QiRx4CWwVHNqWf3dLW12kcnwRovSw8w9K6",
101 | "reward": "402883776"
102 | },
103 | {
104 | "hash": "27687c6f8a9ad7db5d278db9326f3ac9a0b701a851b29fe81b779f95ff759056",
105 | "height": 292737,
106 | "timestamp": 1546386864,
107 | "interval": 496,
108 | "size": 3178,
109 | "transactionCount": 5,
110 | "miner": "QYHV93kbN9osowPHTWHjeYzgrmZasdatov",
111 | "reward": "404394122"
112 | },
113 | {
114 | "hash": "37051ffc1e77a552994a6700bc32a778196f2f7165d07b4df020b134bf662021",
115 | "height": 292736,
116 | "timestamp": 1546386368,
117 | "interval": 64,
118 | "size": 928,
119 | "transactionCount": 2,
120 | "miner": "QdB6krR2kgssX7xnx8E9JPrRktfSBRBrEa",
121 | "reward": "400000000"
122 | },
123 | // ... 592 more items ...
124 | {
125 | "hash": "ed3d13dcc7b91ec384a034c32ff701b874977c06abf25fe5e621a390e147f7f7",
126 | "height": 292142,
127 | "timestamp": 1546301312,
128 | "interval": 16,
129 | "size": 1152,
130 | "transactionCount": 3,
131 | "miner": "Qgdj4FrXXxLVkxPDYtFNNGZNd9TJrNjtpM",
132 | "reward": "404833647"
133 | },
134 | {
135 | "hash": "0fe877ba6c90dfc7de1411e8f69378ddf94aaff718c21d11577661ad82898da1",
136 | "height": 292141,
137 | "timestamp": 1546301296,
138 | "interval": 32,
139 | "size": 1597,
140 | "transactionCount": 4,
141 | "miner": "QNLZS9sVUTTZZQ6UfaDnAwTQifbKWFZwmP",
142 | "reward": "407149600"
143 | },
144 | {
145 | "hash": "4a6f69b82f578c4edceaf03f0015f8ccc1ef613181ec69c64f95cc44e4c6ddc2",
146 | "height": 292140,
147 | "timestamp": 1546301264,
148 | "interval": 512,
149 | "size": 2008,
150 | "transactionCount": 6,
151 | "miner": "QgYB6x5RQBGgGL2R7Qe6WJAsUE4ZkdBj7Z",
152 | "reward": "422733104"
153 | }
154 | ]
155 | ```
156 |
157 |
158 | ## Recent Blocks
159 |
160 | **Request URL**
161 | ```
162 | GET /recent-blocks
163 | ```
164 | **Request Params**
165 |
166 |
167 |
168 | | Name |
169 | Type |
170 | Default |
171 | Description |
172 |
173 |
174 |
175 |
176 |
177 | count
178 | |
179 |
180 | Number (optional)
181 | |
182 | 10 |
183 | Number of Recent Blocks |
184 |
185 |
186 |
187 |
188 | **Request**
189 | ```
190 | GET /recent-blocks?count=5
191 | ```
192 |
193 | **Response**
194 | ```json
195 | [
196 | {
197 | "hash": "cda36eef9029d120c6f0ba34392954aa2a929643f33a791d7f47332721a7ab86",
198 | "height": 405981,
199 | "timestamp": 1562661728,
200 | "interval": 32,
201 | "size": 882,
202 | "transactionCount": 2,
203 | "miner": "QX8U4uMCaQzoLv6rcBvN4dBk9xYPi8TkcR",
204 | "reward": "400000000"
205 | },
206 | {
207 | "hash": "ca51ad6bcf5c300f8ffce4847dbe881b54cfac886746f09dc41e9323447f2ca2",
208 | "height": 405980,
209 | "timestamp": 1562661696,
210 | "interval": 112,
211 | "size": 1151,
212 | "transactionCount": 3,
213 | "miner": "QQrm6av1tWtTmvkTpft3FygcmLFcrEWGWk",
214 | "reward": "400090427"
215 | },
216 | {
217 | "hash": "e7f60b634158ae80347ca84d30388f0b7c48563dcfa9183a4a33c755668636fb",
218 | "height": 405979,
219 | "timestamp": 1562661584,
220 | "interval": 80,
221 | "size": 1406,
222 | "transactionCount": 3,
223 | "miner": "QXDkSQAFEneCaPMC4ML6w8kmcrYxhsz4Qv",
224 | "reward": "402233609"
225 | },
226 | {
227 | "hash": "96adbe135480a585372a67739a3d64d593f7c826c5542e9c5f156de711a7df81",
228 | "height": 405978,
229 | "timestamp": 1562661504,
230 | "interval": 320,
231 | "size": 11337,
232 | "transactionCount": 7,
233 | "miner": "QhUdHBgHcW7rTYxtzbs3By4nz9sggE5EbV",
234 | "reward": "408284149"
235 | },
236 | {
237 | "hash": "9b3dab86fd71d24cb144ae44d8106241b52b9cc2c68a9924e1a371eb897ed4fe",
238 | "height": 405977,
239 | "timestamp": 1562661184,
240 | "interval": 240,
241 | "size": 1852,
242 | "transactionCount": 5,
243 | "miner": "QWKsTwTkH5n2qp1ivWp6ropknSsqKj9NXY",
244 | "reward": "400409061"
245 | }
246 | ]
247 | ```
248 |
--------------------------------------------------------------------------------
/doc/qrc20.md:
--------------------------------------------------------------------------------
1 | # QRC20 API
2 |
3 | - [QRC20 API](#QRC20-API)
4 | - [QRC20 list](#QRC20-list)
5 | - [QRC20 Transaction list](#QRC20-Transaction-list)
6 |
7 |
8 | ## QRC20 list
9 | List all qrc20 tokens order by transfer transactions count.
10 |
11 | **Request URL**
12 | ```
13 | GET /qrc20
14 | ```
15 |
16 | **Request Parameter**
17 |
35 |
36 | **Request**
37 | ```
38 | GET /qrc20?limit=10&offset=0
39 | ```
40 |
41 | **Response**
42 | ```json
43 | {
44 | "totalCount": 184,
45 | "tokens": [
46 | {
47 | "address": "EfDYuWmSUbZPaAe2qzeWurcDGobSnhYa6F",
48 | "addressHex": "f2033ede578e17fa6231047265010445bca8cf1c",
49 | "name": "QCASH",
50 | "symbol": "QC",
51 | "decimals": 8,
52 | "totalSupply": "1000000000000000000",
53 | "version": null,
54 | "holders": 22612,
55 | "transactions": 133347
56 | },
57 | {
58 | "address": "EgLnhSREpcpnrmSHp3uKQT1nPjhiV7DEx9",
59 | "addressHex": "fe59cbc1704e89a698571413a81f0de9d8f00c69",
60 | "name": "INK Coin",
61 | "symbol": "INK",
62 | "decimals": 9,
63 | "totalSupply": "1000000000000000000",
64 | "version": null,
65 | "holders": 33588,
66 | "transactions": 75033
67 | },
68 | {
69 | "address": "ERPLiez3T9MQ21tz9B8kLmajK7xQWrkS6B",
70 | "addressHex": "5a4b7889cad562d6c099bf877c8f5e3d66d579f8",
71 | "name": "FENIX.CASH",
72 | "symbol": "FENIX",
73 | "decimals": 18,
74 | "totalSupply": "432000000000000000000000000",
75 | "version": null,
76 | "holders": 58118,
77 | "transactions": 60343
78 | },
79 | {
80 | "address": "ER8xXcVq6HmXLfPUKmQpusvtxoHoAM9uC3",
81 | "addressHex": "57931faffdec114056a49adfcaa1caac159a1a25",
82 | "name": "SpaceCash",
83 | "symbol": "SPC",
84 | "decimals": 8,
85 | "totalSupply": "100000000000000000",
86 | "version": null,
87 | "holders": 12375,
88 | "transactions": 50625
89 | },
90 | {
91 | "address": "ESxZUjVBnbeeEyx7E1WhwviAujfRmfCgjU",
92 | "addressHex": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7",
93 | "name": "Bodhi Token",
94 | "symbol": "BOT",
95 | "decimals": 8,
96 | "totalSupply": "10000000000000000",
97 | "version": null,
98 | "holders": 33219,
99 | "transactions": 49624
100 | },
101 | {
102 | "address": "EfFoUtTKMVcWHKJeByQate1jdCkYTXHaBC",
103 | "addressHex": "f2703e93f87b846a7aacec1247beaec1c583daa4",
104 | "name": "Hyperpay",
105 | "symbol": "HPY",
106 | "decimals": 8,
107 | "totalSupply": "265000000000000000",
108 | "version": null,
109 | "holders": 7269,
110 | "transactions": 42171
111 | },
112 | {
113 | "address": "EPr1RrYoNpHPiHFV1R1mHjJf6TfzFJXuTd",
114 | "addressHex": "49665919e437a4bedb92faa45ed33ebb5a33ee63",
115 | "name": "AWARE Token",
116 | "symbol": "AWR",
117 | "decimals": 8,
118 | "totalSupply": "100000000000000000",
119 | "version": "1.0",
120 | "holders": 1967,
121 | "transactions": 37871
122 | },
123 | {
124 | "address": "EZRg6Xhqe3V7Sva29irwZHj9LwcoknARtR",
125 | "addressHex": "b27d7bf95b03e02b55d5eb63d3f1692762101bf9",
126 | "name": "Halal Chain",
127 | "symbol": "HLC",
128 | "decimals": 9,
129 | "totalSupply": "1000000000000000000",
130 | "version": null,
131 | "holders": 5404,
132 | "transactions": 30136
133 | },
134 | {
135 | "address": "EMUWzpCfRZbgyQZd6oHdkXsuaJgVy8ncdL",
136 | "addressHex": "2f65a0af11d50d2d15962db39d7f7b0619ed55ae",
137 | "name": "MED TOKEN",
138 | "symbol": "MED",
139 | "decimals": 8,
140 | "totalSupply": "1000000000000000000",
141 | "version": null,
142 | "holders": 4801,
143 | "transactions": 27933
144 | },
145 | {
146 | "address": "ETdR6C6aeX5MqCkNBYNBdTPa5rEdc1khne",
147 | "addressHex": "72e531e37c31ecbe336208fd66e93b48df3af420",
148 | "name": "Luna Stars",
149 | "symbol": "LSTR",
150 | "decimals": 8,
151 | "totalSupply": "3800000000000000000",
152 | "version": null,
153 | "holders": 13374,
154 | "transactions": 26867
155 | }
156 | ]
157 | }
158 | ```
159 |
160 |
161 | ## QRC20 Transaction list
162 |
163 | **Request URL**
164 | ```
165 | GET /qrc20/:token/txs
166 | ```
167 |
168 | **Request Parameter**
169 |
203 |
204 | **Request**
205 | ```
206 | GET /qrc20/f2033ede578e17fa6231047265010445bca8cf1c/txs?limit=5&offset=0
207 | ```
208 |
209 | **Response**
210 | ```json
211 | {
212 | "totalCount": 133563,
213 | "transactions": [
214 | {
215 | "transactionId": "56a76c38966c663bafef3247c2475af45c8d1c81adaa7a27773b05853063d06f",
216 | "outputIndex": 0,
217 | "blockHeight": 408403,
218 | "blockHash": "7feac3a08ae8f5561c39e3c1a9c4c5d79c124bf8d79a671145db6933fdb08266",
219 | "timestamp": 1563009408,
220 | "from": "QWSTGRwdScLfdr6agUqR4G7ow4Mjc4e5re",
221 | "to": "QanTJBm9NTXqiFYNe9rWLi3dJPSTfoHZrL",
222 | "value": "373300000000"
223 | },
224 | {
225 | "transactionId": "b3cff5534d7e7720edb177b0cf36fad1c6839926116bc39175e48ef541b0718a",
226 | "outputIndex": 0,
227 | "blockHeight": 408402,
228 | "blockHash": "f74fa86a8bb9eee226eebcdac76e10e25f966b791bc05b2ab16db9e00b396af6",
229 | "timestamp": 1563009328,
230 | "from": "QUZ6sqXN5dXWUAQniy9uPVpC3QjyXhoznz",
231 | "to": "QhXS93hPpUcjoDxo192bmrbDubhH5UoQDp",
232 | "value": "143458584740"
233 | },
234 | {
235 | "transactionId": "8b79a1fc8f1381ff05fcc4f53430a6be072ca4ba5148037a48ab2b330b66dc60",
236 | "outputIndex": 0,
237 | "blockHeight": 408402,
238 | "blockHash": "f74fa86a8bb9eee226eebcdac76e10e25f966b791bc05b2ab16db9e00b396af6",
239 | "timestamp": 1563009328,
240 | "from": "QaDcm9UeGveyVWY9UiNRN9cTsFMe8v2k2t",
241 | "to": "QhXS93hPpUcjoDxo192bmrbDubhH5UoQDp",
242 | "value": "1097495687249"
243 | },
244 | {
245 | "transactionId": "28948af9bee1cba22b2af04fc35d47bcb5b4dd5d2a817168a4238d141d7f5a62",
246 | "outputIndex": 1,
247 | "blockHeight": 408398,
248 | "blockHash": "4da2cc6042360ade09b4a48b2512fb8e950c97d196e6729718d7301296f0bc3e",
249 | "timestamp": 1563009056,
250 | "from": "QWSTGRwdScLfdr6agUqR4G7ow4Mjc4e5re",
251 | "to": "QUZ6sqXN5dXWUAQniy9uPVpC3QjyXhoznz",
252 | "value": "70729292370"
253 | },
254 | {
255 | "transactionId": "801c94a6b6fa331f517b3dd85dd5ba778b541e913c6590feb9e26590cfad5050",
256 | "outputIndex": 1,
257 | "blockHeight": 408398,
258 | "blockHash": "4da2cc6042360ade09b4a48b2512fb8e950c97d196e6729718d7301296f0bc3e",
259 | "timestamp": 1563009056,
260 | "from": "QWSTGRwdScLfdr6agUqR4G7ow4Mjc4e5re",
261 | "to": "QTSbcq4y5i4SgxSc6Cz5yDV9LEQBKW4eQE",
262 | "value": "1000000000"
263 | }
264 | ]
265 | }
266 | ```
267 |
--------------------------------------------------------------------------------
/app/controller/contract.js:
--------------------------------------------------------------------------------
1 | const {Controller} = require('egg')
2 |
3 | class ContractController extends Controller {
4 | async summary() {
5 | const {ctx} = this
6 | let summary = await ctx.service.contract.getContractSummary(
7 | ctx.state.contract.contractAddress, ctx.state.contract.addressIds
8 | )
9 | ctx.body = {
10 | address: summary.addressHex.toString('hex'),
11 | addressHex: summary.addressHex.toString('hex'),
12 | vm: summary.vm,
13 | type: summary.type,
14 | ...summary.type === 'qrc20' ? {
15 | qrc20: {
16 | name: summary.qrc20.name,
17 | symbol: summary.qrc20.symbol,
18 | decimals: summary.qrc20.decimals,
19 | totalSupply: summary.qrc20.totalSupply.toString(),
20 | version: summary.qrc20.version,
21 | holders: summary.qrc20.holders,
22 | transactions: summary.qrc20.transactions
23 | }
24 | } : {},
25 | ...summary.type === 'qrc721' ? {
26 | qrc721: {
27 | name: summary.qrc721.name,
28 | symbol: summary.qrc721.symbol,
29 | totalSupply: summary.qrc721.totalSupply.toString()
30 | }
31 | } : {},
32 | balance: summary.balance.toString(),
33 | totalReceived: summary.totalReceived.toString(),
34 | totalSent: summary.totalSent.toString(),
35 | unconfirmed: summary.unconfirmed.toString(),
36 | qrc20Balances: summary.qrc20Balances.map(item => ({
37 | address: item.addressHex.toString('hex'),
38 | addressHex: item.addressHex.toString('hex'),
39 | name: item.name,
40 | symbol: item.symbol,
41 | decimals: item.decimals,
42 | balance: item.balance.toString()
43 | })),
44 | qrc721Balances: summary.qrc721Balances.map(item => ({
45 | address: item.addressHex.toString('hex'),
46 | addressHex: item.addressHex.toString('hex'),
47 | name: item.name,
48 | symbol: item.symbol,
49 | count: item.count
50 | })),
51 | transactionCount: summary.transactionCount
52 | }
53 | }
54 |
55 | async transactions() {
56 | let {ctx} = this
57 | let {totalCount, transactions} = await ctx.service.contract.getContractTransactions(
58 | ctx.state.contract.contractAddress, ctx.state.contract.addressIds
59 | )
60 | ctx.body = {
61 | totalCount,
62 | transactions: transactions.map(id => id.toString('hex'))
63 | }
64 | }
65 |
66 | async basicTransactions() {
67 | let {ctx} = this
68 | let {totalCount, transactions} = await ctx.service.contract.getContractBasicTransactions(ctx.state.contract.contractAddress)
69 | ctx.body = {
70 | totalCount,
71 | transactions: transactions.map(transaction => ({
72 | transactionId: transaction.transactionId.toString('hex'),
73 | outputIndex: transaction.outputIndex,
74 | blockHeight: transaction.blockHeight,
75 | blockHash: transaction.blockHash && transaction.blockHash.toString('hex'),
76 | timestamp: transaction.timestamp,
77 | confirmations: transaction.confirmations,
78 | type: transaction.scriptPubKey.type,
79 | gasLimit: transaction.scriptPubKey.gasLimit,
80 | gasPrice: transaction.scriptPubKey.gasPrice,
81 | byteCode: transaction.scriptPubKey.byteCode.toString('hex'),
82 | outputValue: transaction.value.toString(),
83 | sender: transaction.sender.toString(),
84 | gasUsed: transaction.gasUsed,
85 | contractAddress: transaction.contractAddressHex.toString('hex'),
86 | contractAddressHex: transaction.contractAddressHex.toString('hex'),
87 | excepted: transaction.excepted,
88 | exceptedMessage: transaction.exceptedMessage,
89 | evmLogs: transaction.evmLogs.map(log => ({
90 | address: log.addressHex.toString('hex'),
91 | addressHex: log.addressHex.toString('hex'),
92 | topics: log.topics.map(topic => topic.toString('hex')),
93 | data: log.data.toString('hex')
94 | }))
95 | }))
96 | }
97 | }
98 |
99 | async balanceHistory() {
100 | let {ctx} = this
101 | let {totalCount, transactions} = await ctx.service.balance.getBalanceHistory(ctx.state.contract.addressIds, {nonZero: true})
102 | ctx.body = {
103 | totalCount,
104 | transactions: transactions.map(tx => ({
105 | id: tx.id.toString('hex'),
106 | ...tx.block ? {
107 | blockHash: tx.block.hash.toString('hex'),
108 | blockHeight: tx.block.height,
109 | timestamp: tx.block.timestamp
110 | } : {},
111 | amount: tx.amount.toString(),
112 | balance: tx.balance.toString()
113 | }))
114 | }
115 | }
116 |
117 | async qrc20BalanceHistory() {
118 | let {ctx} = this
119 | let tokenAddress = null
120 | if (ctx.state.token) {
121 | if (ctx.state.token.type === 'qrc20') {
122 | tokenAddress = ctx.state.token.contractAddress
123 | } else {
124 | ctx.body = {
125 | totalCount: 0,
126 | transactions: []
127 | }
128 | return
129 | }
130 | }
131 | let {totalCount, transactions} = await ctx.service.qrc20.getQRC20BalanceHistory([ctx.state.contract.contractAddress], tokenAddress)
132 | ctx.body = {
133 | totalCount,
134 | transactions: transactions.map(tx => ({
135 | id: tx.id.toString('hex'),
136 | hash: tx.block.hash.toString('hex'),
137 | height: tx.block.height,
138 | timestamp: tx.block.timestamp,
139 | tokens: tx.tokens.map(item => ({
140 | address: item.addressHex.toString('hex'),
141 | addressHex: item.addressHex.toString('hex'),
142 | name: item.name,
143 | symbol: item.symbol,
144 | decimals: item.decimals,
145 | amount: item.amount.toString(),
146 | balance: item.balance.toString()
147 | }))
148 | }))
149 | }
150 | }
151 |
152 | async callContract() {
153 | const {Address} = this.app.qtuminfo.lib
154 | let {ctx} = this
155 | let {data, sender} = ctx.query
156 | ctx.assert(ctx.state.contract.vm === 'evm', 400)
157 | ctx.assert(/^([0-9a-f]{2})+$/i.test(data), 400)
158 | if (sender != null) {
159 | try {
160 | let address = Address.fromString(sender, this.app.chain)
161 | if ([Address.PAY_TO_PUBLIC_KEY_HASH, Address.CONTRACT, Address.EVM_CONTRACT].includes(address.type)) {
162 | sender = address.data
163 | } else {
164 | ctx.throw(400)
165 | }
166 | } catch (err) {
167 | ctx.throw(400)
168 | }
169 | }
170 | ctx.body = await ctx.service.contract.callContract(ctx.state.contract.contractAddress, data, sender)
171 | }
172 |
173 | async searchLogs() {
174 | let {ctx} = this
175 | let {contract, topic1, topic2, topic3, topic4} = this.ctx.query
176 | if (contract != null) {
177 | contract = (await ctx.service.contract.getContractAddresses([contract]))[0]
178 | }
179 | if (topic1 != null) {
180 | if (/^[0-9a-f]{64}$/i.test(topic1)) {
181 | topic1 = Buffer.from(topic1, 'hex')
182 | } else {
183 | ctx.throw(400)
184 | }
185 | }
186 | if (topic2 != null) {
187 | if (/^[0-9a-f]{64}$/i.test(topic2)) {
188 | topic2 = Buffer.from(topic2, 'hex')
189 | } else {
190 | ctx.throw(400)
191 | }
192 | }
193 | if (topic3 != null) {
194 | if (/^[0-9a-f]{64}$/i.test(topic3)) {
195 | topic3 = Buffer.from(topic3, 'hex')
196 | } else {
197 | ctx.throw(400)
198 | }
199 | }
200 | if (topic4 != null) {
201 | if (/^[0-9a-f]{64}$/i.test(topic4)) {
202 | topic4 = Buffer.from(topic4, 'hex')
203 | } else {
204 | ctx.throw(400)
205 | }
206 | }
207 |
208 | let {totalCount, logs} = await ctx.service.contract.searchLogs({contract, topic1, topic2, topic3, topic4})
209 | ctx.body = {
210 | totalCount,
211 | logs: logs.map(log => ({
212 | transactionId: log.transactionId.toString('hex'),
213 | outputIndex: log.outputIndex,
214 | blockHash: log.blockHash.toString('hex'),
215 | blockHeight: log.blockHeight,
216 | timestamp: log.timestamp,
217 | sender: log.sender.toString(),
218 | contractAddress: log.contractAddressHex.toString('hex'),
219 | contractAddressHex: log.contractAddressHex.toString('hex'),
220 | address: log.addressHex.toString('hex'),
221 | addressHex: log.addressHex.toString('hex'),
222 | topics: log.topics.map(topic => topic.toString('hex')),
223 | data: log.data.toString('hex')
224 | }))
225 | }
226 | }
227 | }
228 |
229 | module.exports = ContractController
230 |
--------------------------------------------------------------------------------
/app/controller/address.js:
--------------------------------------------------------------------------------
1 | const {Controller} = require('egg')
2 |
3 | class AddressController extends Controller {
4 | async summary() {
5 | let {ctx} = this
6 | let {address} = ctx.state
7 | let summary = await ctx.service.address.getAddressSummary(address.addressIds, address.p2pkhAddressIds, address.rawAddresses)
8 | ctx.body = {
9 | balance: summary.balance.toString(),
10 | totalReceived: summary.totalReceived.toString(),
11 | totalSent: summary.totalSent.toString(),
12 | unconfirmed: summary.unconfirmed.toString(),
13 | staking: summary.staking.toString(),
14 | mature: summary.mature.toString(),
15 | qrc20Balances: summary.qrc20Balances.map(item => ({
16 | address: item.addressHex.toString('hex'),
17 | addressHex: item.addressHex.toString('hex'),
18 | name: item.name,
19 | symbol: item.symbol,
20 | decimals: item.decimals,
21 | balance: item.balance.toString(),
22 | unconfirmed: {
23 | received: item.unconfirmed.received.toString(),
24 | sent: item.unconfirmed.sent.toString()
25 | },
26 | isUnconfirmed: item.isUnconfirmed
27 | })),
28 | qrc721Balances: summary.qrc721Balances.map(item => ({
29 | address: item.addressHex.toString('hex'),
30 | addressHex: item.addressHex.toString('hex'),
31 | name: item.name,
32 | symbol: item.symbol,
33 | count: item.count
34 | })),
35 | ranking: summary.ranking,
36 | transactionCount: summary.transactionCount,
37 | blocksMined: summary.blocksMined
38 | }
39 | }
40 |
41 | async balance() {
42 | let {ctx} = this
43 | let balance = await ctx.service.balance.getBalance(ctx.state.address.addressIds)
44 | ctx.body = balance.toString()
45 | }
46 |
47 | async totalReceived() {
48 | let {ctx} = this
49 | let {totalReceived} = await ctx.service.balance.getTotalBalanceChanges(ctx.state.address.addressIds)
50 | ctx.body = totalReceived.toString()
51 | }
52 |
53 | async totalSent() {
54 | let {ctx} = this
55 | let {totalSent} = await ctx.service.balance.getTotalBalanceChanges(ctx.state.address.addressIds)
56 | ctx.body = totalSent.toString()
57 | }
58 |
59 | async unconfirmedBalance() {
60 | let {ctx} = this
61 | let unconfirmed = await ctx.service.balance.getUnconfirmedBalance(ctx.state.address.addressIds)
62 | ctx.body = unconfirmed.toString()
63 | }
64 |
65 | async stakingBalance() {
66 | let {ctx} = this
67 | let unconfirmed = await ctx.service.balance.getStakingBalance(ctx.state.address.addressIds)
68 | ctx.body = unconfirmed.toString()
69 | }
70 |
71 | async matureBalance() {
72 | let {ctx} = this
73 | let unconfirmed = await ctx.service.balance.getMatureBalance(ctx.state.address.p2pkhAddressIds)
74 | ctx.body = unconfirmed.toString()
75 | }
76 |
77 | async qrc20TokenBalance() {
78 | let {ctx} = this
79 | let {address, token} = ctx.state
80 | if (token.type !== 'qrc20') {
81 | ctx.body = {}
82 | }
83 | let {name, symbol, decimals, balance, unconfirmed} = await ctx.service.qrc20.getQRC20Balance(address.rawAddresses, token.contractAddress)
84 | ctx.body = {
85 | name,
86 | symbol,
87 | decimals,
88 | balance: balance.toString(),
89 | unconfirmed: {
90 | received: unconfirmed.received.toString(),
91 | sent: unconfirmed.sent.toString()
92 | }
93 | }
94 | }
95 |
96 | async transactions() {
97 | let {ctx} = this
98 | let {address} = ctx.state
99 | let {totalCount, transactions} = await ctx.service.address.getAddressTransactions(address.addressIds, address.rawAddresses)
100 | ctx.body = {
101 | totalCount,
102 | transactions: transactions.map(id => id.toString('hex'))
103 | }
104 | }
105 |
106 | async basicTransactions() {
107 | let {ctx} = this
108 | let {totalCount, transactions} = await ctx.service.address.getAddressBasicTransactions(ctx.state.address.addressIds)
109 | ctx.body = {
110 | totalCount,
111 | transactions: transactions.map(transaction => ({
112 | id: transaction.id.toString('hex'),
113 | blockHeight: transaction.blockHeight,
114 | blockHash: transaction.blockHash && transaction.blockHash.toString('hex'),
115 | timestamp: transaction.timestamp,
116 | confirmations: transaction.confirmations,
117 | amount: transaction.amount.toString(),
118 | inputValue: transaction.inputValue.toString(),
119 | outputValue: transaction.outputValue.toString(),
120 | refundValue: transaction.refundValue.toString(),
121 | fees: transaction.fees.toString(),
122 | type: transaction.type
123 | }))
124 | }
125 | }
126 |
127 | async contractTransactions() {
128 | let {ctx} = this
129 | let {address, contract} = ctx.state
130 | let {totalCount, transactions} = await ctx.service.address.getAddressContractTransactions(address.rawAddresses, contract)
131 | ctx.body = {
132 | totalCount,
133 | transactions: transactions.map(transaction => ({
134 | transactionId: transaction.transactionId.toString('hex'),
135 | outputIndex: transaction.outputIndex,
136 | blockHeight: transaction.blockHeight,
137 | blockHash: transaction.blockHash && transaction.blockHash.toString('hex'),
138 | timestamp: transaction.timestamp,
139 | confirmations: transaction.confirmations,
140 | type: transaction.scriptPubKey.type,
141 | gasLimit: transaction.scriptPubKey.gasLimit,
142 | gasPrice: transaction.scriptPubKey.gasPrice,
143 | byteCode: transaction.scriptPubKey.byteCode.toString('hex'),
144 | outputValue: transaction.value.toString(),
145 | outputAddress: transaction.outputAddressHex.toString('hex'),
146 | outputAddressHex: transaction.outputAddressHex.toString('hex'),
147 | sender: transaction.sender.toString(),
148 | gasUsed: transaction.gasUsed,
149 | contractAddress: transaction.contractAddressHex.toString('hex'),
150 | contractAddressHex: transaction.contractAddressHex.toString('hex'),
151 | excepted: transaction.excepted,
152 | exceptedMessage: transaction.exceptedMessage,
153 | evmLogs: transaction.evmLogs.map(log => ({
154 | address: log.addressHex.toString('hex'),
155 | addressHex: log.addressHex.toString('hex'),
156 | topics: log.topics.map(topic => topic.toString('hex')),
157 | data: log.data.toString('hex')
158 | }))
159 | }))
160 | }
161 | }
162 |
163 | async qrc20TokenTransactions() {
164 | let {ctx} = this
165 | let {address, token} = ctx.state
166 | let {totalCount, transactions} = await ctx.service.address.getAddressQRC20TokenTransactions(address.rawAddresses, token)
167 | ctx.body = {
168 | totalCount,
169 | transactions: transactions.map(transaction => ({
170 | transactionId: transaction.transactionId.toString('hex'),
171 | outputIndex: transaction.outputIndex,
172 | blockHeight: transaction.blockHeight,
173 | blockHash: transaction.blockHash.toString('hex'),
174 | timestamp: transaction.timestamp,
175 | confirmations: transaction.confirmations,
176 | from: transaction.from,
177 | fromHex: transaction.fromHex && transaction.fromHex.toString('hex'),
178 | to: transaction.to,
179 | toHex: transaction.toHex && transaction.toHex.toString('hex'),
180 | value: transaction.value.toString(),
181 | amount: transaction.amount.toString()
182 | }))
183 | }
184 | }
185 |
186 | async qrc20TokenMempoolTransactions() {
187 | let {ctx} = this
188 | let {address, token} = ctx.state
189 | let transactions = await ctx.service.address.getAddressQRC20TokenMempoolTransactions(address.rawAddresses, token)
190 | ctx.body = transactions.map(transaction => ({
191 | transactionId: transaction.transactionId.toString('hex'),
192 | outputIndex: transaction.outputIndex,
193 | from: transaction.from,
194 | fromHex: transaction.fromHex && transaction.fromHex.toString('hex'),
195 | to: transaction.to,
196 | toHex: transaction.toHex && transaction.toHex.toString('hex'),
197 | value: transaction.value.toString(),
198 | amount: transaction.amount.toString()
199 | }))
200 | }
201 |
202 | async utxo() {
203 | let {ctx} = this
204 | let utxos = await ctx.service.address.getUTXO(ctx.state.address.addressIds)
205 | ctx.body = utxos.map(utxo => ({
206 | transactionId: utxo.transactionId.toString('hex'),
207 | outputIndex: utxo.outputIndex,
208 | scriptPubKey: utxo.scriptPubKey.toString('hex'),
209 | address: utxo.address,
210 | value: utxo.value.toString(),
211 | isStake: utxo.isStake,
212 | blockHeight: utxo.blockHeight,
213 | confirmations: utxo.confirmations
214 | }))
215 | }
216 |
217 | async balanceHistory() {
218 | let {ctx} = this
219 | let {totalCount, transactions} = await ctx.service.balance.getBalanceHistory(ctx.state.address.addressIds)
220 | ctx.body = {
221 | totalCount,
222 | transactions: transactions.map(tx => ({
223 | id: tx.id.toString('hex'),
224 | ...tx.block ? {
225 | blockHash: tx.block.hash.toString('hex'),
226 | blockHeight: tx.block.height,
227 | timestamp: tx.block.timestamp
228 | } : {},
229 | amount: tx.amount.toString(),
230 | balance: tx.balance.toString()
231 | }))
232 | }
233 | }
234 |
235 | async qrc20BalanceHistory() {
236 | const {Address} = this.app.qtuminfo.lib
237 | let {ctx} = this
238 | let tokenAddress = null
239 | if (ctx.state.token) {
240 | if (ctx.state.token.type === 'qrc20') {
241 | tokenAddress = ctx.state.token.contractAddress
242 | } else {
243 | ctx.body = {
244 | totalCount: 0,
245 | transactions: []
246 | }
247 | return
248 | }
249 | }
250 | let hexAddresses = ctx.state.address.rawAddresses
251 | .filter(address => address.type === Address.PAY_TO_PUBLIC_KEY_HASH)
252 | .map(address => address.data)
253 | let {totalCount, transactions} = await ctx.service.qrc20.getQRC20BalanceHistory(hexAddresses, tokenAddress)
254 | ctx.body = {
255 | totalCount,
256 | transactions: transactions.map(tx => ({
257 | id: tx.id.toString('hex'),
258 | blockHash: tx.block.hash.toString('hex'),
259 | blockHeight: tx.block.height,
260 | timestamp: tx.block.timestamp,
261 | tokens: tx.tokens.map(item => ({
262 | address: item.addressHex.toString('hex'),
263 | addressHex: item.addressHex.toString('hex'),
264 | name: item.name,
265 | symbol: item.symbol,
266 | decimals: item.decimals,
267 | amount: item.amount.toString(),
268 | balance: item.balance.toString()
269 | }))
270 | }))
271 | }
272 | }
273 | }
274 |
275 | module.exports = AddressController
276 |
--------------------------------------------------------------------------------
/app/service/balance.js:
--------------------------------------------------------------------------------
1 | const {Service} = require('egg')
2 |
3 | class BalanceService extends Service {
4 | async getBalance(ids) {
5 | const {TransactionOutput} = this.ctx.model
6 | const {in: $in, gt: $gt} = this.app.Sequelize.Op
7 | let result = await TransactionOutput.aggregate('value', 'SUM', {
8 | where: {
9 | addressId: {[$in]: ids},
10 | blockHeight: {[$gt]: 0},
11 | inputId: 0
12 | },
13 | transaction: this.ctx.state.transaction
14 | })
15 | return BigInt(result || 0)
16 | }
17 |
18 | async getTotalBalanceChanges(ids) {
19 | if (ids.length === 0) {
20 | return {totalReceived: 0n, totalSent: 0n}
21 | }
22 |
23 | const db = this.ctx.model
24 | const {sql} = this.ctx.helper
25 | let totalReceived
26 | let totalSent
27 | if (ids.length === 1) {
28 | let [result] = await db.query(sql`
29 | SELECT
30 | SUM(CAST(GREATEST(value, 0) AS DECIMAL(24))) AS totalReceived,
31 | SUM(CAST(GREATEST(-value, 0) AS DECIMAL(24))) AS totalSent
32 | FROM balance_change WHERE address_id = ${ids[0]} AND block_height > 0
33 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction})
34 | totalReceived = result.totalReceived == null ? 0n : BigInt(result.totalReceived)
35 | totalSent = result.totalSent == null ? 0n : BigInt(result.totalSent)
36 | } else {
37 | let [result] = await db.query(sql`
38 | SELECT
39 | SUM(CAST(GREATEST(value, 0) AS DECIMAL(24))) AS totalReceived,
40 | SUM(CAST(GREATEST(-value, 0) AS DECIMAL(24))) AS totalSent
41 | FROM (
42 | SELECT SUM(value) AS value FROM balance_change
43 | WHERE address_id IN ${ids} AND block_height > 0
44 | GROUP BY transaction_id
45 | ) AS temp
46 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction})
47 | totalReceived = result.totalReceived == null ? 0n : BigInt(result.totalReceived)
48 | totalSent = result.totalSent == null ? 0n : BigInt(result.totalSent)
49 | }
50 | return {totalReceived, totalSent}
51 | }
52 |
53 | async getUnconfirmedBalance(ids) {
54 | const {TransactionOutput} = this.ctx.model
55 | const {in: $in} = this.app.Sequelize.Op
56 | let result = await TransactionOutput.aggregate('value', 'SUM', {
57 | where: {
58 | addressId: {[$in]: ids},
59 | blockHeight: 0xffffffff,
60 | inputHeight: null
61 | },
62 | transaction: this.ctx.state.transaction
63 | })
64 | return BigInt(result || 0)
65 | }
66 |
67 | async getStakingBalance(ids) {
68 | const {TransactionOutput} = this.ctx.model
69 | const {in: $in, gt: $gt} = this.app.Sequelize.Op
70 | let result = await TransactionOutput.aggregate('value', 'SUM', {
71 | where: {
72 | addressId: {[$in]: ids},
73 | blockHeight: {[$gt]: this.app.blockchainInfo.tip.height - 500},
74 | inputHeight: null,
75 | isStake: true
76 | },
77 | transaction: this.ctx.state.transaction
78 | })
79 | return BigInt(result || 0)
80 | }
81 |
82 | async getMatureBalance(ids) {
83 | const {TransactionOutput} = this.ctx.model
84 | const {in: $in, between: $between} = this.app.Sequelize.Op
85 | let result = await TransactionOutput.aggregate('value', 'SUM', {
86 | where: {
87 | addressId: {[$in]: ids},
88 | blockHeight: {[$between]: [1, this.app.blockchainInfo.tip.height - 500]},
89 | inputHeight: null
90 | },
91 | transaction: this.ctx.state.transaction
92 | })
93 | return BigInt(result || 0)
94 | }
95 |
96 | async getBalanceHistory(ids, {nonZero = false} = {}) {
97 | if (ids.length === 0) {
98 | return []
99 | }
100 | const db = this.ctx.model
101 | const {sql} = this.ctx.helper
102 | const {Header, Transaction, BalanceChange} = db
103 | const {in: $in, ne: $ne, gt: $gt} = this.app.Sequelize.Op
104 | let {limit, offset, reversed = true} = this.ctx.state.pagination
105 | let order = reversed ? 'DESC' : 'ASC'
106 |
107 | let totalCount
108 | let transactionIds
109 | let list
110 | if (ids.length === 1) {
111 | let valueFilter = nonZero ? {value: {[$ne]: 0}} : {}
112 | totalCount = await BalanceChange.count({
113 | where: {
114 | addressId: ids[0],
115 | blockHeight: {[$gt]: 0},
116 | ...valueFilter
117 | },
118 | distinct: true,
119 | col: 'transactionId',
120 | transaction: this.ctx.state.transaction
121 | })
122 | if (totalCount === 0) {
123 | return {totalCount: 0, transactions: []}
124 | }
125 | transactionIds = (await BalanceChange.findAll({
126 | where: {addressId: ids[0], ...valueFilter},
127 | attributes: ['transactionId'],
128 | order: [['blockHeight', order], ['indexInBlock', order], ['transactionId', order]],
129 | limit,
130 | offset,
131 | transaction: this.ctx.state.transaction
132 | })).map(({transactionId}) => transactionId)
133 | list = await BalanceChange.findAll({
134 | where: {transactionId: {[$in]: transactionIds}, addressId: ids[0]},
135 | attributes: ['transactionId', 'blockHeight', 'indexInBlock', 'value'],
136 | include: [
137 | {
138 | model: Header,
139 | as: 'header',
140 | required: false,
141 | attributes: ['hash', 'timestamp']
142 | },
143 | {
144 | model: Transaction,
145 | as: 'transaction',
146 | required: true,
147 | attributes: ['id']
148 | }
149 | ],
150 | order: [['blockHeight', order], ['indexInBlock', order], ['transactionId', order]],
151 | transaction: this.ctx.state.transaction
152 | })
153 | } else {
154 | let havingFilter = nonZero ? 'SUM(value) != 0' : null
155 | if (havingFilter) {
156 | let [{count}] = await db.query(sql`
157 | SELECT COUNT(*) AS count FROM (
158 | SELECT transaction_id FROM balance_change
159 | WHERE address_id IN ${ids} AND block_height > 0
160 | GROUP BY transaction_id
161 | HAVING ${{raw: havingFilter}}
162 | ) list
163 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction})
164 | totalCount = count
165 | } else {
166 | totalCount = await BalanceChange.count({
167 | where: {addressId: {[$in]: ids}, blockHeight: {[$gt]: 0}},
168 | distinct: true,
169 | col: 'transactionId',
170 | transaction: this.ctx.state.transaction
171 | })
172 | }
173 | if (totalCount === 0) {
174 | return {totalCount: 0, transactions: []}
175 | }
176 | if (havingFilter) {
177 | transactionIds = (await db.query(sql`
178 | SELECT MIN(block_height) AS block_height, MIN(index_in_block) AS index_in_block, transaction_id AS transactionId
179 | FROM balance_change
180 | WHERE address_id IN ${ids} AND block_height > 0
181 | GROUP BY transaction_id
182 | HAVING ${{raw: havingFilter}}
183 | ORDER BY block_height ${{raw: order}}, index_in_block ${{raw: order}}, transaction_id ${{raw: order}}
184 | LIMIT ${offset}, ${limit}
185 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction})).map(({transactionId}) => transactionId)
186 | } else {
187 | transactionIds = (await BalanceChange.findAll({
188 | where: {addressId: {[$in]: ids}},
189 | attributes: ['transactionId'],
190 | order: [['blockHeight', order], ['indexInBlock', order], ['transactionId', order]],
191 | limit,
192 | offset,
193 | transaction: this.ctx.state.transaction
194 | })).map(({transactionId}) => transactionId)
195 | }
196 | list = await db.query(sql`
197 | SELECT
198 | transaction.id AS id, transaction.block_height AS blockHeight,
199 | transaction.index_in_block AS indexInBlock, transaction._id AS transactionId,
200 | header.hash AS blockHash, header.timestamp AS timestamp,
201 | list.value AS value
202 | FROM (
203 | SELECT MIN(block_height) AS block_height, MIN(index_in_block) AS index_in_block, transaction_id, SUM(value) AS value
204 | FROM balance_change
205 | WHERE transaction_id IN ${transactionIds} AND address_id IN ${ids}
206 | GROUP BY transaction_id
207 | ORDER BY block_height ${{raw: order}}, index_in_block ${{raw: order}}, transaction_id ${{raw: order}}
208 | ) list
209 | INNER JOIN transaction ON transaction._id = list.transaction_id
210 | LEFT JOIN header ON header.height = transaction.block_height
211 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction})
212 | }
213 |
214 | if (reversed) {
215 | list = list.reverse()
216 | }
217 | let initialBalance = 0n
218 | if (list.length > 0) {
219 | let {blockHeight, indexInBlock, transactionId} = list[0]
220 | let [{value}] = await db.query(sql`
221 | SELECT SUM(value) AS value FROM balance_change
222 | WHERE address_id IN ${ids}
223 | AND (block_height, index_in_block, transaction_id) < (${blockHeight}, ${indexInBlock}, ${transactionId})
224 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction})
225 | initialBalance = BigInt(value || 0n)
226 | }
227 | let transactions = list.map(item => ({
228 | id: item.id || item.transaction.id,
229 | ...item.header ? {
230 | block: {
231 | hash: item.header.hash,
232 | height: item.blockHeight,
233 | timestamp: item.header.timestamp
234 | }
235 | } : {},
236 | ...item.blockHash ? {
237 | block: {
238 | hash: item.blockHash,
239 | height: item.blockHeight,
240 | timestamp: item.timestamp
241 | }
242 | } : {},
243 | amount: BigInt(item.value),
244 | }))
245 | for (let tx of transactions) {
246 | tx.balance = initialBalance += tx.amount
247 | }
248 | if (reversed) {
249 | transactions = transactions.reverse()
250 | }
251 | return {totalCount, transactions}
252 | }
253 |
254 | async getRichList() {
255 | const db = this.ctx.model
256 | const {RichList} = db
257 | const {sql} = this.ctx.helper
258 | let {limit, offset} = this.ctx.state.pagination
259 | let totalCount = await RichList.count({transaction: this.ctx.state.transaction})
260 | let list = await db.query(sql`
261 | SELECT address.string AS address, rich_list.balance AS balance
262 | FROM (SELECT address_id FROM rich_list ORDER BY balance DESC LIMIT ${offset}, ${limit}) list
263 | INNER JOIN rich_list USING (address_id)
264 | INNER JOIN address ON address._id = list.address_id
265 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction})
266 | return {
267 | totalCount,
268 | list: list.map(item => ({
269 | address: item.address,
270 | balance: BigInt(item.balance)
271 | }))
272 | }
273 | }
274 |
275 | async updateRichList() {
276 | const db = this.ctx.model
277 | const {Address, RichList} = db
278 | const {sql} = this.ctx.helper
279 | let transaction = await db.transaction()
280 | try {
281 | const blockHeight = this.app.blockchainInfo.tip.height
282 | let list = await db.query(sql`
283 | SELECT list.address_id AS addressId, list.balance AS balance
284 | FROM (
285 | SELECT address_id, SUM(value) AS balance
286 | FROM transaction_output
287 | WHERE
288 | address_id > 0
289 | AND (input_height IS NULL OR input_height > ${blockHeight})
290 | AND (block_height BETWEEN 1 AND ${blockHeight})
291 | AND value > 0
292 | GROUP BY address_id
293 | ) list
294 | INNER JOIN address ON address._id = list.address_id
295 | WHERE address.type < ${Address.parseType('contract')}
296 | `, {type: db.QueryTypes.SELECT, transaction})
297 | await db.query(sql`DELETE FROM rich_list`, {transaction})
298 | await RichList.bulkCreate(
299 | list.map(({addressId, balance}) => ({addressId, balance: BigInt(balance)})),
300 | {validate: false, transaction, logging: false}
301 | )
302 | await transaction.commit()
303 | } catch (err) {
304 | await transaction.rollback()
305 | }
306 | }
307 |
308 | async getBalanceRanking(addressIds) {
309 | if (addressIds.length !== 1) {
310 | return null
311 | }
312 | const {RichList} = this.ctx.model
313 | const {gt: $gt} = this.app.Sequelize.Op
314 | let item = await RichList.findOne({
315 | where: {addressId: addressIds[0]},
316 | attributes: ['balance'],
317 | transaction: this.ctx.state.transaction
318 | })
319 | if (item == null) {
320 | return null
321 | } else {
322 | return await RichList.count({
323 | where: {balance: {[$gt]: item.balance.toString()}},
324 | transaction: this.ctx.state.transaction
325 | }) + 1
326 | }
327 | }
328 | }
329 |
330 | module.exports = BalanceService
331 |
--------------------------------------------------------------------------------
/doc/transaction.md:
--------------------------------------------------------------------------------
1 | # Transaction API
2 |
3 | - [Transaction API](#transaction-api)
4 | - [Transaction Information](#transaction-information)
5 | - [Raw Transaction](#raw-transaction)
6 | - [Send Raw Transaction](#send-raw-transaction)
7 |
8 |
9 | ## Transaction Information
10 |
11 | **Request URL**
12 | ```
13 | GET /tx/:id
14 | ```
15 |
16 | **Request #1**
17 | ```
18 | GET /tx/870c6b51d4160b52ce2bd506d0cd7a8438b8aac9afd03c4695f6ab9648bd02dc
19 | ```
20 |
21 | **Response #1**
22 | ```json
23 | {
24 | "id": "870c6b51d4160b52ce2bd506d0cd7a8438b8aac9afd03c4695f6ab9648bd02dc",
25 | "hash": "35559de74dd0d90908a25d39fd471829e351bffb3a3874bf54764b8d79f923aa",
26 | "version": 2,
27 | "lockTime": 246987,
28 | "blockHash": "8275eb2950279df37b8fa62076249ec7076cc030c860df004b8f3b03071bd00b",
29 | "inputs": [
30 | {
31 | "prevTxId": "14383d782ff38067d928acffabb31b46196966fc4a31d9de855a0cbf0535922d",
32 | "outputIndex": 0,
33 | "value": "910800",
34 | "address": "qc1ql8v7fp2j2uk7gjqv5rudlecv09e9x4jdwya5vd",
35 | "scriptSig": {
36 | "type": "witness_v0_keyhash",
37 | "hex": "",
38 | "asm": ""
39 | },
40 | "sequence": 4294967294,
41 | "witness": [
42 | "3044022030bf256669dd7fc6ca00cd393461c09b25421e59696208e8b5cbdbf71ff5050302200707c5d085d7ed06d1e6af9a3ca1c66b0c2e1a73189b24795afc80c8080236a101",
43 | "038c9dfdb356979a56d4b94063b8fb214295bd6a310b315564083a63795556ae93"
44 | ]
45 | },
46 | {
47 | "prevTxId": "14383d782ff38067d928acffabb31b46196966fc4a31d9de855a0cbf0535922d",
48 | "outputIndex": 1,
49 | "value": "1484306",
50 | "address": "QjHokSvA9ZGuVZhNWPsv2e8wmiLHnusurS",
51 | "scriptSig": {
52 | "type": "pubkeyhash",
53 | "hex": "47304402203db3536426e26c17a670ab3127109809f76ce366a55483a5ef9d556ea432cb240220546ae9f85150b21d3692fd89f61b947840d9b96886f830413e444536e97d3ecb0121027d026b7753f3f70ba972c801e9aed0c715dc68b89447bc0c5c18b32f25f3133c",
54 | "asm": "304402203db3536426e26c17a670ab3127109809f76ce366a55483a5ef9d556ea432cb240220546ae9f85150b21d3692fd89f61b947840d9b96886f830413e444536e97d3ecb[ALL] 027d026b7753f3f70ba972c801e9aed0c715dc68b89447bc0c5c18b32f25f3133c"
55 | },
56 | "sequence": 4294967294,
57 | "witness": []
58 | },
59 | {
60 | "prevTxId": "2e1a1985c50a33e342990a475888d74b79e3b122326fc585342a39c71cb5a76e",
61 | "outputIndex": 0,
62 | "value": "1910800",
63 | "address": "qc1ql8v7fp2j2uk7gjqv5rudlecv09e9x4jdwya5vd",
64 | "scriptSig": {
65 | "type": "witness_v0_keyhash",
66 | "hex": "",
67 | "asm": ""
68 | },
69 | "sequence": 4294967294,
70 | "witness": [
71 | "30440220301dafcfe858ca57a1500b64f4dc3331fbf65670169362f4add3204c05b0ccf302207090d1047ee3c509b58d88734fa11afe1c1933ac8026c42ce745c46f30df9f8401",
72 | "038c9dfdb356979a56d4b94063b8fb214295bd6a310b315564083a63795556ae93"
73 | ]
74 | }
75 | ],
76 | "outputs": [
77 | {
78 | "value": "305906",
79 | "address": "qc1ql8v7fp2j2uk7gjqv5rudlecv09e9x4jdwya5vd",
80 | "scriptPubKey": {
81 | "type": "witness_v0_keyhash",
82 | "hex": "0014f9d9e48552572de4480ca0f8dfe70c797253564d",
83 | "asm": "OP_0 f9d9e48552572de4480ca0f8dfe70c797253564d"
84 | },
85 | "spentTxId": "adcf52f3b284195fa8a7b2a4664252f3959ae618c321b35e1deffadec717f3dd",
86 | "spentIndex": 0
87 | },
88 | {
89 | "value": "3857200",
90 | "address": "qc1ql8v7fp2j2uk7gjqv5rudlecv09e9x4jdwya5vd",
91 | "scriptPubKey": {
92 | "type": "witness_v0_keyhash",
93 | "hex": "0014f9d9e48552572de4480ca0f8dfe70c797253564d",
94 | "asm": "OP_0 f9d9e48552572de4480ca0f8dfe70c797253564d"
95 | },
96 | "spentTxId": "adcf52f3b284195fa8a7b2a4664252f3959ae618c321b35e1deffadec717f3dd",
97 | "spentIndex": 1
98 | }
99 | ],
100 | "isCoinbase": false,
101 | "isCoinstake": false,
102 | "blockHeight": 246989,
103 | "confirmations": 159098,
104 | "timestamp": 1539787024,
105 | "inputValue": "4305906",
106 | "outputValue": "4163106",
107 | "refundValue": "0",
108 | "fees": "142800",
109 | "size": 518,
110 | "weight": 1421
111 | }
112 | ```
113 |
114 | **Request #2**
115 | ```
116 | GET /tx/f56ea462337e4732e821eb7ceee5208a5c807fe5f918a342298eb152d75765ee
117 | ```
118 |
119 | **Response #2**
120 | ```json
121 | {
122 | "id": "f56ea462337e4732e821eb7ceee5208a5c807fe5f918a342298eb152d75765ee",
123 | "hash": "f56ea462337e4732e821eb7ceee5208a5c807fe5f918a342298eb152d75765ee",
124 | "version": 2,
125 | "lockTime": 168583,
126 | "blockHash": "67833537107b014a9e3a1666c99f02708a788f81a942a6fa500a0e7c5e5446c4",
127 | "inputs": [
128 | {
129 | "prevTxId": "732fe02e70095557854e419d472388cccc3f8fc00db4b9b1820b9a51c0f1b905",
130 | "outputIndex": 1,
131 | "value": "500000000000",
132 | "address": "QccDD4Vk5Tc5Y84ydAcj4hpNkahYcsCsRq",
133 | "scriptSig": {
134 | "type": "pubkeyhash",
135 | "hex": "483045022100cc4208e82c8d6aadbb6e5ed1465ca1675db472f9e72cef528d52973e632bbafc02202d2a894267a2d3cd1e647ef3a3206d83228c71006a4c3544616dcb58d0b9dbf9012103afb25cf82520925420f07f4b13b17efa0d7868606c66adf7979e15fb3f21721e",
136 | "asm": "3045022100cc4208e82c8d6aadbb6e5ed1465ca1675db472f9e72cef528d52973e632bbafc02202d2a894267a2d3cd1e647ef3a3206d83228c71006a4c3544616dcb58d0b9dbf9[ALL] 03afb25cf82520925420f07f4b13b17efa0d7868606c66adf7979e15fb3f21721e"
137 | },
138 | "sequence": 4294967294
139 | }
140 | ],
141 | "outputs": [
142 | {
143 | "value": "399989865575",
144 | "address": "QccDD4Vk5Tc5Y84ydAcj4hpNkahYcsCsRq",
145 | "scriptPubKey": {
146 | "type": "pubkeyhash",
147 | "hex": "76a914af8cf283ef7d1ad9fa824bd4de564f3b1b9fcd7a88ac",
148 | "asm": "OP_DUP OP_HASH160 af8cf283ef7d1ad9fa824bd4de564f3b1b9fcd7a OP_EQUALVERIFY OP_CHECKSIG"
149 | },
150 | "spentTxId": "eae244f9996cf4b50bf3c35d001aa1d8f60fbd9b7da9e0a37ad4ebb336082a6e",
151 | "spentIndex": 0
152 | },
153 | {
154 | "value": "100000000000",
155 | "address": "0439fcc94493859d9146b6b9a92daa6d6d7b581d",
156 | "addressHex": "0439fcc94493859d9146b6b9a92daa6d6d7b581d",
157 | "scriptPubKey": {
158 | "type": "evm_call",
159 | "hex": "01040390d003012824d0821b0e0000000000000000000000000000000000000000000000000000000000000001140439fcc94493859d9146b6b9a92daa6d6d7b581dc2",
160 | "asm": "4 250000 40 d0821b0e0000000000000000000000000000000000000000000000000000000000000001 0439fcc94493859d9146b6b9a92daa6d6d7b581d OP_CALL"
161 | },
162 | "spentTxId": "4f263d6cfa910e5b763edb1349d34d9a0c97e1ad5a3eb9fd6b2347155da5a246",
163 | "spentIndex": 1,
164 | "receipt": {
165 | "sender": "QccDD4Vk5Tc5Y84ydAcj4hpNkahYcsCsRq",
166 | "gasUsed": 94008,
167 | "contractAddress": "0439fcc94493859d9146b6b9a92daa6d6d7b581d",
168 | "contractAddressHex": "0439fcc94493859d9146b6b9a92daa6d6d7b581d",
169 | "excepted": "None",
170 | "exceptedMessage": "",
171 | "logs": [
172 | {
173 | "address": "0439fcc94493859d9146b6b9a92daa6d6d7b581d",
174 | "addressHex": "0439fcc94493859d9146b6b9a92daa6d6d7b581d",
175 | "topics": [
176 | "fb425c0bd6840437c799f5176836b0ebc76d79351a6981cc4e5fbb0cdbf3e185",
177 | "0000000000000000000000000000000000000000000000000000000000000000",
178 | "0000000000000000000000000439fcc94493859d9146b6b9a92daa6d6d7b581d",
179 | "000000000000000000000000af8cf283ef7d1ad9fa824bd4de564f3b1b9fcd7a"
180 | ],
181 | "data": "0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000174876e8005154554d00000000000000000000000000000000000000000000000000000000"
182 | }
183 | ]
184 | }
185 | }
186 | ],
187 | "isCoinbase": false,
188 | "isCoinstake": false,
189 | "blockHeight": 168584,
190 | "confirmations": 237507,
191 | "timestamp": 1528460544,
192 | "inputValue": "500000000000",
193 | "outputValue": "499989865575",
194 | "refundValue": "6239680",
195 | "fees": "3894745",
196 | "size": 268,
197 | "weight": 1072,
198 | "contractSpends": [
199 | {
200 | "inputs": [
201 | {
202 | "address": "056168620105d8f73a55d8c6542b565aea3665ec",
203 | "addressHex": "056168620105d8f73a55d8c6542b565aea3665ec",
204 | "value": "202560000000"
205 | },
206 | {
207 | "address": "0439fcc94493859d9146b6b9a92daa6d6d7b581d",
208 | "addressHex": "0439fcc94493859d9146b6b9a92daa6d6d7b581d",
209 | "value": "100000000000"
210 | }
211 | ],
212 | "outputs": [
213 | {
214 | "address": "056168620105d8f73a55d8c6542b565aea3665ec",
215 | "addressHex": "056168620105d8f73a55d8c6542b565aea3665ec",
216 | "value": "302560000000"
217 | }
218 | ]
219 | }
220 | ]
221 | }
222 | ```
223 |
224 | **Request #3**
225 | ```
226 | GET /tx/ebf05fbf8dcf10f03a73331abd1ea934d66c03a72ee4a57addeab0225ad9289f
227 | ```
228 |
229 | **Response #3**
230 | ```json
231 | {
232 | "id": "ebf05fbf8dcf10f03a73331abd1ea934d66c03a72ee4a57addeab0225ad9289f",
233 | "hash": "ebf05fbf8dcf10f03a73331abd1ea934d66c03a72ee4a57addeab0225ad9289f",
234 | "version": 2,
235 | "lockTime": 406076,
236 | "blockHash": "7fae36223bbd95b82d9899b767d4289d4fe4cd2b5ee6e248c16694d7352b3b94",
237 | "inputs": [
238 | {
239 | "prevTxId": "6ae7a9cf0763b13185b23f345180607caa71c1cacff9ef6e4a9f9c068bd45c62",
240 | "outputIndex": 1,
241 | "value": "97820400",
242 | "address": "QeZjXLEyqKgUNyoXcTCC2nxVx77cSnmpcy",
243 | "scriptSig": {
244 | "type": "pubkeyhash",
245 | "hex": "47304402206897a7a9502314e3d5f0d62e2fae485746cbbd9524229ab42187f5a766c9ec1d02206701c50e1947dd08c4440db66bf8a534e4a2c95a0779cf1963c229a4ebcc9bda012103d2afef396be37192a1137fe103ace0dd2e861088d0634ffd792d4d43a8bed770",
246 | "asm": "304402206897a7a9502314e3d5f0d62e2fae485746cbbd9524229ab42187f5a766c9ec1d02206701c50e1947dd08c4440db66bf8a534e4a2c95a0779cf1963c229a4ebcc9bda[ALL] 03d2afef396be37192a1137fe103ace0dd2e861088d0634ffd792d4d43a8bed770"
247 | },
248 | "sequence": 4294967294
249 | }
250 | ],
251 | "outputs": [
252 | {
253 | "value": "0",
254 | "address": "EfDYuWmSUbZPaAe2qzeWurcDGobSnhYa6F",
255 | "addressHex": "f2033ede578e17fa6231047265010445bca8cf1c",
256 | "scriptPubKey": {
257 | "type": "evm_call",
258 | "hex": "0104032cc900012844a9059cbb000000000000000000000000bf4e5cb019865cde870642bf2a2dfb375789c23b00000000000000000000000000000000000000000000000000000002540be40014f2033ede578e17fa6231047265010445bca8cf1cc2",
259 | "asm": "4 51500 40 a9059cbb000000000000000000000000bf4e5cb019865cde870642bf2a2dfb375789c23b00000000000000000000000000000000000000000000000000000002540be400 f2033ede578e17fa6231047265010445bca8cf1c OP_CALL"
260 | },
261 | "receipt": {
262 | "sender": "QeZjXLEyqKgUNyoXcTCC2nxVx77cSnmpcy",
263 | "gasUsed": 36359,
264 | "contractAddress": "f2033ede578e17fa6231047265010445bca8cf1c",
265 | "contractAddressHex": "f2033ede578e17fa6231047265010445bca8cf1c",
266 | "excepted": "None",
267 | "exceptedMessage": "",
268 | "logs": [
269 | {
270 | "address": "f2033ede578e17fa6231047265010445bca8cf1c",
271 | "addressHex": "f2033ede578e17fa6231047265010445bca8cf1c",
272 | "topics": [
273 | "ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
274 | "000000000000000000000000c50541b82f4cda2f61cbbc219595c79ffdb4f2fa",
275 | "000000000000000000000000bf4e5cb019865cde870642bf2a2dfb375789c23b"
276 | ],
277 | "data": "00000000000000000000000000000000000000000000000000000002540be400"
278 | }
279 | ]
280 | }
281 | },
282 | {
283 | "value": "95640800",
284 | "address": "QeZjXLEyqKgUNyoXcTCC2nxVx77cSnmpcy",
285 | "scriptPubKey": {
286 | "type": "pubkeyhash",
287 | "hex": "76a914c50541b82f4cda2f61cbbc219595c79ffdb4f2fa88ac",
288 | "asm": "OP_DUP OP_HASH160 c50541b82f4cda2f61cbbc219595c79ffdb4f2fa OP_EQUALVERIFY OP_CHECKSIG"
289 | }
290 | }
291 | ],
292 | "isCoinbase": false,
293 | "isCoinstake": false,
294 | "blockHeight": 406077,
295 | "confirmations": 16,
296 | "timestamp": 1562675696,
297 | "inputValue": "97820400",
298 | "outputValue": "95640800",
299 | "refundValue": "605640",
300 | "fees": "1573960",
301 | "size": 299,
302 | "weight": 1196,
303 | "qrc20TokenTransfers": [
304 | {
305 | "address": "f2033ede578e17fa6231047265010445bca8cf1c",
306 | "addressHex": "f2033ede578e17fa6231047265010445bca8cf1c",
307 | "name": "QCASH",
308 | "symbol": "QC",
309 | "decimals": 8,
310 | "from": "QeZjXLEyqKgUNyoXcTCC2nxVx77cSnmpcy",
311 | "to": "Qe3X3dVeVocnd6f9rsCVJyTmW1Usc8YhLE",
312 | "value": "10000000000"
313 | }
314 | ]
315 | }
316 | ```
317 |
318 |
319 | ## Raw Transaction
320 |
321 | **Request URL**
322 | ```
323 | GET /raw-tx/:id
324 | ```
325 |
326 | **Request**
327 | ```
328 | GET /tx/ebf05fbf8dcf10f03a73331abd1ea934d66c03a72ee4a57addeab0225ad9289f
329 | ```
330 |
331 | **Response**
332 | ```
333 | 0200000001625cd48b069c9f4a6eeff9cfcac171aa7c608051343fb28531b16307cfa9e76a010000006a47304402206897a7a9502314e3d5f0d62e2fae485746cbbd9524229ab42187f5a766c9ec1d02206701c50e1947dd08c4440db66bf8a534e4a2c95a0779cf1963c229a4ebcc9bda012103d2afef396be37192a1137fe103ace0dd2e861088d0634ffd792d4d43a8bed770feffffff020000000000000000630104032cc900012844a9059cbb000000000000000000000000bf4e5cb019865cde870642bf2a2dfb375789c23b00000000000000000000000000000000000000000000000000000002540be40014f2033ede578e17fa6231047265010445bca8cf1cc2e05cb305000000001976a914c50541b82f4cda2f61cbbc219595c79ffdb4f2fa88ac3c320600
334 | ```
335 |
336 |
337 | ## Send Raw Transaction
338 |
339 | **Request URL**
340 | ```
341 | POST /tx/send
342 | ```
343 |
344 | **Request Parameters**
345 | | Name | Type | Description |
346 | | - | - | - |
347 | | `rawtx` | String | Raw Transaction Hex String |
348 |
349 |
350 |
351 | | Name |
352 | Type |
353 | Description |
354 |
355 |
356 |
357 |
358 |
359 | rawtx
360 | |
361 |
362 | String
363 | |
364 | Raw Transaction Hex String |
365 |
366 |
367 |
368 |
369 | **Request**
370 | ```
371 | POST /tx/send
372 | ```
373 |
374 | **Request Body**
375 | ```
376 | rawtx=02000000014727d9d3560b94b0cf1c10daea43920c24fa8451a75f2d5f69f6f585726fcb15000000006a47304402202ac3b7fd62837722fd4ab1dfdfe7d069dcaad348c7a039ed3cfc473ef435a167022033ae815fbcd8c04b2fb98d611a395dd96da757cae94bca780010bd9db6bda7230121027f2fac6638798fe79696bffba971976b325753541d24fc0a920b617b5f23815bfeffffff02c00064f6070000001976a9140588712645b0c536d59c9d7198f492cf6d2eb3cf88ac263f61e9030000001976a91421b92d11d3b7cbe5e76252eab7db8bd5ac47a0ae88ac781a0600
377 | ```
378 |
379 | **Response**
380 | ```json
381 | {
382 | "status": 0,
383 | "id": "56daa4ae91c07b84aad5bcab74bfe0c12e14b228ed755c2aaabc6b027f8698a0"
384 | }
385 | /// or
386 | {
387 | "status": 1,
388 | "message": "{error message}"
389 | }
390 | ```
391 |
--------------------------------------------------------------------------------
/app/service/block.js:
--------------------------------------------------------------------------------
1 | const {Service} = require('egg')
2 |
3 | class BlockService extends Service {
4 | async getBlock(arg) {
5 | const {Header, Address, Block, Transaction} = this.ctx.model
6 | let filter
7 | if (Number.isInteger(arg)) {
8 | filter = {height: arg}
9 | } else if (Buffer.isBuffer(arg)) {
10 | filter = {hash: arg}
11 | } else {
12 | return null
13 | }
14 | let result = await Header.findOne({
15 | where: filter,
16 | include: [{
17 | model: Block,
18 | as: 'block',
19 | required: true,
20 | attributes: ['size', 'weight'],
21 | include: [{
22 | model: Address,
23 | as: 'miner',
24 | attributes: ['string']
25 | }]
26 | }],
27 | transaction: this.ctx.state.transaction
28 | })
29 | if (!result) {
30 | return null
31 | }
32 | let [prevHeader, nextHeader, transactions, [reward]] = await Promise.all([
33 | Header.findOne({
34 | where: {height: result.height - 1},
35 | attributes: ['timestamp'],
36 | transaction: this.ctx.state.transaction
37 | }),
38 | Header.findOne({
39 | where: {height: result.height + 1},
40 | attributes: ['hash'],
41 | transaction: this.ctx.state.transaction
42 | }),
43 | Transaction.findAll({
44 | where: {blockHeight: result.height},
45 | attributes: ['id'],
46 | order: [['indexInBlock', 'ASC']],
47 | transaction: this.ctx.state.transaction
48 | }),
49 | this.getBlockRewards(result.height)
50 | ])
51 | return {
52 | hash: result.hash,
53 | height: result.height,
54 | version: result.version,
55 | prevHash: result.prevHash,
56 | nextHash: nextHeader && nextHeader.hash,
57 | merkleRoot: result.merkleRoot,
58 | timestamp: result.timestamp,
59 | bits: result.bits,
60 | nonce: result.nonce,
61 | hashStateRoot: result.hashStateRoot,
62 | hashUTXORoot: result.hashUTXORoot,
63 | stakePrevTxId: result.stakePrevTxId,
64 | stakeOutputIndex: result.stakeOutputIndex,
65 | signature: result.signature,
66 | chainwork: result.chainwork,
67 | proofOfStake: result.isProofOfStake(),
68 | interval: result.height > 0 ? result.timestamp - prevHeader.timestamp : null,
69 | size: result.block.size,
70 | weight: result.block.weight,
71 | transactions: transactions.map(tx => tx.id),
72 | miner: result.block.miner.string,
73 | difficulty: result.difficulty,
74 | reward,
75 | confirmations: this.app.blockchainInfo.tip.height - result.height + 1
76 | }
77 | }
78 |
79 | async getRawBlock(arg) {
80 | const {Header, Transaction} = this.ctx.model
81 | const {Header: RawHeader, Block: RawBlock} = this.app.qtuminfo.lib
82 | let filter
83 | if (Number.isInteger(arg)) {
84 | filter = {height: arg}
85 | } else if (Buffer.isBuffer(arg)) {
86 | filter = {hash: arg}
87 | } else {
88 | return null
89 | }
90 | let block = await Header.findOne({where: filter, transaction: this.ctx.state.transaction})
91 | if (!block) {
92 | return null
93 | }
94 | let transactionIds = (await Transaction.findAll({
95 | where: {blockHeight: block.height},
96 | attributes: ['id'],
97 | order: [['indexInBlock', 'ASC']],
98 | transaction: this.ctx.state.transaction
99 | })).map(tx => tx.id)
100 | let transactions = await Promise.all(transactionIds.map(id => this.ctx.service.transaction.getRawTransaction(id)))
101 | return new RawBlock({
102 | header: new RawHeader({
103 | version: block.version,
104 | prevHash: block.prevHash,
105 | merkleRoot: block.merkleRoot,
106 | timestamp: block.timestamp,
107 | bits: block.bits,
108 | nonce: block.nonce,
109 | hashStateRoot: block.hashStateRoot,
110 | hashUTXORoot: block.hashUTXORoot,
111 | stakePrevTxId: block.stakePrevTxId,
112 | stakeOutputIndex: block.stakeOutputIndex,
113 | signature: block.signature
114 | }),
115 | transactions
116 | })
117 | }
118 |
119 | async listBlocks(dateFilter) {
120 | const db = this.ctx.model
121 | const {sql} = this.ctx.helper
122 | let dateFilterString = ''
123 | if (dateFilter) {
124 | dateFilterString = sql`AND timestamp BETWEEN ${dateFilter.min} AND ${dateFilter.max - 1}`
125 | }
126 | let [{totalCount}] = await db.query(sql`
127 | SELECT COUNT(*) AS totalCount FROM header WHERE height <= ${this.app.blockchainInfo.tip.height} ${{raw: dateFilterString}}
128 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction})
129 | let blocks
130 | if (this.ctx.state.pagination) {
131 | let {limit, offset} = this.ctx.state.pagination
132 | blocks = await db.query(sql`
133 | SELECT
134 | header.hash AS hash, l.height AS height, header.timestamp AS timestamp,
135 | block.size AS size, address.string AS miner
136 | FROM (
137 | SELECT height FROM header
138 | WHERE height <= ${this.app.blockchainInfo.tip.height} ${{raw: dateFilterString}}
139 | ORDER BY height DESC
140 | LIMIT ${offset}, ${limit}
141 | ) l, header, block, address
142 | WHERE l.height = header.height AND l.height = block.height AND address._id = block.miner_id
143 | ORDER BY l.height ASC
144 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction})
145 | } else {
146 | blocks = await db.query(sql`
147 | SELECT
148 | header.hash AS hash, l.height AS height, header.timestamp AS timestamp,
149 | block.size AS size, address.string AS miner
150 | FROM (
151 | SELECT height FROM header
152 | WHERE height <= ${this.app.blockchainInfo.tip.height} ${{raw: dateFilterString}}
153 | ORDER BY height DESC
154 | ) l, header, block, address
155 | WHERE l.height = header.height AND l.height = block.height AND address._id = block.miner_id
156 | ORDER BY l.height ASC
157 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction})
158 | }
159 | if (blocks.length === 0) {
160 | return {totalCount, blocks: []}
161 | } else {
162 | return {totalCount, blocks: await this.getBlockSummary(blocks)}
163 | }
164 | }
165 |
166 | async getRecentBlocks(count) {
167 | const db = this.ctx.model
168 | const {sql} = this.ctx.helper
169 | let blocks = await db.query(sql`
170 | SELECT
171 | l.hash AS hash, l.height AS height, header.timestamp AS timestamp,
172 | l.size AS size, address.string AS miner
173 | FROM (
174 | SELECT hash, height, size, miner_id FROM block
175 | ORDER BY height DESC
176 | LIMIT ${count}
177 | ) l, header, address WHERE l.height = header.height AND l.miner_id = address._id
178 | ORDER BY l.height DESC
179 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction})
180 | if (blocks.length === 0) {
181 | return []
182 | }
183 | blocks.reverse()
184 | return await this.getBlockSummary(blocks)
185 | }
186 |
187 | async getBlockRewards(startHeight, endHeight = startHeight + 1) {
188 | const db = this.ctx.model
189 | const {sql} = this.ctx.helper
190 | let rewards = await db.query(sql`
191 | SELECT SUM(value) AS value FROM (
192 | SELECT tx.block_height AS height, output.value AS value FROM header, transaction tx, transaction_output output
193 | WHERE
194 | tx.block_height BETWEEN ${startHeight} AND ${endHeight - 1}
195 | AND header.height = tx.block_height
196 | AND tx.index_in_block = (SELECT CASE header.stake_prev_transaction_id WHEN ${Buffer.alloc(32)} THEN 0 ELSE 1 END)
197 | AND output.transaction_id = tx._id
198 | AND NOT EXISTS (
199 | SELECT refund_id FROM gas_refund
200 | WHERE refund_id = output.transaction_id AND refund_index = output.output_index
201 | )
202 | UNION ALL
203 | SELECT tx.block_height AS height, -input.value AS value
204 | FROM header, transaction tx, transaction_input input
205 | WHERE
206 | tx.block_height BETWEEN ${startHeight} AND ${endHeight - 1}
207 | AND header.height = tx.block_height
208 | AND tx.index_in_block = (SELECT CASE header.stake_prev_transaction_id WHEN ${Buffer.alloc(32)} THEN 0 ELSE 1 END)
209 | AND input.transaction_id = tx._id
210 | ) block_reward
211 | GROUP BY height
212 | ORDER BY height ASC
213 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction})
214 | let result = rewards.map(reward => BigInt(reward.value))
215 | if (startHeight[0] === 0) {
216 | result[0] = 0n
217 | }
218 | return result
219 | }
220 |
221 | async getBlockSummary(blocks) {
222 | const db = this.ctx.model
223 | const {Header} = db
224 | const {sql} = this.ctx.helper
225 | let transactionCountMapping = new Map(
226 | (await db.query(sql`
227 | SELECT block.height AS height, MAX(transaction.index_in_block) + 1 AS transactionsCount
228 | FROM block
229 | INNER JOIN transaction ON block.height = transaction.block_height
230 | WHERE block.height BETWEEN ${blocks[0].height} AND ${blocks[blocks.length - 1].height}
231 | GROUP BY block.height
232 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction}))
233 | .map(({height, transactionsCount}) => [height, transactionsCount])
234 | )
235 | let [prevHeader, rewards] = await Promise.all([
236 | Header.findOne({
237 | where: {height: blocks[0].height - 1},
238 | attributes: ['timestamp'],
239 | transaction: this.ctx.state.transaction
240 | }),
241 | this.getBlockRewards(blocks[0].height, blocks[blocks.length - 1].height + 1)
242 | ])
243 | let result = []
244 | for (let i = blocks.length; --i >= 0;) {
245 | let block = blocks[i]
246 | let interval
247 | if (i === 0) {
248 | interval = prevHeader ? block.timestamp - prevHeader.timestamp : null
249 | } else {
250 | interval = block.timestamp - blocks[i - 1].timestamp
251 | }
252 | result.push({
253 | hash: block.hash,
254 | height: block.height,
255 | timestamp: block.timestamp,
256 | transactionsCount: transactionCountMapping.get(block.height),
257 | interval,
258 | size: block.size,
259 | miner: block.miner,
260 | reward: rewards[i]
261 | })
262 | }
263 | return result
264 | }
265 |
266 | async getBiggestMiners(lastNBlocks) {
267 | const fromBlock = this.app.chain.lastPoWBlockHeight >= 0xffffffff ? this.app.chain.lastPoWBlockHeight : 1
268 | const db = this.ctx.model
269 | const {sql} = this.ctx.helper
270 | const {Block} = db
271 | const {gte: $gte} = this.app.Sequelize.Op
272 | let fromBlockHeight = lastNBlocks == null ? fromBlock : Math.max(this.app.blockchainInfo.height - lastNBlocks + 1, fromBlock)
273 | let {limit, offset} = this.ctx.state.pagination
274 | let totalCount = await Block.count({
275 | where: {height: {[$gte]: fromBlockHeight}},
276 | distinct: true,
277 | col: 'minerId',
278 | transaction: this.ctx.state.transaction
279 | })
280 | let list = await db.query(sql`
281 | SELECT address.string AS address, list.blocks AS blocks, rich_list.balance AS balance FROM (
282 | SELECT miner_id, COUNT(*) AS blocks FROM block
283 | WHERE height >= ${fromBlockHeight}
284 | GROUP BY miner_id
285 | ORDER BY blocks DESC
286 | LIMIT ${offset}, ${limit}
287 | ) list
288 | INNER JOIN address ON address._id = list.miner_id
289 | LEFT JOIN rich_list ON rich_list.address_id = address._id
290 | ORDER BY blocks DESC
291 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction})
292 | return {
293 | totalCount,
294 | list: list.map(({address, blocks, balance}) => ({address, blocks, balance: BigInt(balance || 0)}))
295 | }
296 | }
297 |
298 | async getBlockTransactions(height) {
299 | const {Transaction} = this.ctx.model
300 | let transactions = await Transaction.findAll({
301 | where: {blockHeight: height},
302 | attributes: ['id'],
303 | transaction: this.ctx.state.transaction
304 | })
305 | return transactions.map(tx => tx.id)
306 | }
307 |
308 | async getBlockAddressTransactions(height) {
309 | const {Address, Transaction, BalanceChange, EvmReceipt: EVMReceipt, EvmReceiptLog: EVMReceiptLog, Contract} = this.ctx.model
310 | const {Address: RawAddress} = this.app.qtuminfo.lib
311 | const TransferABI = this.app.qtuminfo.lib.Solidity.qrc20ABIs.find(abi => abi.name === 'Transfer')
312 | let result = []
313 | let balanceChanges = await BalanceChange.findAll({
314 | attributes: [],
315 | include: [
316 | {
317 | model: Transaction,
318 | as: 'transaction',
319 | required: true,
320 | where: {blockHeight: height},
321 | attributes: ['indexInBlock']
322 | },
323 | {
324 | model: Address,
325 | as: 'address',
326 | required: true,
327 | attributes: ['string']
328 | }
329 | ]
330 | })
331 | for (let {transaction, address} of balanceChanges) {
332 | result[transaction.indexInBlock] = result[transaction.indexInBlock] || new Set()
333 | result[transaction.indexInBlock].add(address.string)
334 | }
335 | let receipts = await EVMReceipt.findAll({
336 | where: {blockHeight: height},
337 | attributes: ['indexInBlock', 'senderType', 'senderData']
338 | })
339 | for (let {indexInBlock, senderType, senderData} of receipts) {
340 | result[indexInBlock] = result[indexInBlock] || new Set()
341 | result[indexInBlock].add(new RawAddress({type: senderType, data: senderData, chain: this.app.chain}).toString())
342 | }
343 | let receiptLogs = await EVMReceiptLog.findAll({
344 | attributes: ['topic1', 'topic2', 'topic3', 'topic4'],
345 | include: [
346 | {
347 | model: EVMReceipt,
348 | as: 'receipt',
349 | required: true,
350 | where: {blockHeight: height},
351 | attributes: ['indexInBlock']
352 | },
353 | {
354 | model: Contract,
355 | as: 'contract',
356 | required: true,
357 | attributes: ['addressString', 'type']
358 | }
359 | ]
360 | })
361 | for (let {topic1, topic2, topic3, topic4, receipt, contract} of receiptLogs) {
362 | let set = result[receipt.indexInBlock] = result[receipt.indexInBlock] || new Set()
363 | set.add(contract.addressString)
364 | if (Buffer.compare(topic1, TransferABI.id) === 0 && topic3) {
365 | if (contract.type === 'qrc20' && !topic4 || contract.type === 'qrc721' && topic4) {
366 | let sender = topic2.slice(12)
367 | let receiver = topic3.slice(12)
368 | if (Buffer.compare(sender, Buffer.alloc(20)) !== 0) {
369 | set.add(new RawAddress({type: Address.PAY_TO_PUBLIC_KEY_HASH, data: sender, chain: this.app.chain}).toString())
370 | set.add(new RawAddress({type: Address.EVM_CONTRACT, data: sender, chain: this.app.chain}).toString())
371 | }
372 | if (Buffer.compare(receiver, Buffer.alloc(20)) !== 0) {
373 | set.add(new RawAddress({type: Address.PAY_TO_PUBLIC_KEY_HASH, data: receiver, chain: this.app.chain}).toString())
374 | set.add(new RawAddress({type: Address.EVM_CONTRACT, data: receiver, chain: this.app.chain}).toString())
375 | }
376 | }
377 | }
378 | }
379 | return result
380 | }
381 |
382 | getBlockFilter(category = 'blockHeight') {
383 | const {gte: $gte, lte: $lte, between: $between} = this.app.Sequelize.Op
384 | let {fromBlock, toBlock} = this.ctx.state
385 | let blockFilter = null
386 | if (fromBlock != null && toBlock != null) {
387 | blockFilter = {[$between]: [fromBlock, toBlock]}
388 | } else if (fromBlock != null) {
389 | blockFilter = {[$gte]: fromBlock}
390 | } else if (toBlock != null) {
391 | blockFilter = {[$lte]: toBlock}
392 | }
393 | return blockFilter ? {[category]: blockFilter} : {}
394 | }
395 |
396 | getRawBlockFilter(category = 'block_height') {
397 | const {sql} = this.ctx.helper
398 | let {fromBlock, toBlock} = this.ctx.state
399 | let blockFilter = 'TRUE'
400 | if (fromBlock != null && toBlock != null) {
401 | blockFilter = sql`${{raw: category}} BETWEEN ${fromBlock} AND ${toBlock}`
402 | } else if (fromBlock != null) {
403 | blockFilter = sql`${{raw: category}} >= ${fromBlock}`
404 | } else if (toBlock != null) {
405 | blockFilter = sql`${{raw: category}} <= ${toBlock}`
406 | }
407 | return {raw: blockFilter}
408 | }
409 | }
410 |
411 | module.exports = BlockService
412 |
--------------------------------------------------------------------------------
/app/service/contract.js:
--------------------------------------------------------------------------------
1 | const {Service} = require('egg')
2 |
3 | class ContractService extends Service {
4 | async getContractAddresses(list) {
5 | const {Address} = this.app.qtuminfo.lib
6 | const chain = this.app.chain
7 | const {Contract} = this.ctx.model
8 |
9 | let result = []
10 | for (let item of list) {
11 | let rawAddress
12 | try {
13 | rawAddress = Address.fromString(item, chain)
14 | } catch (err) {
15 | this.ctx.throw(400)
16 | }
17 | let filter
18 | if (rawAddress.type === Address.CONTRACT) {
19 | filter = {address: Buffer.from(item, 'hex')}
20 | } else if (rawAddress.type === Address.EVM_CONTRACT) {
21 | filter = {addressString: item}
22 | } else {
23 | this.ctx.throw(400)
24 | }
25 | let contractResult = await Contract.findOne({
26 | where: filter,
27 | attributes: ['address', 'addressString', 'vm', 'type'],
28 | transaction: this.ctx.state.transaction
29 | })
30 | this.ctx.assert(contractResult, 404)
31 | result.push(contractResult.address)
32 | }
33 | return result
34 | }
35 |
36 | async getContractSummary(contractAddress, addressIds) {
37 | const {Contract, Qrc20: QRC20, Qrc20Statistics: QRC20Statistics, Qrc721: QRC721} = this.ctx.model
38 | const {balance: balanceService, qrc20: qrc20Service, qrc721: qrc721Service} = this.ctx.service
39 | let contract = await Contract.findOne({
40 | where: {address: contractAddress},
41 | attributes: ['addressString', 'vm', 'type'],
42 | include: [
43 | {
44 | model: QRC20,
45 | as: 'qrc20',
46 | required: false,
47 | attributes: ['name', 'symbol', 'decimals', 'totalSupply', 'version'],
48 | include: [{
49 | model: QRC20Statistics,
50 | as: 'statistics',
51 | required: true
52 | }]
53 | },
54 | {
55 | model: QRC721,
56 | as: 'qrc721',
57 | required: false,
58 | attributes: ['name', 'symbol', 'totalSupply']
59 | }
60 | ],
61 | transaction: this.ctx.state.transaction
62 | })
63 | let [
64 | {totalReceived, totalSent},
65 | unconfirmed,
66 | qrc20Balances,
67 | qrc721Balances,
68 | transactionCount
69 | ] = await Promise.all([
70 | balanceService.getTotalBalanceChanges(addressIds),
71 | balanceService.getUnconfirmedBalance(addressIds),
72 | qrc20Service.getAllQRC20Balances([contractAddress]),
73 | qrc721Service.getAllQRC721Balances([contractAddress]),
74 | this.getContractTransactionCount(contractAddress, addressIds)
75 | ])
76 | return {
77 | address: contractAddress.toString('hex'),
78 | addressHex: contractAddress,
79 | vm: contract.vm,
80 | type: contract.type,
81 | ...contract.type === 'qrc20' ? {
82 | qrc20: {
83 | name: contract.qrc20.name,
84 | symbol: contract.qrc20.symbol,
85 | decimals: contract.qrc20.decimals,
86 | totalSupply: contract.qrc20.totalSupply,
87 | version: contract.qrc20.version,
88 | holders: contract.qrc20.statistics.holders,
89 | transactions: contract.qrc20.statistics.transactions
90 | }
91 | } : {},
92 | ...contract.type === 'qrc721' ? {
93 | qrc721: {
94 | name: contract.qrc721.name,
95 | symbol: contract.qrc721.symbol,
96 | totalSupply: contract.qrc721.totalSupply
97 | }
98 | } : {},
99 | balance: totalReceived - totalSent,
100 | totalReceived,
101 | totalSent,
102 | unconfirmed,
103 | qrc20Balances,
104 | qrc721Balances,
105 | transactionCount
106 | }
107 | }
108 |
109 | async getContractTransactionCount(contractAddress, addressIds) {
110 | const TransferABI = this.app.qtuminfo.lib.Solidity.qrc20ABIs.find(abi => abi.name === 'Transfer')
111 | const db = this.ctx.model
112 | let {sql} = this.ctx.helper
113 | let topic = Buffer.concat([Buffer.alloc(12), contractAddress])
114 | let [{count}] = await db.query(sql`
115 | SELECT COUNT(*) AS count FROM (
116 | SELECT transaction_id FROM balance_change
117 | WHERE address_id IN ${addressIds} AND ${this.ctx.service.block.getRawBlockFilter()}
118 | UNION
119 | SELECT transaction_id FROM evm_receipt
120 | WHERE contract_address = ${contractAddress} AND ${this.ctx.service.block.getRawBlockFilter()}
121 | UNION
122 | SELECT receipt.transaction_id AS transaction_id FROM evm_receipt receipt, evm_receipt_log log
123 | WHERE log.receipt_id = receipt._id AND log.address = ${contractAddress}
124 | AND ${this.ctx.service.block.getRawBlockFilter('receipt.block_height')}
125 | UNION
126 | SELECT receipt.transaction_id AS transaction_id FROM evm_receipt receipt, evm_receipt_log log, contract
127 | WHERE log.receipt_id = receipt._id
128 | AND ${this.ctx.service.block.getRawBlockFilter('receipt.block_height')}
129 | AND contract.address = log.address AND contract.type IN ('qrc20', 'qrc721')
130 | AND log.topic1 = ${TransferABI.id}
131 | AND (log.topic2 = ${topic} OR log.topic3 = ${topic})
132 | AND (
133 | (contract.type = 'qrc20' AND log.topic3 IS NOT NULL AND log.topic4 IS NULL)
134 | OR (contract.type = 'qrc721' AND log.topic4 IS NOT NULL)
135 | )
136 | ) list
137 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction})
138 | return count
139 | }
140 |
141 | async getContractTransactions(contractAddress, addressIds) {
142 | const TransferABI = this.app.qtuminfo.lib.Solidity.qrc20ABIs.find(abi => abi.name === 'Transfer')
143 | const db = this.ctx.model
144 | let {sql} = this.ctx.helper
145 | let {limit, offset, reversed = true} = this.ctx.state.pagination
146 | let order = reversed ? 'DESC' : 'ASC'
147 | let topic = Buffer.concat([Buffer.alloc(12), contractAddress])
148 | let totalCount = await this.getContractTransactionCount(contractAddress, addressIds)
149 | let transactions = await db.query(sql`
150 | SELECT tx.id AS id FROM (
151 | SELECT block_height, index_in_block, _id FROM (
152 | SELECT block_height, index_in_block, transaction_id AS _id FROM balance_change
153 | WHERE address_id IN ${addressIds} AND ${this.ctx.service.block.getRawBlockFilter()}
154 | UNION
155 | SELECT block_height, index_in_block, transaction_id AS _id FROM evm_receipt
156 | WHERE contract_address = ${contractAddress} AND ${this.ctx.service.block.getRawBlockFilter()}
157 | UNION
158 | SELECT receipt.block_height AS block_height, receipt.index_in_block AS index_in_block, receipt.transaction_id AS _id
159 | FROM evm_receipt receipt, evm_receipt_log log
160 | WHERE log.receipt_id = receipt._id AND log.address = ${contractAddress}
161 | AND ${this.ctx.service.block.getRawBlockFilter('receipt.block_height')}
162 | UNION
163 | SELECT receipt.block_height AS block_height, receipt.index_in_block AS index_in_block, receipt.transaction_id AS _id
164 | FROM evm_receipt receipt, evm_receipt_log log, contract
165 | WHERE log.receipt_id = receipt._id
166 | AND ${this.ctx.service.block.getRawBlockFilter('receipt.block_height')}
167 | AND contract.address = log.address AND contract.type IN ('qrc20', 'qrc721')
168 | AND log.topic1 = ${TransferABI.id}
169 | AND (log.topic2 = ${topic} OR log.topic3 = ${topic})
170 | AND (
171 | (contract.type = 'qrc20' AND log.topic3 IS NOT NULL AND log.topic4 IS NULL)
172 | OR (contract.type = 'qrc721' AND log.topic4 IS NOT NULL)
173 | )
174 | ) list
175 | ORDER BY block_height ${{raw: order}}, index_in_block ${{raw: order}}, _id ${{raw: order}}
176 | LIMIT ${offset}, ${limit}
177 | ) list, transaction tx
178 | WHERE tx._id = list._id
179 | ORDER BY list.block_height ${{raw: order}}, list.index_in_block ${{raw: order}}, list._id ${{raw: order}}
180 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.state.transaction}).map(({id}) => id)
181 | return {totalCount, transactions}
182 | }
183 |
184 | async getContractBasicTransactionCount(contractAddress) {
185 | const {EvmReceipt: EVMReceipt} = this.ctx.model
186 | return await EVMReceipt.count({
187 | where: {
188 | contractAddress,
189 | ...this.ctx.service.block.getBlockFilter()
190 | },
191 | transaction: this.ctx.state.transaction
192 | })
193 | }
194 |
195 | async getContractBasicTransactions(contractAddress) {
196 | const {Address, OutputScript} = this.app.qtuminfo.lib
197 | const {
198 | Header, Transaction, TransactionOutput, Contract, EvmReceipt: EVMReceipt, EvmReceiptLog: EVMReceiptLog,
199 | where, col
200 | } = this.ctx.model
201 | const {in: $in} = this.app.Sequelize.Op
202 | let {limit, offset, reversed = true} = this.ctx.state.pagination
203 | let order = reversed ? 'DESC' : 'ASC'
204 | let totalCount = await this.getContractBasicTransactionCount(contractAddress)
205 | let receiptIds = (await EVMReceipt.findAll({
206 | where: {
207 | contractAddress,
208 | ...this.ctx.service.block.getBlockFilter()
209 | },
210 | attributes: ['_id'],
211 | order: [['blockHeight', order], ['indexInBlock', order], ['transactionId', order], ['outputIndex', order]],
212 | limit,
213 | offset,
214 | transaction: this.ctx.state.transaction
215 | })).map(receipt => receipt._id)
216 | let receipts = await EVMReceipt.findAll({
217 | where: {_id: {[$in]: receiptIds}},
218 | include: [
219 | {
220 | model: Header,
221 | as: 'header',
222 | required: false,
223 | attributes: ['hash', 'timestamp']
224 | },
225 | {
226 | model: Transaction,
227 | as: 'transaction',
228 | required: true,
229 | attributes: ['id']
230 | },
231 | {
232 | model: TransactionOutput,
233 | as: 'output',
234 | on: {
235 | transactionId: where(col('output.transaction_id'), '=', col('evm_receipt.transaction_id')),
236 | outputIndex: where(col('output.output_index'), '=', col('evm_receipt.output_index'))
237 | },
238 | required: true,
239 | attributes: ['scriptPubKey', 'value']
240 | },
241 | {
242 | model: EVMReceiptLog,
243 | as: 'logs',
244 | required: false,
245 | include: [{
246 | model: Contract,
247 | as: 'contract',
248 | required: true,
249 | attributes: ['addressString']
250 | }]
251 | },
252 | {
253 | model: Contract,
254 | as: 'contract',
255 | required: true,
256 | attributes: ['addressString']
257 | }
258 | ],
259 | order: [['blockHeight', order], ['indexInBlock', order], ['transactionId', order], ['outputIndex', order]],
260 | transaction: this.ctx.state.transaction
261 | })
262 | let transactions = receipts.map(receipt => ({
263 | transactionId: receipt.transaction.id,
264 | outputIndex: receipt.outputIndex,
265 | ...receipt.header ? {
266 | blockHeight: receipt.blockHeight,
267 | blockHash: receipt.header.hash,
268 | timestamp: receipt.header.timestamp,
269 | confirmations: this.app.blockchainInfo.tip.height - receipt.blockHeight + 1
270 | } : {confirmations: 0},
271 | scriptPubKey: OutputScript.fromBuffer(receipt.output.scriptPubKey),
272 | value: receipt.output.value,
273 | sender: new Address({type: receipt.senderType, data: receipt.senderData, chain: this.app.chain}),
274 | gasUsed: receipt.gasUsed,
275 | contractAddress: receipt.contractAddress.toString('hex'),
276 | contractAddressHex: receipt.contractAddress,
277 | excepted: receipt.excepted,
278 | exceptedMessage: receipt.exceptedMessage,
279 | evmLogs: receipt.logs.sort((x, y) => x.logIndex - y.logIndex).map(log => ({
280 | address: log.address.toString('hex'),
281 | addressHex: log.address,
282 | topics: this.ctx.service.transaction.transformTopics(log),
283 | data: log.data
284 | }))
285 | }))
286 | return {totalCount, transactions}
287 | }
288 |
289 | async callContract(contract, data, sender) {
290 | let client = new this.app.qtuminfo.rpc(this.app.config.qtuminfo.rpc)
291 | return await client.callcontract(
292 | contract.toString('hex'),
293 | data.toString('hex'),
294 | ...sender == null ? [] : [sender.toString('hex')]
295 | )
296 | }
297 |
298 | async searchLogs({contract, topic1, topic2, topic3, topic4} = {}) {
299 | const {Address} = this.app.qtuminfo.lib
300 | const db = this.ctx.model
301 | const {Header, Transaction, EvmReceipt: EVMReceipt, EvmReceiptLog: EVMReceiptLog, Contract} = db
302 | const {in: $in} = this.ctx.app.Sequelize.Op
303 | const {sql} = this.ctx.helper
304 | let {limit, offset} = this.ctx.state.pagination
305 |
306 | let blockFilter = this.ctx.service.block.getRawBlockFilter('receipt.block_height')
307 | let contractFilter = contract ? sql`log.address = ${contract}` : 'TRUE'
308 | let topic1Filter = topic1 ? sql`log.topic1 = ${topic1}` : 'TRUE'
309 | let topic2Filter = topic2 ? sql`log.topic2 = ${topic2}` : 'TRUE'
310 | let topic3Filter = topic3 ? sql`log.topic3 = ${topic3}` : 'TRUE'
311 | let topic4Filter = topic4 ? sql`log.topic4 = ${topic4}` : 'TRUE'
312 |
313 | let [{count: totalCount}] = await db.query(sql`
314 | SELECT COUNT(DISTINCT(log._id)) AS count from evm_receipt receipt, evm_receipt_log log
315 | WHERE receipt._id = log.receipt_id AND ${blockFilter} AND ${{raw: contractFilter}}
316 | AND ${{raw: topic1Filter}} AND ${{raw: topic2Filter}} AND ${{raw: topic3Filter}} AND ${{raw: topic4Filter}}
317 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.transaction})
318 | if (totalCount === 0) {
319 | return {totalCount, logs: []}
320 | }
321 |
322 | let ids = (await db.query(sql`
323 | SELECT log._id AS _id from evm_receipt receipt, evm_receipt_log log
324 | WHERE receipt._id = log.receipt_id AND ${blockFilter} AND ${{raw: contractFilter}}
325 | AND ${{raw: topic1Filter}} AND ${{raw: topic2Filter}} AND ${{raw: topic3Filter}} AND ${{raw: topic4Filter}}
326 | ORDER BY log._id ASC
327 | LIMIT ${offset}, ${limit}
328 | `, {type: db.QueryTypes.SELECT, transaction: this.ctx.transaction})).map(log => log._id)
329 |
330 | let logs = await EVMReceiptLog.findAll({
331 | where: {_id: {[$in]: ids}},
332 | attributes: ['topic1', 'topic2', 'topic3', 'topic4', 'data'],
333 | include: [
334 | {
335 | model: EVMReceipt,
336 | as: 'receipt',
337 | required: true,
338 | attributes: ['transactionId', 'outputIndex', 'blockHeight', 'senderType', 'senderData'],
339 | include: [
340 | {
341 | model: Transaction,
342 | as: 'transaction',
343 | required: true,
344 | attributes: ['id'],
345 | include: [{
346 | model: Header,
347 | as: 'header',
348 | required: true,
349 | attributes: ['hash', 'height', 'timestamp']
350 | }]
351 | },
352 | {
353 | model: Contract,
354 | as: 'contract',
355 | required: true,
356 | attributes: ['address', 'addressString']
357 | }
358 | ]
359 | },
360 | {
361 | model: Contract,
362 | as: 'contract',
363 | required: true,
364 | attributes: ['address', 'addressString']
365 | }
366 | ],
367 | order: [['_id', 'ASC']],
368 | transaction: this.ctx.state.transaction
369 | })
370 |
371 | return {
372 | totalCount,
373 | logs: logs.map(log => ({
374 | transactionId: log.receipt.transaction.id,
375 | outputIndex: log.receipt.outputIndex,
376 | blockHeight: log.receipt.transaction.header.height,
377 | blockHash: log.receipt.transaction.header.hash,
378 | timestamp: log.receipt.transaction.header.timestamp,
379 | sender: new Address({type: log.receipt.senderType, data: log.receipt.senderData, chain: this.app.chain}),
380 | contractAddress: log.receipt.contract.address.toString('hex'),
381 | contractAddressHex: log.receipt.contract.address,
382 | address: log.contract.address.toString('hex'),
383 | addressHex: log.contract.address,
384 | topics: this.ctx.service.transaction.transformTopics(log),
385 | data: log.data
386 | }))
387 | }
388 | }
389 |
390 | async transformHexAddresses(addresses) {
391 | if (addresses.length === 0) {
392 | return []
393 | }
394 | const {Contract} = this.ctx.model
395 | const {in: $in} = this.app.Sequelize.Op
396 | const {Address} = this.app.qtuminfo.lib
397 | let result = addresses.map(address => Buffer.compare(address, Buffer.alloc(20)) === 0 ? null : address)
398 |
399 | let contracts = await Contract.findAll({
400 | where: {address: {[$in]: addresses.filter(address => Buffer.compare(address, Buffer.alloc(20)) !== 0)}},
401 | attributes: ['address', 'addressString'],
402 | transaction: this.ctx.state.transaction
403 | })
404 | let mapping = new Map(contracts.map(({address, addressString}) => [address.toString('hex'), addressString]))
405 | for (let i = 0; i < result.length; ++i) {
406 | if (result[i]) {
407 | let string = mapping.get(result[i].toString('hex'))
408 | if (string) {
409 | result[i] = {string, hex: result[i]}
410 | } else {
411 | result[i] = new Address({
412 | type: Address.PAY_TO_PUBLIC_KEY_HASH,
413 | data: result[i],
414 | chain: this.app.chain
415 | }).toString()
416 | }
417 | }
418 | }
419 | return result
420 | }
421 | }
422 |
423 | module.exports = ContractService
424 |
--------------------------------------------------------------------------------
/doc/contract.md:
--------------------------------------------------------------------------------
1 | # Contract API
2 |
3 | - [Contract API](#contract-api)
4 | - [Contract Information](#contract-information)
5 | - [Contract Transactions](#contract-transactions)
6 | - [Contract Basic Transactions](#contract-basic-transactions)
7 | - [Call Contract](#call-contract)
8 | - [Search Logs](#search-logs)
9 |
10 |
11 | ## Contract Information
12 |
13 | **Request URL**
14 | ```
15 | GET /contract/:contract
16 | ```
17 |
18 | **Request**
19 | ```
20 | GET /contract/6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7
21 | ```
22 |
23 | **Response**
24 | ```json
25 | {
26 | "address": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7",
27 | "addressHex": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7",
28 | "vm": "evm",
29 | "type": "qrc20",
30 | "qrc20": {
31 | "name": "Bodhi Token",
32 | "symbol": "BOT",
33 | "decimals": 8,
34 | "totalSupply": "10000000000000000",
35 | "version": null,
36 | "holders": 33219,
37 | "transactions": 49622
38 | },
39 | "balance": "0",
40 | "totalReceived": "1086500002",
41 | "totalSent": "1086500002",
42 | "unconfirmed": "0",
43 | "qrc20Balances": [],
44 | "qrc721Balances": [],
45 | "transactionCount": 20572
46 | }
47 | ```
48 |
49 |
50 | ## Contract Transactions
51 |
52 | **Request URL**
53 | ```
54 | GET /contract/:contract/txs
55 | ```
56 |
57 | **Request Parameters**
58 |
92 |
93 | **Request**
94 | ```
95 | GET /contract/6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7/txs?limit=10&offset=10
96 | ```
97 |
98 | **Response**
99 | ```json
100 | {
101 | "totalCount": 20572,
102 | "transactions": [
103 | "30563670995c63fb99301c32498bf6bd32059d2ed680491ddb87bde60c425b8c",
104 | "3ea37cf8fa77031a7470001e422a618e5307711693d1c1ed81fa90542ec6d977",
105 | "60ceddf045a6892fa95084313f551f980b087c115dbfe00f420b34b2929c78c0",
106 | "1ecd472c91f9786b34e419e46755c0543a044c2d35373fec7a24a23cc2444390",
107 | "4449fa6a69f7af45cc3e16bb56a0b29437041b80aebf0a1644151c98810232c8",
108 | "fdebd8f01a5815db6b60a256dfd2b58c51ae6c1ab25de948bed9d10c5cbd12da",
109 | "561b2c5401972f211384c9eaf5e5ab3002546dd424526295cf7261583ebe88b0",
110 | "7662edfd2f5514a9276df7d5fa779adbefebcb69ae5d6fc13eb32d212a4d6755",
111 | "e2b6a4c9fe45e96406b8434d0d0c7cd47ce4daff7044b1202ef35b45020aa6c4",
112 | "56ba80c4bf87ad00d4e63cbbe6f98799626bd2017dfa369656ba0ba11ed3a181"
113 | ]
114 | }
115 | ```
116 |
117 |
118 | ## Contract Basic Transactions
119 | List of transactions the contract is called.
120 |
121 | **Request URL**
122 | ```
123 | GET /contract/:contract/basic-txs
124 | ```
125 |
126 | **Request Parameters**
127 |
161 |
162 | **Request**
163 | ```
164 | GET /contract/6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7/basic-txs?limit=3&offset=10
165 | ```
166 |
167 | **Response**
168 | ```json
169 | {
170 | "totalCount": 19611,
171 | "transactions": [
172 | {
173 | "transactionId": "30563670995c63fb99301c32498bf6bd32059d2ed680491ddb87bde60c425b8c",
174 | "outputIndex": 0,
175 | "blockHeight": 398666,
176 | "blockHash": "d958ea4817a4511ac0cd601deb7a7380cbfc12fcde21e495157b913048f6aa81",
177 | "timestamp": 1561618976,
178 | "confirmations": 8658,
179 | "type": "evm_call",
180 | "gasLimit": 100000,
181 | "gasPrice": 40,
182 | "byteCode": "a9059cbb0000000000000000000000005dbf04d00b2e13b820db7afc1bdab3d4e303e8b6000000000000000000000000000000000000000000000000000000471ca54070",
183 | "outputValue": "0",
184 | "sender": "QgAwsgVLnREV4GYTP1zishNmsfrGHNYMM3",
185 | "gasUsed": 21614,
186 | "contractAddress": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7",
187 | "contractAddressHex": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7",
188 | "excepted": "None",
189 | "exceptedMessage": "",
190 | "evmLogs": [
191 | {
192 | "address": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7",
193 | "addressHex": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7",
194 | "topics": [
195 | "ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
196 | "000000000000000000000000d6a6404a173de5604ed9a5334854786514c9c125",
197 | "0000000000000000000000005dbf04d00b2e13b820db7afc1bdab3d4e303e8b6"
198 | ],
199 | "data": "000000000000000000000000000000000000000000000000000000471ca54070"
200 | }
201 | ]
202 | },
203 | {
204 | "transactionId": "3ea37cf8fa77031a7470001e422a618e5307711693d1c1ed81fa90542ec6d977",
205 | "outputIndex": 1,
206 | "blockHeight": 398660,
207 | "blockHash": "b2b8f46df7d70c587ea2c81919a7cc4ec12b80441e4e3e36230e270f1fe25fe2",
208 | "timestamp": 1561618240,
209 | "confirmations": 8664,
210 | "type": "evm_call",
211 | "gasLimit": 250000,
212 | "gasPrice": 40,
213 | "byteCode": "a9059cbb000000000000000000000000d6a6404a173de5604ed9a5334854786514c9c125000000000000000000000000000000000000000000000000000000471ca54070",
214 | "outputValue": "0",
215 | "sender": "QhHMyzZRN4neqp5SjovBYUVJZneFPcRN9A",
216 | "gasUsed": 36614,
217 | "contractAddress": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7",
218 | "contractAddressHex": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7",
219 | "excepted": "None",
220 | "exceptedMessage": "",
221 | "evmLogs": [
222 | {
223 | "address": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7",
224 | "addressHex": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7",
225 | "topics": [
226 | "ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
227 | "000000000000000000000000e2d4ff6a36f6307e1bc034695c8a24a11fc504c6",
228 | "000000000000000000000000d6a6404a173de5604ed9a5334854786514c9c125"
229 | ],
230 | "data": "000000000000000000000000000000000000000000000000000000471ca54070"
231 | }
232 | ]
233 | },
234 | {
235 | "transactionId": "60ceddf045a6892fa95084313f551f980b087c115dbfe00f420b34b2929c78c0",
236 | "outputIndex": 0,
237 | "blockHeight": 397976,
238 | "blockHash": "bce13e100201df586df2b24cb6db9bfdc2a92f7f6de60ce7d9e449a3dfa344f1",
239 | "timestamp": 1561519520,
240 | "confirmations": 9348,
241 | "type": "evm_call",
242 | "gasLimit": 100000,
243 | "gasPrice": 40,
244 | "byteCode": "a9059cbb0000000000000000000000002354d9f2bbd1d14e18287aa44ec4dc2b237040b600000000000000000000000000000000000000000000000000000010b1a72d00",
245 | "outputValue": "0",
246 | "sender": "QW9VdHxy9xbeMq74bnZ2NqVQTgiXDbELDy",
247 | "gasUsed": 51550,
248 | "contractAddress": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7",
249 | "contractAddressHex": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7",
250 | "excepted": "None",
251 | "exceptedMessage": "",
252 | "evmLogs": [
253 | {
254 | "address": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7",
255 | "addressHex": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7",
256 | "topics": [
257 | "ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
258 | "00000000000000000000000068aeaaa277528eb19cf582784f8eda0090e13a0c",
259 | "0000000000000000000000002354d9f2bbd1d14e18287aa44ec4dc2b237040b6"
260 | ],
261 | "data": "00000000000000000000000000000000000000000000000000000010b1a72d00"
262 | }
263 | ]
264 | }
265 | ]
266 | }
267 | ```
268 |
269 |
270 | ## Call Contract
271 | Returns RPC `callcontract` result.
272 |
273 | **Request URL**
274 | ```
275 | GET /contract/:contract/call
276 | ```
277 |
278 | **Request Parameters**
279 |
280 |
281 |
282 | | Name |
283 | Type |
284 | Description |
285 |
286 |
287 |
288 |
289 |
290 | date
291 | |
292 |
293 | String
294 | |
295 | Hexadecimal data to send to contract |
296 |
297 |
298 |
299 | sender
300 | |
301 |
302 | String (optional)
303 | |
304 | Base58 P2PKH or 20-byte hexadecimal sender address |
305 |
306 |
307 |
308 |
309 | **Request**
310 | ```
311 | GET /contract/6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7/call?data=313ce567
312 | ```
313 |
314 | **Response**
315 | ```json
316 | {
317 | "address": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7",
318 | "executionResult": {
319 | "gasUsed": 21533,
320 | "excepted": "None",
321 | "newAddress": "6b8bf98ff497c064e8f0bde13e0c4f5ed5bf8ce7",
322 | "output": "0000000000000000000000000000000000000000000000000000000000000008",
323 | "codeDeposit": 0,
324 | "gasRefunded": 0,
325 | "depositSize": 0,
326 | "gasForDeposit": 0
327 | },
328 | "transactionReceipt": {
329 | "stateRoot": "d314a238f5431c7aeb8aea73790fce0d41bd5f8d7d46b37a1f5692bdf0d14acd",
330 | "gasUsed": 21533,
331 | "bloom": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
332 | "log": []
333 | }
334 | }
335 | ```
336 |
337 |
338 | ## Search Logs
339 |
340 | **Request URL**
341 | ```
342 | GET /searchlogs
343 | ```
344 |
345 | **Request Parameters**
346 |
347 |
348 |
349 | | Name |
350 | Type |
351 | Description |
352 |
353 |
354 |
355 |
356 | |
357 |
358 | Pagination Parameters
359 |
360 | |
361 |
362 |
363 | |
364 |
365 | Block / Timestamp Filter Parameters
366 |
367 | |
368 |
369 |
370 |
371 | contract
372 | |
373 |
374 | String (optional)
375 | |
376 | Filter contract address in log |
377 |
378 |
379 |
380 | topic1
381 | |
382 |
383 | Hexadecimal String (optional)
384 | |
385 | Filter first topic in log |
386 |
387 |
388 |
389 | topic2
390 | |
391 |
392 | Hexadecimal String (optional)
393 | |
394 | Filter second topic in log |
395 |
396 |
397 |
398 | topic3
399 | |
400 |
401 | Hexadecimal String (optional)
402 | |
403 | Filter third topic in log |
404 |
405 |
406 |
407 | topic4
408 | |
409 |
410 | Hexadecimal String (optional)
411 | |
412 | Filter fourth topic in log |
413 |
414 |
415 |
416 |
417 | **Request**
418 | ```
419 | GET /searchlogs?contract=056168620105d8f73a55d8c6542b565aea3665ec&topic1=2b37430897e8d659983fc8ae7ab83ad5b3be5a7db7ea0add5706731c2395f550
420 | ```
421 |
422 | **Response**
423 | ```json
424 | {
425 | "totalCount": 5,
426 | "logs": [
427 | {
428 | "transactionId": "4de540d9be565ac079c735d5d6b641b07be6eeb54f098bda460edf1553b08fe2",
429 | "outputIndex": 0,
430 | "blockHash": "cff6b7b7a5cc2e39228d0f3f010e94eaab7cc2bbddd99ce1860991262a653a9c",
431 | "blockHeight": 171565,
432 | "timestamp": 1528883856,
433 | "sender": "Qar3EUkbk6N9rRw2NtBj8Rsd55jbhj3wLV",
434 | "contractAddress": "056168620105d8f73a55d8c6542b565aea3665ec",
435 | "contractAddressHex": "056168620105d8f73a55d8c6542b565aea3665ec",
436 | "address": "056168620105d8f73a55d8c6542b565aea3665ec",
437 | "addressHex": "056168620105d8f73a55d8c6542b565aea3665ec",
438 | "topics": [
439 | "2b37430897e8d659983fc8ae7ab83ad5b3be5a7db7ea0add5706731c2395f550",
440 | "0000000000000000000000000000000000000000000000000000000000000000",
441 | "0000000000000000000000009c3a312a838351572971a76aee8a195e3cd2fac7"
442 | ],
443 | "data": "00000000000000000000000000000000000000000000000000000001013e02b90000000000000000000000000000000000000000000000000000000000000000"
444 | },
445 | {
446 | "transactionId": "97e313601a26deef95267fc1152fcc7065b204fdd2c6df1bbe5bdcaa6053859d",
447 | "outputIndex": 1,
448 | "blockHash": "95c913bec498cdedc187f72849930cbfc624cdf8f7a45b1852d4702e923a3750",
449 | "blockHeight": 171576,
450 | "timestamp": 1528884928,
451 | "sender": "QiyuDBQuvw8PFNZ4nDU7ST7PBYFRmffrh6",
452 | "contractAddress": "056168620105d8f73a55d8c6542b565aea3665ec",
453 | "contractAddressHex": "056168620105d8f73a55d8c6542b565aea3665ec",
454 | "address": "056168620105d8f73a55d8c6542b565aea3665ec",
455 | "addressHex": "056168620105d8f73a55d8c6542b565aea3665ec",
456 | "topics": [
457 | "2b37430897e8d659983fc8ae7ab83ad5b3be5a7db7ea0add5706731c2395f550",
458 | "0000000000000000000000000000000000000000000000000000000000000000",
459 | "000000000000000000000000f577d44a6e8585cfec280afb990ffc2b414b00ca"
460 | ],
461 | "data": "0000000000000000000000000000000000000000000000000000000d65e579a30000000000000000000000000000000000000000000000000000000000000000"
462 | },
463 | {
464 | "transactionId": "7f2a6c58440dd6de7f4742aff1969f842921c85024280bc08985b67f7d338c85",
465 | "outputIndex": 1,
466 | "blockHash": "69832b0dc05cbe2c2a35a5d0ae7a7670f8bd59ff3af1adfdae4a27d383c8a0ee",
467 | "blockHeight": 171578,
468 | "timestamp": 1528885280,
469 | "sender": "QXUANYANRVAeX2Tomy9W1FTV6LQxWiNc99",
470 | "contractAddress": "056168620105d8f73a55d8c6542b565aea3665ec",
471 | "contractAddressHex": "056168620105d8f73a55d8c6542b565aea3665ec",
472 | "address": "056168620105d8f73a55d8c6542b565aea3665ec",
473 | "addressHex": "056168620105d8f73a55d8c6542b565aea3665ec",
474 | "topics": [
475 | "2b37430897e8d659983fc8ae7ab83ad5b3be5a7db7ea0add5706731c2395f550",
476 | "0000000000000000000000000000000000000000000000000000000000000000",
477 | "000000000000000000000000772e9f721feca4a06169938cdaf02584e6c2b529"
478 | ],
479 | "data": "00000000000000000000000000000000000000000000000000000006b2f2bcd10000000000000000000000000000000000000000000000000000000000000000"
480 | },
481 | {
482 | "transactionId": "1ab6822f1613e15fad017410c2a732dfb40d446ec2de95ea87e29796f13285c6",
483 | "outputIndex": 0,
484 | "blockHash": "592ae3cc803d6f7ea0f00a9af1eea484e0a308767cd45a2e75d2dcf7d6501ec6",
485 | "blockHeight": 171750,
486 | "timestamp": 1528909824,
487 | "sender": "QUkSNYKHFB1HsiwPzv7Lmgr62T16XwWNUd",
488 | "contractAddress": "056168620105d8f73a55d8c6542b565aea3665ec",
489 | "contractAddressHex": "056168620105d8f73a55d8c6542b565aea3665ec",
490 | "address": "056168620105d8f73a55d8c6542b565aea3665ec",
491 | "addressHex": "056168620105d8f73a55d8c6542b565aea3665ec",
492 | "topics": [
493 | "2b37430897e8d659983fc8ae7ab83ad5b3be5a7db7ea0add5706731c2395f550",
494 | "0000000000000000000000000000000000000000000000000000000000000000",
495 | "000000000000000000000000595a41cd3d6f997beeb1a6fc52f978c28582d2e0"
496 | ],
497 | "data": "00000000000000000000000000000000000000000000000000000000949f2e2000000000000000000000000000000000000000000000000000000002540be400"
498 | },
499 | {
500 | "transactionId": "fab9e5bc74b504f81f3b32e2ec548746924616dbe9addf379405f38a7231cdb5",
501 | "outputIndex": 0,
502 | "blockHash": "99d238d7527a57b4bf3fc908a6a9996470a2b4ded75d30e76e46bfec57d3fe43",
503 | "blockHeight": 174491,
504 | "timestamp": 1529298256,
505 | "sender": "QaRg6Nrf5uMEggBGv8v8SpzdK6NqWZEf3F",
506 | "contractAddress": "056168620105d8f73a55d8c6542b565aea3665ec",
507 | "contractAddressHex": "056168620105d8f73a55d8c6542b565aea3665ec",
508 | "address": "056168620105d8f73a55d8c6542b565aea3665ec",
509 | "addressHex": "056168620105d8f73a55d8c6542b565aea3665ec",
510 | "topics": [
511 | "2b37430897e8d659983fc8ae7ab83ad5b3be5a7db7ea0add5706731c2395f550",
512 | "0000000000000000000000000000000000000000000000000000000000000000",
513 | "000000000000000000000000979e8ffce8ba65cc610c22bf412841ab9861ab53"
514 | ],
515 | "data": "00000000000000000000000000000000000000000000000000000042fd7b60310000000000000000000000000000000000000000000000000000000000000000"
516 | }
517 | ]
518 | }
519 | ```
520 |
--------------------------------------------------------------------------------