├── .dockerignore ├── test ├── sdk-examples │ ├── .gitignore │ ├── import-test-account.js │ ├── send-signed-tx.js │ ├── store-and-remove.js │ └── wrap-ccc-and-unwrap-ccc.js ├── custom.spec.ts ├── mint-asset.spec.ts ├── db-block-interface.spec.ts ├── store-and-remove.spec.ts ├── pending-tx.spec.ts └── wrap-unwrap.spec.ts ├── resources └── wCCC.png ├── src ├── models │ ├── logic │ │ ├── utils │ │ │ ├── format.ts │ │ │ ├── asset.ts │ │ │ ├── middleware.ts │ │ │ ├── transaction.ts │ │ │ ├── address.ts │ │ │ ├── custom.ts │ │ │ └── includeArray.ts │ │ ├── setRegularKey.ts │ │ ├── setShardUsers.ts │ │ ├── store.ts │ │ ├── remove.ts │ │ ├── setShardOwners.ts │ │ ├── custom.ts │ │ ├── pay.ts │ │ ├── createShard.ts │ │ ├── addressLog.ts │ │ ├── assetTypeLog.ts │ │ ├── account.ts │ │ ├── assetAddressLog.ts │ │ ├── changeAssetScheme.ts │ │ ├── unwrapCCC.ts │ │ ├── assetimage.ts │ │ ├── increaseassetsupply.ts │ │ └── wrapCCC.ts │ ├── account.ts │ ├── setRegularKey.ts │ ├── createShard.ts │ ├── custom.ts │ ├── pay.ts │ ├── setShardUsers.ts │ ├── setShardOwners.ts │ ├── unwrapCCC.ts │ ├── remove.ts │ ├── assetimage.ts │ ├── store.ts │ ├── aggsUTXO.ts │ ├── wrapCCC.ts │ ├── log.ts │ ├── assetTypeLog.ts │ ├── changeAssetScheme.ts │ ├── assetscheme.ts │ ├── assetAddressLog.ts │ ├── increaseAssetSupply.ts │ ├── cccChanges.ts │ ├── addressLog.ts │ └── assettransferoutput.ts ├── migrations │ ├── 20191113075708-add-utxos-index-address.js │ ├── 20191112103807-add-transaction-index-tracker.js │ ├── 20191021033504-add-transaction-index-is-pending.js │ ├── 20191111091152-add-address-log-index-block-number.js │ ├── 20190412063112-add-size-to-block.js │ ├── 20190411072140-add-tx-count-to-block.js │ ├── 20190412082903-add-tx-count-by-type.js │ ├── 20190624073521-add-signers-to-block.js │ ├── 20190626072921-add-delayed-author-reward-to-block.js │ ├── 20190418112715-add-set-regular-key-key-index.js │ ├── 20190613112240-change-custom.js │ ├── 20191121090356-add-pagination-index-to-ccc-changes.js │ ├── 20191114095633-add-tx-index-to-utxo.js │ ├── 20190619082717-change-transaction-index.js │ ├── 20190415112710-change-blocks_number_idx_to_unique_idx.js │ ├── 20191119050057-change-address-log-index.js │ ├── 20191119050110-change-asset-type-log-index.js │ ├── 20191113091514-change-transaction-index-is-pending-type.js │ ├── 20191121041217-add-tx-index-for-pending.js │ ├── 20191112091700-add-ccc-changes-index-address-block-number-id.js │ ├── 20181221075330-create-account.js │ ├── 20190408032155-add-seq-to-asset-scheme.js │ ├── 20190116201000-create-set-regular-key.js │ ├── 20190103031812-create-log.js │ ├── 20181228101830-create-asset-image.js │ ├── 20190116201000-create-create-shard.js │ ├── 20190118045800-create-unwrap-ccc.js │ ├── 20190116201000-create-custom.js │ ├── 20190116201000-create-remove.js │ ├── 20190116201000-create-set-shard-users.js │ ├── 20190116201000-create-set-shard-owners.js │ ├── 20190116201000-create-pay.js │ ├── 20191121041429-add-seq-to-address-log.js │ ├── 20190116201000-create-store.js │ ├── 20190116201000-create-decompose-asset.js │ ├── 20190920091121-drop-compose-asset.js │ ├── 20190920091124-drop-decompose-asset.js │ ├── 20190820060110-ccc-changes-unique-index-deposit.js │ ├── 20190118045800-create-wrap-ccc.js │ ├── 20190212000000-create-change-asset-scheme.js │ ├── 20191114103355-change-utxo-table-index.js │ ├── 20190116201000-create-transfer-asset.js │ ├── 20190116201000-create-increase-asset-supply.js │ ├── 20181220093812-create-asset-scheme.js │ ├── 20190326000001-create-asset-type-log.js │ ├── 20190326000000-create-address-log.js │ ├── 20190116201000-create-mint-asset.js │ ├── 20181217110415-create-block.js │ ├── 20190116201000-create-compose-asset.js │ ├── 20181223012324-create-asset-transfer-output.js │ └── 20181220022044-create-transaction.js ├── config.ts ├── exception.ts ├── log.ts ├── worker │ └── commonFeeDistribution.ts ├── api.ts ├── context.ts ├── index.ts ├── server.ts └── checker │ ├── slack.ts │ └── email.ts ├── config ├── dev.json ├── test.json ├── test-int.json └── default.json ├── .sequelizerc ├── database.js ├── .editorconfig ├── tsconfig.json ├── wait_to_start.sh ├── create_user_and_db.sql ├── tslint.json ├── .gitignore ├── Dockerfile ├── script ├── deleteBlock.ts ├── removePending.ts ├── fixSupplyOfWCCC.ts ├── updateAssetTypeLog.ts ├── updateAddressLog.ts ├── fillBlockSize.ts ├── fillTransactionsCount.ts ├── fillTransactionsCountByType.ts ├── fixBalanceAndSeq.ts └── 20190415112715-createCCCChange.ts ├── check-schema-migration-reversable.sh ├── .travis.yml ├── docker-compose.yml └── package.json /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .git 3 | -------------------------------------------------------------------------------- /test/sdk-examples/.gitignore: -------------------------------------------------------------------------------- 1 | test-*.js 2 | -------------------------------------------------------------------------------- /resources/wCCC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CodeChain-io/codechain-indexer/HEAD/resources/wCCC.png -------------------------------------------------------------------------------- /test/custom.spec.ts: -------------------------------------------------------------------------------- 1 | import "mocha"; 2 | 3 | describe.skip("Test for custom action", function() {}); 4 | -------------------------------------------------------------------------------- /src/models/logic/utils/format.ts: -------------------------------------------------------------------------------- 1 | export function strip0xPrefix(hash: string) { 2 | if (hash.startsWith("0x")) { 3 | return hash.substr(2); 4 | } 5 | return hash; 6 | } 7 | -------------------------------------------------------------------------------- /config/dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "pg": { 3 | "user": "user", 4 | "password": "password", 5 | "database": "codechain-indexer-dev", 6 | "port": 5432 7 | }, 8 | "sequelize": { 9 | "logging": false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /config/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "pg": { 3 | "user": "user", 4 | "password": "password", 5 | "database": "codechain-indexer-test", 6 | "port": 5432 7 | }, 8 | "sequelize": { 9 | "logging": false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.sequelizerc: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | "config": path.resolve("database.js"), 5 | "models-path": path.resolve("src/", "models"), 6 | "migrations-path": path.resolve("src/", "migrations") 7 | } 8 | -------------------------------------------------------------------------------- /config/test-int.json: -------------------------------------------------------------------------------- 1 | { 2 | "pg": { 3 | "user": "user", 4 | "password": "password", 5 | "database": "codechain-indexer-test-int", 6 | "port": 5432 7 | }, 8 | "sequelize": { 9 | "logging": false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /database.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = process.env.NODE_ENV || "dev"; 2 | const { pg, sequelize } = require("config"); 3 | const _ = require("lodash"); 4 | 5 | const config = _.assign({}, pg, { username: pg.user }, sequelize); 6 | const mode = process.env.NODE_ENV; 7 | 8 | module.exports[mode] = config; 9 | -------------------------------------------------------------------------------- /src/migrations/20191113075708-add-utxos-index-address.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, Sequelize) => { 5 | await queryInterface.addIndex("UTXOs", ["address", "id"]); 6 | }, 7 | 8 | down: async (queryInterface, Sequelize) => { 9 | await queryInterface.removeIndex("UTXOs", ["address", "id"]); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src/migrations/20191112103807-add-transaction-index-tracker.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, Sequelize) => { 5 | await queryInterface.addIndex("Transactions", ["tracker"]); 6 | }, 7 | 8 | down: async (queryInterface, Sequelize) => { 9 | await queryInterface.removeIndex("Transactions", ["tracker"]); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src/migrations/20191021033504-add-transaction-index-is-pending.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, Sequelize) => { 5 | await queryInterface.addIndex("Transactions", ["isPending"]); 6 | }, 7 | 8 | down: async (queryInterface, Sequelize) => { 9 | await queryInterface.removeIndex("Transactions", ["isPending"]); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /src/migrations/20191111091152-add-address-log-index-block-number.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, Sequelize) => { 5 | await queryInterface.addIndex("AddressLogs", ["blockNumber"]); 6 | }, 7 | 8 | down: async (queryInterface, Sequelize) => { 9 | await queryInterface.removeIndex("AddressLogs", ["blockNumber"]); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | insert_final_newline = true 7 | 8 | [*.yml] 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [{src,test,script}/**.{ts,tsx,json,js,jsx}] 13 | trim_trailing_whitespace = true 14 | indent_style = space 15 | indent_size = 4 16 | 17 | # npm inserts a final newline. 18 | [package.json] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "types": ["node"], 5 | "module": "commonjs", 6 | "declaration": true, 7 | "outDir": "build", 8 | "strict": true, 9 | "noImplicitAny": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true 12 | }, 13 | "include": ["./src/**/*"], 14 | "exclude": ["./src/**/*.spec.ts", "node_modules/**/*.ts", "./build/**/*"] 15 | } 16 | -------------------------------------------------------------------------------- /src/migrations/20190412063112-add-size-to-block.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: (queryInterface, Sequelize) => { 5 | return queryInterface.addColumn("Blocks", "size", { 6 | type: Sequelize.INTEGER, 7 | defaultValue: null 8 | }); 9 | }, 10 | 11 | down: (queryInterface, Sequelize) => { 12 | return queryInterface.removeColumn("Blocks", "size"); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/migrations/20190411072140-add-tx-count-to-block.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: (queryInterface, Sequelize) => { 5 | return queryInterface.addColumn("Blocks", "transactionsCount", { 6 | type: Sequelize.INTEGER, 7 | defaultValue: null 8 | }); 9 | }, 10 | 11 | down: (queryInterface, Sequelize) => { 12 | return queryInterface.removeColumn("Blocks", "transactionsCount"); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /wait_to_start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo $WAIT_COMMAND 4 | 5 | is_ready() { 6 | eval "$WAIT_COMMAND" 7 | } 8 | 9 | # wait until is ready 10 | i=0 11 | while ! is_ready; do 12 | i=`expr $i + 1` 13 | if [ $i -ge $WAIT_LOOPS ]; then 14 | echo "$(date) - still not ready, giving up" 15 | exit 1 16 | fi 17 | echo "$(date) - waiting to be ready" 18 | sleep $WAIT_SLEEP 19 | done 20 | 21 | echo "Elasticsearch is up - executing command" 22 | -------------------------------------------------------------------------------- /create_user_and_db.sql: -------------------------------------------------------------------------------- 1 | CREATE USER "user" WITH ENCRYPTED PASSWORD 'password'; 2 | CREATE DATABASE "codechain-indexer-dev" WITH OWNER "user"; 3 | CREATE DATABASE "codechain-indexer-test" WITH OWNER "user"; 4 | CREATE DATABASE "codechain-indexer-test-int" WITH OWNER "user"; 5 | GRANT ALL PRIVILEGES ON DATABASE "codechain-indexer-dev" TO "user"; 6 | GRANT ALL PRIVILEGES ON DATABASE "codechain-indexer-test" TO "user"; 7 | GRANT ALL PRIVILEGES ON DATABASE "codechain-indexer-test-int" TO "user"; 8 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import * as pg from "pg"; 2 | 3 | export interface IndexerConfig { 4 | version: number; 5 | publicAddress: string; 6 | httpPort: number; 7 | codechain: { 8 | host: string; 9 | networkId: "cc" | "tc" | "sc" | "wc"; 10 | }; 11 | pg: pg.ConnectionConfig; 12 | sequelize: { 13 | dialect: string; 14 | operatorsAliases: boolean; 15 | }; 16 | worker: { 17 | watchSchedule: string; 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/exception.ts: -------------------------------------------------------------------------------- 1 | export const AlreadyExist = () => new Error("AlreadyExist"); 2 | export const DBError = () => new Error("DBError"); 3 | export const InvalidTransaction = () => new Error("InvalidTransaction"); 4 | export const InvalidBlockNumber = () => new Error("InvalidBlockNumber"); 5 | export const InvalidUTXO = () => new Error("InvalidUTXO"); 6 | export const InvalidLogType = () => new Error("InvalidLogType"); 7 | export const InvalidDateParam = () => new Error("InvalidDateParam"); 8 | -------------------------------------------------------------------------------- /src/migrations/20190412082903-add-tx-count-by-type.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: (queryInterface, Sequelize) => { 5 | return queryInterface.addColumn("Blocks", "transactionsCountByType", { 6 | type: Sequelize.JSONB, 7 | defaultValue: null 8 | }); 9 | }, 10 | 11 | down: (queryInterface, Sequelize) => { 12 | return queryInterface.removeColumn("Blocks", "transactionsCountByType"); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/migrations/20190624073521-add-signers-to-block.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: (queryInterface, Sequelize) => { 5 | return queryInterface.addColumn("Blocks", "missedSignersOfPrev", { 6 | type: Sequelize.ARRAY(Sequelize.TEXT), 7 | defaultValue: [] 8 | }); 9 | }, 10 | 11 | down: (queryInterface, Sequelize) => { 12 | return queryInterface.removeColumn("Blocks", "missedSignersOfPrev"); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/migrations/20190626072921-add-delayed-author-reward-to-block.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: (queryInterface, Sequelize) => { 5 | return queryInterface.addColumn("Blocks", "intermediateRewards", { 6 | type: Sequelize.NUMERIC({ precision: 20, scale: 0 }), 7 | defaultValue: "0" 8 | }); 9 | }, 10 | 11 | down: (queryInterface, Sequelize) => { 12 | return queryInterface.removeColumn("Blocks", "intermediateRewards"); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/migrations/20190418112715-add-set-regular-key-key-index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const TABLE_NAME = "SetRegularKeys"; 4 | const ATTRIBUTES = ["key"]; 5 | module.exports = { 6 | up: (queryInterface, Sequelize) => { 7 | return queryInterface.addIndex(TABLE_NAME, ATTRIBUTES, { 8 | unique: false 9 | }); 10 | }, 11 | down: (queryInterface, Sequelize) => { 12 | return queryInterface.removeIndex(TABLE_NAME, ATTRIBUTES, { 13 | force: true 14 | }); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /src/migrations/20190613112240-change-custom.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: (queryInterface, Sequelize) => { 5 | return queryInterface.changeColumn("Customs", "content", { 6 | allowNull: false, 7 | type: Sequelize.TEXT 8 | }); 9 | }, 10 | 11 | down: (queryInterface, Sequelize) => { 12 | return queryInterface.changeColumn("Customs", "content", { 13 | allowNull: false, 14 | type: Sequelize.STRING 15 | }); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-config-prettier"], 3 | "rules": { 4 | "interface-name": false, 5 | "no-console": false, 6 | "object-literal-sort-keys": false, 7 | "no-var-requires": false, 8 | "array-type": false, 9 | "interface-over-type-literal": false 10 | }, 11 | "jsRules": { 12 | "no-console": false, 13 | "object-literal-sort-keys": false 14 | }, 15 | "linterOptions": { 16 | "exclude": ["config/**/*.js", "node_modules/**/*.ts", "./build/**/*"] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # misc 10 | .DS_Store 11 | .env.local 12 | .env.development.local 13 | .env.test.local 14 | .env.production.local 15 | 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | 20 | # dotenv 21 | .env 22 | 23 | # Visual Studio Code - https://code.visualstudio.com/ 24 | .settings/ 25 | .vscode/ 26 | 27 | kibana.yml 28 | 29 | # keyfile for test 30 | keystore.db 31 | 32 | /build/ 33 | -------------------------------------------------------------------------------- /src/log.ts: -------------------------------------------------------------------------------- 1 | import * as winston from "winston"; 2 | 3 | const logger = new winston.Logger({ 4 | transports: [ 5 | new winston.transports.Console({ 6 | level: "verbose", 7 | handleExceptions: true, 8 | json: false, 9 | colorize: true 10 | }) 11 | ], 12 | exitOnError: false 13 | }); 14 | 15 | class LogStream { 16 | public write(text: string) { 17 | logger.info(text); 18 | } 19 | } 20 | 21 | export const logStream = new LogStream(); 22 | 23 | export default logger; 24 | -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "publicAddress": "localhost", 4 | "httpPort": 9001, 5 | "codechain": { 6 | "host": "http://localhost:8080", 7 | "networkId": "tc" 8 | }, 9 | "sequelize": { 10 | "dialect": "postgres", 11 | "operatorsAliases": false, 12 | "host": "localhost", 13 | "pool": { 14 | "min": 1, 15 | "max": 10, 16 | "handleDisconnects": true 17 | }, 18 | "retry": { 19 | "max": 5 20 | } 21 | }, 22 | "worker": { 23 | "watchSchedule": "*/10 * * * * *" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use minideb instead of alpine since some modules use native binary not supported in alpine 2 | FROM bitnami/node:10.9.0 3 | WORKDIR /code 4 | 5 | # Install git because we currently fetch codechain core from github 6 | RUN apt-get update && apt-get install git 7 | 8 | # Install yarn 9 | RUN npm install yarn -g 10 | 11 | 12 | # Copy package.json and lock file to install dependencies 13 | COPY package.json yarn.lock /code/ 14 | 15 | # Install dependencies 16 | RUN yarn 17 | 18 | # Copy codechain indexer 19 | COPY . /code 20 | 21 | # Run server 22 | CMD sh -c "./wait_to_start.sh && yarn run start" 23 | -------------------------------------------------------------------------------- /src/migrations/20191121090356-add-pagination-index-to-ccc-changes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const tableName = "CCCChanges"; 4 | module.exports = { 5 | up: async (queryInterface, Sequelize) => { 6 | await queryInterface.addIndex(tableName, [ 7 | "address", 8 | "reason", 9 | "blockNumber", 10 | "id" 11 | ]); 12 | }, 13 | 14 | down: async (queryInterface, Sequelize) => { 15 | await queryInterface.removeIndex(tableName, [ 16 | "address", 17 | "reason", 18 | "blockNumber", 19 | "id" 20 | ]); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/migrations/20191114095633-add-tx-index-to-utxo.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, Sequelize) => { 5 | await queryInterface.addColumn("UTXOs", "transactionIndex", { 6 | type: Sequelize.INTEGER 7 | }); 8 | 9 | await queryInterface.sequelize.query( 10 | `UPDATE "UTXOs" SET "transactionIndex" = (SELECT "Transactions"."transactionIndex" FROM "Transactions" WHERE hash="UTXOs"."transactionHash")` 11 | ); 12 | }, 13 | 14 | down: async (queryInterface, Sequelize) => { 15 | return queryInterface.removeColumn("UTXOs", "transactionIndex"); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/migrations/20190619082717-change-transaction-index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, Sequelize) => { 5 | await queryInterface.removeIndex("Transactions", ["blockNumber"]); 6 | await queryInterface.addIndex("Transactions", [ 7 | "blockNumber", 8 | "transactionIndex" 9 | ]); 10 | }, 11 | 12 | down: async (queryInterface, Sequelize) => { 13 | await queryInterface.removeIndex("Transactions", [ 14 | "blockNumber", 15 | "transactionIndex" 16 | ]); 17 | await queryInterface.addIndex("Transactions", ["blockNumber"]); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /src/migrations/20190415112710-change-blocks_number_idx_to_unique_idx.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: (queryInterface, Sequelize) => { 5 | return queryInterface 6 | .removeIndex("Blocks", "blocks_number_idx") 7 | .then(() => 8 | queryInterface.addConstraint("Blocks", ["number"], { 9 | type: "unique", 10 | name: "blocks_number_idx" 11 | }) 12 | ); 13 | }, 14 | 15 | down: (queryInterface, Sequelize) => { 16 | return queryInterface 17 | .removeConstraint("Blocks", "blocks_number_idx") 18 | .then(() => queryInterface.addIndex("Blocks", ["number"])); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/migrations/20191119050057-change-address-log-index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const tableName = "AddressLogs"; 4 | module.exports = { 5 | up: async (queryInterface, Sequelize) => { 6 | await queryInterface.addIndex(tableName, [ 7 | "address", 8 | "blockNumber", 9 | "transactionIndex" 10 | ]); 11 | await queryInterface.removeIndex(tableName, ["address"]); 12 | }, 13 | 14 | down: async (queryInterface, Sequelize) => { 15 | await queryInterface.addIndex(tableName, ["address"]); 16 | await queryInterface.removeIndex(tableName, [ 17 | "address", 18 | "blockNumber", 19 | "transactionIndex" 20 | ]); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/migrations/20191119050110-change-asset-type-log-index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const tableName = "AssetTypeLogs"; 4 | module.exports = { 5 | up: async (queryInterface, Sequelize) => { 6 | await queryInterface.addIndex(tableName, [ 7 | "assetType", 8 | "blockNumber", 9 | "transactionIndex" 10 | ]); 11 | await queryInterface.removeIndex(tableName, ["assetType"]); 12 | }, 13 | 14 | down: async (queryInterface, Sequelize) => { 15 | await queryInterface.addIndex(tableName, ["assetType"]); 16 | await queryInterface.removeIndex(tableName, [ 17 | "assetType", 18 | "blockNumber", 19 | "transactionIndex" 20 | ]); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/worker/commonFeeDistribution.ts: -------------------------------------------------------------------------------- 1 | import { SDK } from "codechain-sdk"; 2 | import { PlatformAddress, U64 } from "codechain-sdk/lib/core/classes"; 3 | 4 | const rlp = require("rlp"); 5 | 6 | export async function getDelegation( 7 | sdk: SDK, 8 | delegator: PlatformAddress, 9 | blockNumber: number 10 | ): Promise { 11 | const data = await sdk.rpc.engine.getCustomActionData( 12 | 2, 13 | ["Delegation", delegator.accountId.toEncodeObject()], 14 | blockNumber 15 | ); 16 | if (data == null) { 17 | return []; 18 | } 19 | const list: Buffer[][] = rlp.decode(Buffer.from(data, "hex")); 20 | return list.map( 21 | ([_, quantity]) => new U64(`0x${quantity.toString("hex")}`) 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/migrations/20191113091514-change-transaction-index-is-pending-type.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, Sequelize) => { 5 | await queryInterface.removeIndex("Transactions", ["isPending"]); 6 | await queryInterface.addIndex("Transactions", [ 7 | "isPending", 8 | "type", 9 | "blockNumber", 10 | "transactionIndex" 11 | ]); 12 | }, 13 | 14 | down: async (queryInterface, Sequelize) => { 15 | await queryInterface.removeIndex("Transactions", [ 16 | "isPending", 17 | "type", 18 | "blockNumber", 19 | "transactionIndex" 20 | ]); 21 | await queryInterface.addIndex("Transactions", ["isPending"]); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /src/migrations/20191121041217-add-tx-index-for-pending.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const tableName = "Transactions"; 4 | module.exports = { 5 | up: async (queryInterface, Sequelize) => { 6 | await queryInterface.addIndex( 7 | tableName, 8 | ["isPending", "pendingTimestamp"], 9 | { 10 | where: { 11 | isPending: true 12 | } 13 | } 14 | ); 15 | }, 16 | 17 | down: async (queryInterface, Sequelize) => { 18 | await queryInterface.removeIndex( 19 | tableName, 20 | ["isPending", "pendingTimestamp"], 21 | { 22 | where: { 23 | isPending: true 24 | } 25 | } 26 | ); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/models/logic/utils/asset.ts: -------------------------------------------------------------------------------- 1 | import { H160 } from "codechain-sdk/lib/core/classes"; 2 | import { InvalidTransaction } from "../../../exception"; 3 | import { AssetSchemeAttribute } from "../../assetscheme"; 4 | import { getByAssetType } from "../assetscheme"; 5 | 6 | export function getAssetName(metadata: string): string | undefined { 7 | try { 8 | return JSON.parse(metadata).name; 9 | } catch (e) { 10 | return undefined; 11 | } 12 | } 13 | 14 | export async function getAssetScheme( 15 | assetType: H160 16 | ): Promise { 17 | const assetSchemeInstance = await getByAssetType(assetType); 18 | if (!assetSchemeInstance) { 19 | throw InvalidTransaction(); 20 | } 21 | return assetSchemeInstance.get({ 22 | plain: true 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /test/sdk-examples/import-test-account.js: -------------------------------------------------------------------------------- 1 | const SDK = require("codechain-sdk"); 2 | 3 | const sdk = new SDK({ 4 | server: process.env.CODECHAIN_RPC_HTTP || "http://localhost:8080", 5 | networkId: process.env.CODECHAIN_NETWORK_ID || "tc" 6 | }); 7 | 8 | const ACCOUNT_SECRET = 9 | process.env.ACCOUNT_SECRET || 10 | "ede1d4ccb4ec9a8bbbae9a13db3f4a7b56ea04189be86ac3a6a439d9a0a1addd"; 11 | const ACCOUNT_PASSPHRASE = process.env.ACCOUNT_PASSPHRASE || "satoshi"; 12 | 13 | (async () => { 14 | const address = await sdk.rpc.account.importRaw( 15 | ACCOUNT_SECRET, 16 | ACCOUNT_PASSPHRASE 17 | ); 18 | console.log(address); // tccq9h7vnl68frvqapzv3tujrxtxtwqdnxw6yamrrgd 19 | })().catch(e => { 20 | if (e.message !== "Already Exists") { 21 | console.error(e); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /script/deleteBlock.ts: -------------------------------------------------------------------------------- 1 | import * as Sequelize from "sequelize"; 2 | import models from "../src/models"; 3 | 4 | async function runRemoveAfterBlockNumber() { 5 | const blockNumber = process.argv[2]; 6 | 7 | console.log( 8 | "Remove blocks that have equal or greater than a specified number " 9 | ); 10 | if (blockNumber === undefined) { 11 | console.error("A number required in the arguments"); 12 | return; 13 | } 14 | await models.Block.destroy({ 15 | where: { number: { [Sequelize.Op.gte]: blockNumber } } 16 | }) 17 | .then(_ => { 18 | console.log("success"); 19 | return; 20 | }) 21 | .catch(_ => { 22 | console.log("failed"); 23 | }); 24 | 25 | process.exit(); 26 | } 27 | runRemoveAfterBlockNumber(); 28 | -------------------------------------------------------------------------------- /check-schema-migration-reversable.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ex; 3 | 4 | export NODE_ENV="test" 5 | DATABASE="codechain-indexer-test" 6 | 7 | function mktemp2 { 8 | PREFIX="$1" 9 | POSTFIX="$2" 10 | python -c "import tempfile; print(tempfile.NamedTemporaryFile(prefix='$PREFIX', suffix='$SUFFIX', delete=False).name)" 11 | } 12 | BEFORE=$(mktemp2 "${DATABASE}." ".schema.before.sql") 13 | AFTER=$(mktemp2 "${DATABASE}." ".schema.after.sql") 14 | 15 | LAST_BEFORE=$(perl -e 'my @files=`ls src/migrations`; print @files[-2]') 16 | if [[ "${LAST_BEFORE}" = "" ]]; then 17 | echo "$0: Can't find a migration file before the last one, exiting..." 18 | exit 1; 19 | fi 20 | 21 | yarn sequelize db:migrate --to "${LAST_BEFORE}" 22 | 23 | pg_dump -s "${DATABASE}" > "${BEFORE}" 24 | 25 | yarn sequelize db:migrate 26 | yarn sequelize db:migrate:undo 27 | 28 | pg_dump -s "${DATABASE}" > "${AFTER}" 29 | 30 | diff "${BEFORE}" "${AFTER}" 31 | -------------------------------------------------------------------------------- /script/removePending.ts: -------------------------------------------------------------------------------- 1 | import models from "../src/models"; 2 | 3 | (async () => { 4 | const hashes = await models.Transaction.findAll({ 5 | attributes: ["hash"], 6 | where: { 7 | isPending: true, 8 | blockNumber: null 9 | }, 10 | limit: 10, 11 | order: [["pendingTimestamp", "ASC"]] 12 | }).then(instances => instances.map(instance => instance.get().hash)); 13 | 14 | console.time("destroy"); 15 | const deleted = await models.Transaction.destroy({ 16 | where: { 17 | isPending: true, 18 | blockNumber: { 19 | [models.Sequelize.Op.eq]: null 20 | }, 21 | hash: { 22 | [models.Sequelize.Op.in]: hashes 23 | } 24 | } 25 | }); 26 | console.timeEnd("destroy"); 27 | 28 | console.log(`Deleted ${deleted} rows`); 29 | 30 | await models.sequelize.close(); 31 | })(); 32 | -------------------------------------------------------------------------------- /src/migrations/20191112091700-add-ccc-changes-index-address-block-number-id.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const TABLE_NAME = "CCCChanges"; 4 | module.exports = { 5 | up: async (queryInterface, Sequelize) => { 6 | await queryInterface.removeIndex(TABLE_NAME, "ccc_changes_address"); 7 | await queryInterface.addIndex( 8 | TABLE_NAME, 9 | ["address", "blockNumber", "id"], 10 | { 11 | name: "ccc_changes_address_block_number_id", 12 | unique: true 13 | } 14 | ); 15 | }, 16 | 17 | down: async (queryInterface, Sequelize) => { 18 | await queryInterface.removeIndex( 19 | TABLE_NAME, 20 | "ccc_changes_address_block_number_id" 21 | ); 22 | await queryInterface.addIndex(TABLE_NAME, ["address"], { 23 | name: "ccc_changes_address", 24 | unique: false 25 | }); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/models/logic/setRegularKey.ts: -------------------------------------------------------------------------------- 1 | import { SignedTransaction } from "codechain-sdk/lib/core/classes"; 2 | import { SetRegularKeyActionJSON } from "codechain-sdk/lib/core/transaction/SetRegularKey"; 3 | import { Transaction } from "sequelize"; 4 | import models from "../index"; 5 | import { SetRegularKeyInstance } from "../setRegularKey"; 6 | import { strip0xPrefix } from "./utils/format"; 7 | 8 | export async function createSetRegularKey( 9 | transaction: SignedTransaction, 10 | options: { transaction?: Transaction } = {} 11 | ): Promise { 12 | const transactionHash = transaction.hash().value; 13 | const { key } = transaction.toJSON().action as SetRegularKeyActionJSON; 14 | return await models.SetRegularKey.create( 15 | { 16 | transactionHash: strip0xPrefix(transactionHash), 17 | key: strip0xPrefix(key) 18 | }, 19 | { transaction: options.transaction } 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/models/logic/setShardUsers.ts: -------------------------------------------------------------------------------- 1 | import { SignedTransaction } from "codechain-sdk/lib/core/classes"; 2 | import { SetShardUsersActionJSON } from "codechain-sdk/lib/core/transaction/SetShardUsers"; 3 | import { Transaction } from "sequelize"; 4 | import models from "../index"; 5 | import { SetShardUsersInstance } from "../setShardUsers"; 6 | import { strip0xPrefix } from "./utils/format"; 7 | 8 | export async function createSetShardUsers( 9 | transaction: SignedTransaction, 10 | options: { 11 | transaction?: Transaction; 12 | } = {} 13 | ): Promise { 14 | const transactionHash = transaction.hash().value; 15 | const { shardId, users } = transaction.toJSON() 16 | .action as SetShardUsersActionJSON; 17 | return await models.SetShardUsers.create( 18 | { 19 | transactionHash: strip0xPrefix(transactionHash), 20 | shardId, 21 | users 22 | }, 23 | { transaction: options.transaction } 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/models/logic/store.ts: -------------------------------------------------------------------------------- 1 | import { SignedTransaction } from "codechain-sdk/lib/core/classes"; 2 | import { StoreActionJSON } from "codechain-sdk/lib/core/transaction/Store"; 3 | import { Transaction } from "sequelize"; 4 | import models from "../index"; 5 | import { StoreInstance } from "../store"; 6 | import { strip0xPrefix } from "./utils/format"; 7 | 8 | export async function createStore( 9 | transaction: SignedTransaction, 10 | options: { 11 | transaction?: Transaction; 12 | } = {} 13 | ): Promise { 14 | const transactionHash = transaction.hash().value; 15 | const { content, certifier, signature } = transaction.toJSON() 16 | .action as StoreActionJSON; 17 | return await models.Store.create( 18 | { 19 | transactionHash: strip0xPrefix(transactionHash), 20 | content, 21 | certifier, 22 | signature: strip0xPrefix(signature) 23 | }, 24 | { transaction: options.transaction } 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/migrations/20181221075330-create-account.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => { 4 | return queryInterface.createTable("Accounts", { 5 | address: { 6 | primaryKey: true, 7 | type: Sequelize.STRING 8 | }, 9 | balance: { 10 | allowNull: false, 11 | type: Sequelize.NUMERIC({ precision: 20, scale: 0 }) 12 | }, 13 | seq: { 14 | allowNull: false, 15 | type: Sequelize.INTEGER 16 | }, 17 | createdAt: { 18 | allowNull: false, 19 | type: Sequelize.DATE 20 | }, 21 | updatedAt: { 22 | allowNull: false, 23 | type: Sequelize.DATE 24 | } 25 | }); 26 | }, 27 | down: (queryInterface, Sequelize) => { 28 | return queryInterface.dropTable("Accounts", { force: true }); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /src/models/logic/remove.ts: -------------------------------------------------------------------------------- 1 | import { SignedTransaction } from "codechain-sdk/lib/core/classes"; 2 | import { RemoveActionJSON } from "codechain-sdk/lib/core/transaction/Remove"; 3 | import { Transaction } from "sequelize"; 4 | import models from "../index"; 5 | import { RemoveInstance } from "../remove"; 6 | import { strip0xPrefix } from "./utils/format"; 7 | 8 | export async function createRemove( 9 | transaction: SignedTransaction, 10 | options: { 11 | transaction?: Transaction; 12 | } = {} 13 | ): Promise { 14 | const transactionHash = transaction.hash().value; 15 | const { hash, signature } = transaction.toJSON().action as RemoveActionJSON; 16 | return await models.Remove.create( 17 | { 18 | transactionHash: strip0xPrefix(transactionHash), 19 | textHash: strip0xPrefix(hash), 20 | signature: strip0xPrefix(signature) 21 | }, 22 | { 23 | transaction: options.transaction 24 | } 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/models/logic/setShardOwners.ts: -------------------------------------------------------------------------------- 1 | import { SignedTransaction } from "codechain-sdk/lib/core/classes"; 2 | import { SetShardOwnersActionJSON } from "codechain-sdk/lib/core/transaction/SetShardOwners"; 3 | import { Transaction } from "sequelize"; 4 | import models from "../index"; 5 | import { SetShardOwnersInstance } from "../setShardOwners"; 6 | import { strip0xPrefix } from "./utils/format"; 7 | 8 | export async function createSetShardOwners( 9 | transaction: SignedTransaction, 10 | options: { 11 | transaction?: Transaction; 12 | } = {} 13 | ): Promise { 14 | const transactionHash = transaction.hash().value; 15 | const { shardId, owners } = transaction.toJSON() 16 | .action as SetShardOwnersActionJSON; 17 | return await models.SetShardOwners.create( 18 | { 19 | transactionHash: strip0xPrefix(transactionHash), 20 | shardId, 21 | owners 22 | }, 23 | { transaction: options.transaction } 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /script/fixSupplyOfWCCC.ts: -------------------------------------------------------------------------------- 1 | import { H160 } from "codechain-primitives/lib"; 2 | 3 | import models from "../src/models"; 4 | 5 | const networkId: string = process.env.CODECHAIN_NETWORK_ID!; 6 | const shardId: number = parseInt(process.env.CODECHAIN_SHARD_ID!, 10); 7 | 8 | if (!Number.isInteger(shardId) || networkId == null) { 9 | console.error( 10 | "check env variables CODECHAIN_NETWORK_ID and CODECHAIN_SHARD_ID" 11 | ); 12 | process.exit(); 13 | } 14 | 15 | (async () => { 16 | await models.AssetScheme.findByPk(H160.zero().value).then( 17 | async instance => { 18 | if (instance == null) { 19 | console.error("AssetScheme not found"); 20 | return; 21 | } 22 | await instance.update({ 23 | supply: 24 | "10000000000000000000000000000000000000000000000000" /* FIXME */ 25 | }); 26 | console.log("updated"); 27 | } 28 | ); 29 | await models.sequelize.close(); 30 | })(); 31 | -------------------------------------------------------------------------------- /src/models/logic/custom.ts: -------------------------------------------------------------------------------- 1 | import { SignedTransaction } from "codechain-sdk/lib/core/classes"; 2 | import { CustomActionJSON } from "codechain-sdk/lib/core/transaction/Custom"; 3 | import { Transaction } from "sequelize"; 4 | import { CustomInstance } from "../custom"; 5 | import models from "../index"; 6 | import { strip0xPrefix } from "./utils/format"; 7 | 8 | export async function createCustom( 9 | transaction: SignedTransaction, 10 | options: { 11 | transaction?: Transaction; 12 | } = {} 13 | ): Promise { 14 | const transactionHash = transaction.hash().value; 15 | const { handlerId, bytes } = transaction.toJSON() 16 | .action as CustomActionJSON; 17 | const content = Buffer.from(bytes).toString("hex"); 18 | return await models.Custom.create( 19 | { 20 | transactionHash: strip0xPrefix(transactionHash), 21 | handlerId: parseInt(handlerId, 10), 22 | content 23 | }, 24 | { transaction: options.transaction } 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/migrations/20190408032155-add-seq-to-asset-scheme.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: (queryInterface, Sequelize) => { 5 | return Promise.all([ 6 | queryInterface.addColumn("AssetSchemes", "seq", { 7 | type: Sequelize.INTEGER, 8 | defaultValue: 0 9 | }), 10 | queryInterface.addColumn("IncreaseAssetSupplies", "seq", { 11 | type: Sequelize.INTEGER, 12 | defaultValue: 0 13 | }), 14 | queryInterface.addColumn("ChangeAssetSchemes", "seq", { 15 | type: Sequelize.INTEGER, 16 | defaultValue: 0 17 | }) 18 | ]); 19 | }, 20 | 21 | down: (queryInterface, Sequelize) => { 22 | return Promise.all([ 23 | queryInterface.removeColumn("AssetSchemes", "seq"), 24 | queryInterface.removeColumn("IncreaseAssetSupplies", "seq"), 25 | queryInterface.removeColumn("ChangeAssetSchemes", "seq") 26 | ]); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /test/mint-asset.spec.ts: -------------------------------------------------------------------------------- 1 | import { U64 } from "codechain-primitives"; 2 | import "mocha"; 3 | import * as BlockModel from "../src/models/logic/block"; 4 | import * as Helper from "./helper"; 5 | 6 | describe("mint-asset", function() { 7 | before(async function() { 8 | await Helper.runExample("import-test-account"); 9 | await Helper.worker.sync(); 10 | }); 11 | 12 | it("MintAsset", async function() { 13 | const scheme = Helper.sdk.core.createAssetScheme({ 14 | shardId: 0, 15 | // NOTE: The random number prevents a conflict of assetType 16 | metadata: "" + Math.random(), 17 | supply: 1 18 | }); 19 | const recipient = await Helper.sdk.key.createAssetAddress(); 20 | const { block } = await Helper.sendTransactionAndGetBlock( 21 | Helper.sdk.core.createMintAssetTransaction({ 22 | scheme, 23 | recipient 24 | }) 25 | ); 26 | await BlockModel.createBlock(block, Helper.sdk, new U64(1)); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/sdk-examples/send-signed-tx.js: -------------------------------------------------------------------------------- 1 | const SDK = require("codechain-sdk"); 2 | 3 | const sdk = new SDK({ 4 | server: process.env.CODECHAIN_RPC_HTTP || "http://localhost:8080", 5 | networkId: process.env.CODECHAIN_NETWORK_ID || "tc" 6 | }); 7 | 8 | const ACCOUNT_SECRET = 9 | process.env.ACCOUNT_SECRET || 10 | "ede1d4ccb4ec9a8bbbae9a13db3f4a7b56ea04189be86ac3a6a439d9a0a1addd"; 11 | const ACCOUNT_ADDRESS = 12 | process.env.ACCOUNT_ADDRESS || 13 | "tccq9h7vnl68frvqapzv3tujrxtxtwqdnxw6yamrrgd"; 14 | 15 | const tx = sdk.core.createPayTransaction({ 16 | recipient: "tccqxv9y4cw0jwphhu65tn4605wadyd2sxu5yezqghw", 17 | quantity: 10000 18 | }); 19 | 20 | (async () => { 21 | const seq = await sdk.rpc.chain.getSeq(ACCOUNT_ADDRESS); 22 | const hash = await sdk.rpc.chain.sendSignedTransaction( 23 | tx.sign({ 24 | secret: ACCOUNT_SECRET, 25 | fee: 10, 26 | seq 27 | }) 28 | ); 29 | const result = await sdk.rpc.chain.containsTransaction(hash); 30 | console.log(result); // true 31 | })().catch(console.error); 32 | -------------------------------------------------------------------------------- /src/models/logic/pay.ts: -------------------------------------------------------------------------------- 1 | import { SignedTransaction, U64 } from "codechain-sdk/lib/core/classes"; 2 | import { PayActionJSON } from "codechain-sdk/lib/core/transaction/Pay"; 3 | import { Transaction } from "sequelize"; 4 | import models from "../index"; 5 | import { PayInstance } from "../pay"; 6 | import { createAddressLog } from "./addressLog"; 7 | import { strip0xPrefix } from "./utils/format"; 8 | 9 | export async function createPay( 10 | transaction: SignedTransaction, 11 | options: { transaction?: Transaction } = {} 12 | ): Promise { 13 | const transactionHash = transaction.hash().value; 14 | const { quantity, receiver } = transaction.toJSON().action as PayActionJSON; 15 | const instance = await models.Pay.create( 16 | { 17 | transactionHash: strip0xPrefix(transactionHash), 18 | quantity: new U64(quantity).toString(), 19 | receiver 20 | }, 21 | { transaction: options.transaction } 22 | ); 23 | await createAddressLog(transaction, receiver, "AssetOwner", options); 24 | return instance; 25 | } 26 | -------------------------------------------------------------------------------- /test/db-block-interface.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { U64 } from "codechain-sdk/lib/core/classes"; 3 | import "mocha"; 4 | import * as BlockModel from "../src/models/logic/block"; 5 | import * as Helper from "./helper"; 6 | 7 | describe("db-block-interface", function() { 8 | before(async function() { 9 | await Helper.runExample("import-test-account"); 10 | await Helper.worker.sync(); 11 | await Helper.runExample("mint-and-transfer"); 12 | }); 13 | 14 | it("Check getLastBlock", async function() { 15 | const mintBlockNumber = 16 | (await Helper.sdk.rpc.chain.getBestBlockNumber()) - 1; 17 | const mintBlock = await Helper.sdk.rpc.chain.getBlock(mintBlockNumber); 18 | expect(mintBlock).not.null; 19 | await BlockModel.createBlock(mintBlock!, Helper.sdk, new U64("1000")); 20 | const lastBlockInstance = await BlockModel.getLatestBlock(); 21 | expect(lastBlockInstance).not.null; 22 | expect(lastBlockInstance!.get({ plain: true }).number).equal( 23 | mintBlock!.number 24 | ); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | 5 | jobs: 6 | include: 7 | - name: test 8 | before_install: 9 | - npm install -g yarn 10 | - yarn install 11 | before_script: 12 | - docker pull kodebox/codechain:v2.0.0 13 | - docker run -d -p 8080:8080 kodebox/codechain:v2.0.0 --jsonrpc-interface 0.0.0.0 -c solo --reseal-min-period 0 --enable-devel-api 14 | - psql -c 'CREATE DATABASE "codechain-indexer-test";' -U postgres 15 | - psql -c "CREATE USER \"user\" WITH ENCRYPTED PASSWORD 'password';" -U postgres 16 | - docker ps -a 17 | script: 18 | - set -e 19 | - yarn run lint 20 | - NODE_ENV=test yarn migrate 21 | - yarn run test 22 | - name: Check schema migration is reversable 23 | before_install: 24 | - npm install -g yarn 25 | - yarn install 26 | before_script: 27 | - psql postgres -f create_user_and_db.sql 28 | script: 29 | - ./check-schema-migration-reversable.sh 30 | services: 31 | - docker 32 | - postgresql 33 | addons: 34 | postgresql: "9.5" 35 | cache: yarn 36 | -------------------------------------------------------------------------------- /src/api.ts: -------------------------------------------------------------------------------- 1 | import * as cors from "cors"; 2 | import * as express from "express"; 3 | import * as AccountHandler from "./routers/account"; 4 | import * as AssetHandler from "./routers/asset"; 5 | import * as BlockHandler from "./routers/block"; 6 | import * as LogHandler from "./routers/log"; 7 | import * as StatusHandler from "./routers/status"; 8 | import * as TxHandler from "./routers/transaction"; 9 | 10 | import { IndexerContext } from "./context"; 11 | 12 | const corsOptions = { 13 | origin: true, 14 | credentials: true, 15 | exposedHeaders: ["Location", "Link"] 16 | }; 17 | 18 | export function createApiRouter(context: IndexerContext, useCors = false) { 19 | const router = express.Router(); 20 | 21 | if (useCors) { 22 | router.options("*", cors(corsOptions)).use(cors(corsOptions)); 23 | } 24 | 25 | StatusHandler.handle(context, router); 26 | BlockHandler.handle(context, router); 27 | TxHandler.handle(context, router); 28 | AssetHandler.handle(context, router); 29 | AccountHandler.handle(context, router); 30 | LogHandler.handle(context, router); 31 | 32 | return router; 33 | } 34 | -------------------------------------------------------------------------------- /src/migrations/20190116201000-create-set-regular-key.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const tableName = "SetRegularKeys"; 3 | module.exports = { 4 | up: (queryInterface, DataTypes) => { 5 | return queryInterface.createTable(tableName, { 6 | transactionHash: { 7 | allowNull: false, 8 | primaryKey: true, 9 | type: DataTypes.STRING, 10 | onDelete: "CASCADE", 11 | references: { 12 | model: "Transactions", 13 | key: "hash" 14 | } 15 | }, 16 | 17 | key: { 18 | allowNull: false, 19 | type: DataTypes.STRING 20 | }, 21 | 22 | createdAt: { 23 | allowNull: false, 24 | type: DataTypes.DATE 25 | }, 26 | updatedAt: { 27 | allowNull: false, 28 | type: DataTypes.DATE 29 | } 30 | }); 31 | }, 32 | down: (queryInterface, Sequelize) => { 33 | return queryInterface.dropTable(tableName, { force: true }); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/context.ts: -------------------------------------------------------------------------------- 1 | import { SDK } from "codechain-sdk"; 2 | import { createSlack, Slack } from "./checker/slack"; 3 | import { IndexerConfig } from "./config"; 4 | import models from "./models"; 5 | import Worker from "./worker"; 6 | 7 | export class IndexerContext { 8 | public static newInstance(options: IndexerConfig) { 9 | return new IndexerContext(options); 10 | } 11 | public sdk: SDK; 12 | public worker: Worker; 13 | public slack: Slack; 14 | 15 | private constructor(public readonly options: IndexerConfig) { 16 | this.sdk = new SDK({ 17 | server: options.codechain.host, 18 | networkId: options.codechain.networkId 19 | }); 20 | this.slack = createSlack( 21 | `[${options.codechain.networkId}][indexer]`, 22 | process.env.SLACK_WEBHOOK 23 | ); 24 | this.worker = new Worker( 25 | { sdk: this.sdk, slack: this.slack }, 26 | options.worker 27 | ); 28 | } 29 | 30 | public destroy = async () => { 31 | console.log("Destroying the context..."); 32 | await models.sequelize.close(); 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/migrations/20190103031812-create-log.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => { 4 | return queryInterface.createTable("Logs", { 5 | id: { 6 | allowNull: false, 7 | primaryKey: true, 8 | type: Sequelize.STRING 9 | }, 10 | date: { 11 | allowNull: false, 12 | type: Sequelize.STRING 13 | }, 14 | count: { 15 | allowNull: false, 16 | type: Sequelize.INTEGER 17 | }, 18 | type: { 19 | allowNull: false, 20 | type: Sequelize.STRING 21 | }, 22 | value: { 23 | type: Sequelize.STRING 24 | }, 25 | createdAt: { 26 | allowNull: false, 27 | type: Sequelize.DATE 28 | }, 29 | updatedAt: { 30 | allowNull: false, 31 | type: Sequelize.DATE 32 | } 33 | }); 34 | }, 35 | down: (queryInterface, Sequelize) => { 36 | return queryInterface.dropTable("Logs", { force: true }); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /src/models/account.ts: -------------------------------------------------------------------------------- 1 | import * as Sequelize from "sequelize"; 2 | 3 | export interface AccountAttribtue { 4 | address: string; 5 | balance: string; 6 | seq: number; 7 | } 8 | 9 | export interface AccountInstance extends Sequelize.Instance {} 10 | 11 | export default ( 12 | sequelize: Sequelize.Sequelize, 13 | DataTypes: Sequelize.DataTypes 14 | ) => { 15 | const Account = sequelize.define( 16 | "Account", 17 | { 18 | address: { 19 | primaryKey: true, 20 | type: DataTypes.STRING 21 | }, 22 | balance: { 23 | type: DataTypes.NUMERIC({ precision: 20, scale: 0 }) 24 | }, 25 | seq: { 26 | type: DataTypes.INTEGER 27 | }, 28 | createdAt: { 29 | allowNull: false, 30 | type: DataTypes.DATE 31 | }, 32 | updatedAt: { 33 | allowNull: false, 34 | type: DataTypes.DATE 35 | } 36 | }, 37 | {} 38 | ); 39 | Account.associate = () => { 40 | // associations can be defined here 41 | }; 42 | return Account; 43 | }; 44 | -------------------------------------------------------------------------------- /src/migrations/20181228101830-create-asset-image.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => { 4 | return queryInterface.createTable("AssetImages", { 5 | transactionHash: { 6 | allowNull: false, 7 | type: Sequelize.STRING 8 | }, 9 | assetType: { 10 | allowNull: false, 11 | primaryKey: true, 12 | type: Sequelize.STRING, 13 | onDelete: "CASCADE", 14 | references: { 15 | model: "AssetSchemes", 16 | key: "assetType" 17 | } 18 | }, 19 | image: { 20 | allowNull: false, 21 | type: Sequelize.BLOB 22 | }, 23 | createdAt: { 24 | allowNull: false, 25 | type: Sequelize.DATE 26 | }, 27 | updatedAt: { 28 | allowNull: false, 29 | type: Sequelize.DATE 30 | } 31 | }); 32 | }, 33 | down: (queryInterface, Sequelize) => { 34 | return queryInterface.dropTable("AssetImages", { force: true }); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /src/migrations/20190116201000-create-create-shard.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const tableName = "CreateShards"; 3 | module.exports = { 4 | up: (queryInterface, DataTypes) => { 5 | return queryInterface.createTable(tableName, { 6 | transactionHash: { 7 | allowNull: false, 8 | primaryKey: true, 9 | type: DataTypes.STRING, 10 | onDelete: "CASCADE", 11 | references: { 12 | model: "Transactions", 13 | key: "hash" 14 | } 15 | }, 16 | 17 | shardId: { 18 | type: DataTypes.INTEGER 19 | }, 20 | users: { 21 | allowNull: false, 22 | type: DataTypes.JSONB 23 | }, 24 | 25 | createdAt: { 26 | allowNull: false, 27 | type: DataTypes.DATE 28 | }, 29 | updatedAt: { 30 | allowNull: false, 31 | type: DataTypes.DATE 32 | } 33 | }); 34 | }, 35 | down: (queryInterface, Sequelize) => { 36 | return queryInterface.dropTable(tableName, { force: true }); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | codechain-indexer: 4 | build: . 5 | ports: 6 | - "8081:8081" 7 | depends_on: 8 | - codechain 9 | - elasticsearch 10 | environment: 11 | - CODECHAIN_HOST=http://codechain:8080 12 | - ELASTICSEARCH_HOST=http://elasticsearch:9200 13 | - WAIT_COMMAND=[ $$(curl --write-out %{http_code} --silent --output /dev/null http://elasticsearch:9200/_cat/health?h=st) = 200 ] 14 | - WAIT_SLEEP=2 15 | - WAIT_LOOPS=20 16 | 17 | codechain: 18 | image: kodebox/codechain:25b8af1dd5945c9a71ce4523375a391ee390e547 19 | ports: 20 | - "8080:8080" 21 | command: 22 | - -c=solo 23 | - --jsonrpc-interface=0.0.0.0 24 | - --db-path=codechaindata:/db 25 | - --reseal-min-period=0 26 | volumes: 27 | - codechaindata:/db 28 | 29 | elasticsearch: 30 | image: docker.elastic.co/elasticsearch/elasticsearch:6.4.3 31 | environment: 32 | - ES_HOSTS=0.0.0.0:9200 33 | volumes: 34 | - esdata:/usr/share/elasticsearch/data 35 | 36 | kibana: 37 | image: docker.elastic.co/kibana/kibana-oss:6.4.3 38 | ports: 39 | - "5601:5601" 40 | 41 | volumes: 42 | ? codechaindata 43 | ? esdata 44 | -------------------------------------------------------------------------------- /src/migrations/20190118045800-create-unwrap-ccc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const tableName = "UnwrapCCCs"; 3 | module.exports = { 4 | up: (queryInterface, DataTypes) => { 5 | return queryInterface.createTable(tableName, { 6 | transactionHash: { 7 | allowNull: false, 8 | primaryKey: true, 9 | type: DataTypes.STRING, 10 | onDelete: "CASCADE", 11 | references: { 12 | model: "Transactions", 13 | key: "hash" 14 | } 15 | }, 16 | receiver: { 17 | allowNull: false, 18 | type: DataTypes.STRING 19 | }, 20 | burn: { 21 | allowNull: false, 22 | type: DataTypes.JSONB 23 | }, 24 | 25 | createdAt: { 26 | allowNull: false, 27 | type: DataTypes.DATE 28 | }, 29 | updatedAt: { 30 | allowNull: false, 31 | type: DataTypes.DATE 32 | } 33 | }); 34 | }, 35 | down: (queryInterface, Sequelize) => { 36 | return queryInterface.dropTable(tableName, { force: true }); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /src/migrations/20190116201000-create-custom.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const tableName = "Customs"; 3 | module.exports = { 4 | up: (queryInterface, DataTypes) => { 5 | return queryInterface.createTable(tableName, { 6 | transactionHash: { 7 | allowNull: false, 8 | primaryKey: true, 9 | type: DataTypes.STRING, 10 | onDelete: "CASCADE", 11 | references: { 12 | model: "Transactions", 13 | key: "hash" 14 | } 15 | }, 16 | 17 | handlerId: { 18 | allowNull: false, 19 | type: DataTypes.INTEGER 20 | }, 21 | content: { 22 | allowNull: false, 23 | type: DataTypes.STRING 24 | }, 25 | 26 | createdAt: { 27 | allowNull: false, 28 | type: DataTypes.DATE 29 | }, 30 | updatedAt: { 31 | allowNull: false, 32 | type: DataTypes.DATE 33 | } 34 | }); 35 | }, 36 | down: (queryInterface, Sequelize) => { 37 | return queryInterface.dropTable(tableName, { force: true }); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/migrations/20190116201000-create-remove.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const tableName = "Removes"; 3 | module.exports = { 4 | up: (queryInterface, DataTypes) => { 5 | return queryInterface.createTable(tableName, { 6 | transactionHash: { 7 | allowNull: false, 8 | primaryKey: true, 9 | type: DataTypes.STRING, 10 | onDelete: "CASCADE", 11 | references: { 12 | model: "Transactions", 13 | key: "hash" 14 | } 15 | }, 16 | 17 | textHash: { 18 | allowNull: false, 19 | type: DataTypes.STRING 20 | }, 21 | signature: { 22 | allowNull: false, 23 | type: DataTypes.STRING 24 | }, 25 | 26 | createdAt: { 27 | allowNull: false, 28 | type: DataTypes.DATE 29 | }, 30 | updatedAt: { 31 | allowNull: false, 32 | type: DataTypes.DATE 33 | } 34 | }); 35 | }, 36 | down: (queryInterface, Sequelize) => { 37 | return queryInterface.dropTable(tableName, { force: true }); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/migrations/20190116201000-create-set-shard-users.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const tableName = "SetShardUsers"; 3 | module.exports = { 4 | up: (queryInterface, DataTypes) => { 5 | return queryInterface.createTable(tableName, { 6 | transactionHash: { 7 | allowNull: false, 8 | primaryKey: true, 9 | type: DataTypes.STRING, 10 | onDelete: "CASCADE", 11 | references: { 12 | model: "Transactions", 13 | key: "hash" 14 | } 15 | }, 16 | 17 | shardId: { 18 | allowNull: false, 19 | type: DataTypes.INTEGER 20 | }, 21 | users: { 22 | allowNull: false, 23 | type: DataTypes.JSONB 24 | }, 25 | 26 | createdAt: { 27 | allowNull: false, 28 | type: DataTypes.DATE 29 | }, 30 | updatedAt: { 31 | allowNull: false, 32 | type: DataTypes.DATE 33 | } 34 | }); 35 | }, 36 | down: (queryInterface, Sequelize) => { 37 | return queryInterface.dropTable(tableName, { force: true }); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/migrations/20190116201000-create-set-shard-owners.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const tableName = "SetShardOwners"; 3 | module.exports = { 4 | up: (queryInterface, DataTypes) => { 5 | return queryInterface.createTable(tableName, { 6 | transactionHash: { 7 | allowNull: false, 8 | primaryKey: true, 9 | type: DataTypes.STRING, 10 | onDelete: "CASCADE", 11 | references: { 12 | model: "Transactions", 13 | key: "hash" 14 | } 15 | }, 16 | 17 | shardId: { 18 | allowNull: false, 19 | type: DataTypes.INTEGER 20 | }, 21 | owners: { 22 | allowNull: false, 23 | type: DataTypes.JSONB 24 | }, 25 | 26 | createdAt: { 27 | allowNull: false, 28 | type: DataTypes.DATE 29 | }, 30 | updatedAt: { 31 | allowNull: false, 32 | type: DataTypes.DATE 33 | } 34 | }); 35 | }, 36 | down: (queryInterface, Sequelize) => { 37 | return queryInterface.dropTable(tableName, { force: true }); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/migrations/20190116201000-create-pay.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const tableName = "Pays"; 3 | module.exports = { 4 | up: (queryInterface, DataTypes) => { 5 | return queryInterface.createTable(tableName, { 6 | transactionHash: { 7 | allowNull: false, 8 | primaryKey: true, 9 | type: DataTypes.STRING, 10 | onDelete: "CASCADE", 11 | references: { 12 | model: "Transactions", 13 | key: "hash" 14 | } 15 | }, 16 | 17 | receiver: { 18 | allowNull: false, 19 | type: DataTypes.STRING 20 | }, 21 | quantity: { 22 | allowNull: false, 23 | type: DataTypes.NUMERIC({ precision: 20, scale: 0 }) 24 | }, 25 | 26 | createdAt: { 27 | allowNull: false, 28 | type: DataTypes.DATE 29 | }, 30 | updatedAt: { 31 | allowNull: false, 32 | type: DataTypes.DATE 33 | } 34 | }); 35 | }, 36 | down: (queryInterface, Sequelize) => { 37 | return queryInterface.dropTable(tableName, { force: true }); 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /src/migrations/20191121041429-add-seq-to-address-log.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const tableName = "AddressLogs"; 4 | module.exports = { 5 | up: async (queryInterface, Sequelize) => { 6 | await queryInterface.addColumn(tableName, "seq", { 7 | type: Sequelize.INTEGER 8 | }); 9 | await queryInterface.addIndex( 10 | tableName, 11 | ["isPending", "address", "type", "seq"], 12 | { 13 | where: { 14 | type: "TransactionSigner" 15 | } 16 | } 17 | ); 18 | 19 | await queryInterface.sequelize.query( 20 | `UPDATE "AddressLogs" 21 | SET "seq" = (SELECT "Transactions"."seq" FROM "Transactions" WHERE hash="AddressLogs"."transactionHash") 22 | WHERE type='TransactionSigner'` 23 | ); 24 | }, 25 | 26 | down: async (queryInterface, Sequelize) => { 27 | await queryInterface.removeIndex( 28 | tableName, 29 | ["isPending", "address", "type", "seq"], 30 | { 31 | where: { 32 | type: "TransactionSigner" 33 | } 34 | } 35 | ); 36 | await queryInterface.removeColumn(tableName, "seq"); 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /script/updateAssetTypeLog.ts: -------------------------------------------------------------------------------- 1 | import models from "../src/models"; 2 | 3 | (async () => { 4 | let updated = 0; 5 | let pending = 0; 6 | 7 | await models.AssetTypeLog.findAll({ 8 | where: { 9 | isPending: true 10 | } 11 | }).then(logs => 12 | Promise.all( 13 | logs.map(async log => { 14 | const { transactionHash } = log.get(); 15 | const tx = await models.Transaction.findByPk(transactionHash); 16 | if (tx == null) { 17 | console.error("never"); 18 | return; 19 | } 20 | const { isPending } = tx.get(); 21 | if (isPending === false) { 22 | const { blockNumber, transactionIndex } = tx.get(); 23 | await log.update({ 24 | blockNumber, 25 | transactionIndex, 26 | isPending 27 | }); 28 | updated++; 29 | } else { 30 | pending++; 31 | } 32 | }) 33 | ) 34 | ); 35 | 36 | console.log(`updated ${updated} rows`); 37 | console.log(`# of pending ${pending}`); 38 | 39 | await models.sequelize.close(); 40 | })(); 41 | -------------------------------------------------------------------------------- /script/updateAddressLog.ts: -------------------------------------------------------------------------------- 1 | import models from "../src/models"; 2 | 3 | (async () => { 4 | let updated = 0; 5 | let pending = 0; 6 | 7 | await models.AddressLog.findAll({ 8 | where: { 9 | isPending: true 10 | } 11 | }).then(logs => 12 | Promise.all( 13 | logs.map(async log => { 14 | const { transactionHash } = log.get(); 15 | const tx = await models.Transaction.findByPk(transactionHash); 16 | if (tx == null) { 17 | console.error("never"); 18 | return; 19 | } 20 | const { isPending } = tx.get(); 21 | if (isPending === false) { 22 | const { blockNumber, transactionIndex } = tx.get(); 23 | await log.update({ 24 | blockNumber, 25 | transactionIndex, 26 | success: true, 27 | isPending 28 | }); 29 | updated++; 30 | } else { 31 | pending++; 32 | } 33 | }) 34 | ) 35 | ); 36 | 37 | console.log(`updated ${updated} rows`); 38 | console.log(`# of pending ${pending}`); 39 | 40 | await models.sequelize.close(); 41 | })(); 42 | -------------------------------------------------------------------------------- /src/migrations/20190116201000-create-store.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const tableName = "Stores"; 3 | module.exports = { 4 | up: (queryInterface, DataTypes) => { 5 | return queryInterface.createTable(tableName, { 6 | transactionHash: { 7 | allowNull: false, 8 | primaryKey: true, 9 | type: DataTypes.STRING, 10 | onDelete: "CASCADE", 11 | references: { 12 | model: "Transactions", 13 | key: "hash" 14 | } 15 | }, 16 | 17 | content: { 18 | allowNull: false, 19 | type: DataTypes.STRING 20 | }, 21 | certifier: { 22 | allowNull: false, 23 | type: DataTypes.STRING 24 | }, 25 | signature: { 26 | allowNull: false, 27 | type: DataTypes.STRING 28 | }, 29 | 30 | createdAt: { 31 | allowNull: false, 32 | type: DataTypes.DATE 33 | }, 34 | updatedAt: { 35 | allowNull: false, 36 | type: DataTypes.DATE 37 | } 38 | }); 39 | }, 40 | down: (queryInterface, Sequelize) => { 41 | return queryInterface.dropTable(tableName, { force: true }); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /script/fillBlockSize.ts: -------------------------------------------------------------------------------- 1 | import { SDK } from "codechain-sdk"; 2 | 3 | import models from "../src/models"; 4 | 5 | async function main() { 6 | const sdk = new SDK({ server: "http://localhost:8080", networkId: "cc" }); 7 | const numbers = await models.Block.findAll({ 8 | attributes: ["number"], 9 | where: { 10 | size: { 11 | [models.Sequelize.Op.eq]: null 12 | } 13 | } 14 | }).then(blocks => blocks.map(b => b.get({ plain: true }).number)); 15 | 16 | console.log(`${numbers.length} rows to update found`); 17 | 18 | let updated = 0; 19 | for (const n of numbers) { 20 | await models.Block.findOne({ 21 | where: { 22 | number: n 23 | } 24 | }).then(async instance => { 25 | if (instance == null) { 26 | throw Error(`Block ${n} not found`); 27 | } 28 | const block = await sdk.rpc.chain.getBlock(n); 29 | const size = block!.getSize(); 30 | updated++; 31 | if (updated % 100 === 0) { 32 | console.log(updated); 33 | } 34 | return instance.update({ 35 | size 36 | }); 37 | }); 38 | } 39 | 40 | console.log(`Updated ${updated} rows`); 41 | } 42 | 43 | main() 44 | .catch(console.error) 45 | .finally(() => models.sequelize.close()); 46 | -------------------------------------------------------------------------------- /src/models/setRegularKey.ts: -------------------------------------------------------------------------------- 1 | import * as Sequelize from "sequelize"; 2 | 3 | export interface SetRegularKeyAttribute { 4 | transactionHash: string; 5 | key: string; 6 | } 7 | 8 | export interface SetRegularKeyInstance 9 | extends Sequelize.Instance {} 10 | 11 | export default ( 12 | sequelize: Sequelize.Sequelize, 13 | DataTypes: Sequelize.DataTypes 14 | ) => { 15 | return sequelize.define( 16 | "SetRegularKey", 17 | { 18 | transactionHash: { 19 | allowNull: false, 20 | primaryKey: true, 21 | type: DataTypes.STRING, 22 | onDelete: "CASCADE", 23 | validate: { 24 | is: ["^[a-f0-9]{64}$"] 25 | }, 26 | references: { 27 | model: "Transactions", 28 | key: "hash" 29 | } 30 | }, 31 | key: { 32 | allowNull: false, 33 | type: DataTypes.STRING, 34 | validate: { 35 | is: ["^[a-f0-9]{128}$"] 36 | } 37 | }, 38 | 39 | createdAt: { 40 | allowNull: false, 41 | type: DataTypes.DATE 42 | }, 43 | updatedAt: { 44 | allowNull: false, 45 | type: DataTypes.DATE 46 | } 47 | }, 48 | {} 49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /src/models/createShard.ts: -------------------------------------------------------------------------------- 1 | import * as Sequelize from "sequelize"; 2 | 3 | export interface CreateShardAttribute { 4 | transactionHash: string; 5 | shardId?: number; 6 | users: string[]; 7 | } 8 | 9 | export interface CreateShardInstance 10 | extends Sequelize.Instance {} 11 | 12 | export default ( 13 | sequelize: Sequelize.Sequelize, 14 | DataTypes: Sequelize.DataTypes 15 | ) => { 16 | return sequelize.define( 17 | "CreateShard", 18 | { 19 | transactionHash: { 20 | allowNull: false, 21 | primaryKey: true, 22 | type: Sequelize.STRING, 23 | onDelete: "CASCADE", 24 | references: { 25 | model: "Transactions", 26 | key: "hash" 27 | }, 28 | validate: { 29 | is: ["^[a-f0-9]{64}$"] 30 | } 31 | }, 32 | 33 | shardId: { 34 | type: DataTypes.INTEGER 35 | }, 36 | users: { 37 | allowNull: false, 38 | type: DataTypes.JSONB 39 | }, 40 | 41 | createdAt: { 42 | allowNull: false, 43 | type: DataTypes.DATE 44 | }, 45 | updatedAt: { 46 | allowNull: false, 47 | type: DataTypes.DATE 48 | } 49 | }, 50 | {} 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /src/models/custom.ts: -------------------------------------------------------------------------------- 1 | import * as Sequelize from "sequelize"; 2 | 3 | export interface CustomAttribute { 4 | transactionHash: string; 5 | handlerId: number; 6 | content: string; 7 | } 8 | 9 | export interface CustomInstance extends Sequelize.Instance {} 10 | 11 | export default ( 12 | sequelize: Sequelize.Sequelize, 13 | DataTypes: Sequelize.DataTypes 14 | ) => { 15 | return sequelize.define( 16 | "Custom", 17 | { 18 | transactionHash: { 19 | allowNull: false, 20 | primaryKey: true, 21 | type: DataTypes.STRING, 22 | onDelete: "CASCADE", 23 | validate: { 24 | is: ["^[a-f0-9]{64}$"] 25 | }, 26 | references: { 27 | model: "Transactions", 28 | key: "hash" 29 | } 30 | }, 31 | 32 | handlerId: { 33 | allowNull: false, 34 | type: DataTypes.INTEGER 35 | }, 36 | content: { 37 | allowNull: false, 38 | type: DataTypes.STRING 39 | }, 40 | 41 | createdAt: { 42 | allowNull: false, 43 | type: DataTypes.DATE 44 | }, 45 | updatedAt: { 46 | allowNull: false, 47 | type: DataTypes.DATE 48 | } 49 | }, 50 | {} 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /src/models/pay.ts: -------------------------------------------------------------------------------- 1 | import * as Sequelize from "sequelize"; 2 | 3 | export interface PayAttribute { 4 | transactionHash: string; 5 | receiver: string; 6 | quantity: string; 7 | } 8 | 9 | export interface PayInstance extends Sequelize.Instance {} 10 | 11 | export default ( 12 | sequelize: Sequelize.Sequelize, 13 | DataTypes: Sequelize.DataTypes 14 | ) => { 15 | return sequelize.define( 16 | "Pay", 17 | { 18 | transactionHash: { 19 | allowNull: false, 20 | primaryKey: true, 21 | type: DataTypes.STRING, 22 | onDelete: "CASCADE", 23 | validate: { 24 | is: ["^[a-f0-9]{64}$"] 25 | }, 26 | references: { 27 | model: "Transactions", 28 | key: "hash" 29 | } 30 | }, 31 | receiver: { 32 | allowNull: false, 33 | type: DataTypes.STRING 34 | }, 35 | quantity: { 36 | allowNull: false, 37 | type: DataTypes.NUMERIC({ precision: 20, scale: 0 }) 38 | }, 39 | 40 | createdAt: { 41 | allowNull: false, 42 | type: DataTypes.DATE 43 | }, 44 | updatedAt: { 45 | allowNull: false, 46 | type: DataTypes.DATE 47 | } 48 | }, 49 | {} 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /src/migrations/20190116201000-create-decompose-asset.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const tableName = "DecomposeAssets"; 3 | module.exports = { 4 | up: (queryInterface, DataTypes) => { 5 | return queryInterface.createTable(tableName, { 6 | transactionHash: { 7 | allowNull: false, 8 | primaryKey: true, 9 | type: DataTypes.STRING, 10 | onDelete: "CASCADE", 11 | references: { 12 | model: "Transactions", 13 | key: "hash" 14 | } 15 | }, 16 | 17 | networkId: { 18 | allowNull: false, 19 | type: DataTypes.STRING 20 | }, 21 | approvals: { 22 | allowNull: false, 23 | type: DataTypes.JSONB 24 | }, 25 | input: { 26 | allowNull: false, 27 | type: DataTypes.JSONB 28 | }, 29 | outputs: { 30 | allowNull: false, 31 | type: DataTypes.JSONB 32 | }, 33 | 34 | createdAt: { 35 | allowNull: false, 36 | type: DataTypes.DATE 37 | }, 38 | updatedAt: { 39 | allowNull: false, 40 | type: DataTypes.DATE 41 | } 42 | }); 43 | }, 44 | down: (queryInterface, Sequelize) => { 45 | return queryInterface.dropTable(tableName, { force: true }); 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /src/migrations/20190920091121-drop-compose-asset.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const tableName = "ComposeAssets"; 3 | module.exports = { 4 | up: (queryInterface, Sequelize) => { 5 | return queryInterface.dropTable(tableName, { force: true }); 6 | }, 7 | 8 | down: (queryInterface, DataTypes) => { 9 | return queryInterface.createTable(tableName, { 10 | transactionHash: { 11 | allowNull: false, 12 | primaryKey: true, 13 | type: DataTypes.STRING, 14 | onDelete: "CASCADE", 15 | references: { 16 | model: "Transactions", 17 | key: "hash" 18 | } 19 | }, 20 | 21 | networkId: { 22 | allowNull: false, 23 | type: DataTypes.STRING 24 | }, 25 | approvals: { 26 | allowNull: false, 27 | type: DataTypes.JSONB 28 | }, 29 | input: { 30 | allowNull: false, 31 | type: DataTypes.JSONB 32 | }, 33 | outputs: { 34 | allowNull: false, 35 | type: DataTypes.JSONB 36 | }, 37 | 38 | createdAt: { 39 | allowNull: false, 40 | type: DataTypes.DATE 41 | }, 42 | updatedAt: { 43 | allowNull: false, 44 | type: DataTypes.DATE 45 | } 46 | }); 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /src/migrations/20190920091124-drop-decompose-asset.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const tableName = "DecomposeAssets"; 3 | module.exports = { 4 | up: (queryInterface, Sequelize) => { 5 | return queryInterface.dropTable(tableName, { force: true }); 6 | }, 7 | 8 | down: (queryInterface, DataTypes) => { 9 | return queryInterface.createTable(tableName, { 10 | transactionHash: { 11 | allowNull: false, 12 | primaryKey: true, 13 | type: DataTypes.STRING, 14 | onDelete: "CASCADE", 15 | references: { 16 | model: "Transactions", 17 | key: "hash" 18 | } 19 | }, 20 | 21 | networkId: { 22 | allowNull: false, 23 | type: DataTypes.STRING 24 | }, 25 | approvals: { 26 | allowNull: false, 27 | type: DataTypes.JSONB 28 | }, 29 | input: { 30 | allowNull: false, 31 | type: DataTypes.JSONB 32 | }, 33 | outputs: { 34 | allowNull: false, 35 | type: DataTypes.JSONB 36 | }, 37 | 38 | createdAt: { 39 | allowNull: false, 40 | type: DataTypes.DATE 41 | }, 42 | updatedAt: { 43 | allowNull: false, 44 | type: DataTypes.DATE 45 | } 46 | }); 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /src/models/setShardUsers.ts: -------------------------------------------------------------------------------- 1 | import * as Sequelize from "sequelize"; 2 | 3 | export interface SetShardUsersAttribute { 4 | transactionHash: string; 5 | shardId: number; 6 | users: string[]; 7 | } 8 | 9 | export interface SetShardUsersInstance 10 | extends Sequelize.Instance {} 11 | 12 | export default ( 13 | sequelize: Sequelize.Sequelize, 14 | DataTypes: Sequelize.DataTypes 15 | ) => { 16 | return sequelize.define( 17 | "SetShardUsers", 18 | { 19 | transactionHash: { 20 | allowNull: false, 21 | primaryKey: true, 22 | type: DataTypes.STRING, 23 | onDelete: "CASCADE", 24 | validate: { 25 | is: ["^[a-f0-9]{64}$"] 26 | }, 27 | references: { 28 | model: "Transactions", 29 | key: "hash" 30 | } 31 | }, 32 | 33 | shardId: { 34 | allowNull: false, 35 | type: DataTypes.INTEGER 36 | }, 37 | users: { 38 | allowNull: false, 39 | type: DataTypes.JSONB 40 | }, 41 | 42 | createdAt: { 43 | allowNull: false, 44 | type: DataTypes.DATE 45 | }, 46 | updatedAt: { 47 | allowNull: false, 48 | type: DataTypes.DATE 49 | } 50 | }, 51 | {} 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /src/models/setShardOwners.ts: -------------------------------------------------------------------------------- 1 | import * as Sequelize from "sequelize"; 2 | 3 | export interface SetShardOwnersAttribute { 4 | transactionHash: string; 5 | shardId: number; 6 | owners: string[]; 7 | } 8 | 9 | export interface SetShardOwnersInstance 10 | extends Sequelize.Instance {} 11 | 12 | export default ( 13 | sequelize: Sequelize.Sequelize, 14 | DataTypes: Sequelize.DataTypes 15 | ) => { 16 | return sequelize.define( 17 | "SetShardOwners", 18 | { 19 | transactionHash: { 20 | allowNull: false, 21 | primaryKey: true, 22 | type: DataTypes.STRING, 23 | onDelete: "CASCADE", 24 | validate: { 25 | is: ["^[a-f0-9]{64}$"] 26 | }, 27 | references: { 28 | model: "Transactions", 29 | key: "hash" 30 | } 31 | }, 32 | 33 | shardId: { 34 | allowNull: false, 35 | type: DataTypes.INTEGER 36 | }, 37 | owners: { 38 | allowNull: false, 39 | type: DataTypes.JSONB 40 | }, 41 | 42 | createdAt: { 43 | allowNull: false, 44 | type: DataTypes.DATE 45 | }, 46 | updatedAt: { 47 | allowNull: false, 48 | type: DataTypes.DATE 49 | } 50 | }, 51 | {} 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /src/models/logic/utils/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextFunction, Request, RequestHandler, Response } from "express"; 2 | import { SERVICE_UNAVAILABLE } from "http-status-codes"; 3 | import { IndexerContext } from "../../../context"; 4 | 5 | export function syncIfNeeded(context: IndexerContext): RequestHandler { 6 | return async (req, res, next) => { 7 | if (req.query.sync === true) { 8 | try { 9 | await context.worker.sync(); 10 | } catch (e) { 11 | const error = e as Error; 12 | if (error.message.search(/ECONNRESET|ECONNREFUSED/) >= 0) { 13 | res.status(SERVICE_UNAVAILABLE).send(); 14 | return; 15 | } 16 | next(e); 17 | } 18 | } 19 | next(); 20 | }; 21 | } 22 | 23 | export function parseEvaluatedKey( 24 | req: Request, 25 | _: Response, 26 | next: NextFunction 27 | ): any { 28 | try { 29 | if (req.query.firstEvaluatedKey) { 30 | req.query.firstEvaluatedKey = JSON.parse( 31 | req.query.firstEvaluatedKey 32 | ); 33 | } 34 | if (req.query.lastEvaluatedKey) { 35 | req.query.lastEvaluatedKey = JSON.parse(req.query.lastEvaluatedKey); 36 | } 37 | if (req.query.lastEvaluatedKey && req.query.firstEvaluatedKey) { 38 | throw new Error("Conflict lastEvaluatedKey and firstEvaluatedKey"); 39 | } 40 | next(); 41 | } catch (e) { 42 | next(e); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/models/logic/createShard.ts: -------------------------------------------------------------------------------- 1 | import { SDK } from "codechain-sdk"; 2 | import { SignedTransaction } from "codechain-sdk/lib/core/classes"; 3 | import { CreateShardActionJSON } from "codechain-sdk/lib/core/transaction/CreateShard"; 4 | import { Transaction } from "sequelize"; 5 | import { CreateShardInstance } from "../createShard"; 6 | import models from "../index"; 7 | import { TransactionInstance } from "../transaction"; 8 | import { strip0xPrefix } from "./utils/format"; 9 | 10 | export async function createCreateShard( 11 | transaction: SignedTransaction, 12 | options: { transaction?: Transaction } = {} 13 | ): Promise { 14 | const transactionHash = transaction.hash().value; 15 | const { users } = transaction.toJSON().action as CreateShardActionJSON; 16 | return await models.CreateShard.create( 17 | { 18 | transactionHash: strip0xPrefix(transactionHash), 19 | users 20 | }, 21 | { transaction: options.transaction } 22 | ); 23 | } 24 | 25 | export async function updateShardId( 26 | tx: TransactionInstance, 27 | sdk: SDK, 28 | options: { transaction?: Transaction } = {} 29 | ) { 30 | const { hash } = tx.get(); 31 | const shardId = await sdk.rpc.chain.getShardIdByHash(hash); 32 | await models.CreateShard.update( 33 | { 34 | shardId: shardId! 35 | }, 36 | { 37 | where: { 38 | transactionHash: strip0xPrefix(hash) 39 | }, 40 | transaction: options.transaction 41 | } 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/migrations/20190820060110-ccc-changes-unique-index-deposit.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const TABLE_NAME = "CCCChanges"; 4 | module.exports = { 5 | up: async (queryInterface, _Sequelize) => { 6 | await queryInterface.removeIndex( 7 | TABLE_NAME, 8 | "ccc_changes_unique_index2" 9 | ); 10 | await queryInterface.addIndex( 11 | TABLE_NAME, 12 | ["address", "blockNumber", "reason"], 13 | { 14 | unique: true, 15 | name: "ccc_changes_unique_index2", 16 | where: { 17 | reason: [ 18 | "author", 19 | "stake", 20 | "initial_distribution", 21 | "validator" 22 | ] 23 | } 24 | } 25 | ); 26 | }, 27 | down: async (queryInterface, _Sequelize) => { 28 | await queryInterface.removeIndex( 29 | TABLE_NAME, 30 | "ccc_changes_unique_index2" 31 | ); 32 | await queryInterface.addIndex( 33 | TABLE_NAME, 34 | ["address", "blockNumber", "reason"], 35 | { 36 | unique: true, 37 | name: "ccc_changes_unique_index2", 38 | where: { 39 | reason: [ 40 | "author", 41 | "stake", 42 | "initial_distribution", 43 | "deposit", 44 | "validator" 45 | ] 46 | } 47 | } 48 | ); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /src/models/unwrapCCC.ts: -------------------------------------------------------------------------------- 1 | import * as Sequelize from "sequelize"; 2 | import { AssetTransferInput } from "./transferAsset"; 3 | 4 | export interface UnwrapCCCAttribute { 5 | transactionHash: string; 6 | receiver: string; 7 | burn: AssetTransferInput; 8 | } 9 | 10 | export type UnwrapCCCInstance = Sequelize.Instance; 11 | 12 | export default ( 13 | sequelize: Sequelize.Sequelize, 14 | DataTypes: Sequelize.DataTypes 15 | ) => { 16 | const Action = sequelize.define( 17 | "UnwrapCCC", 18 | { 19 | transactionHash: { 20 | allowNull: false, 21 | primaryKey: true, 22 | type: DataTypes.STRING, 23 | onDelete: "CASCADE", 24 | validate: { 25 | is: ["^[a-f0-9]{64}$"] 26 | }, 27 | references: { 28 | model: "Transactions", 29 | key: "hash" 30 | } 31 | }, 32 | receiver: { 33 | allowNull: false, 34 | type: DataTypes.STRING 35 | }, 36 | burn: { 37 | allowNull: false, 38 | type: DataTypes.JSONB 39 | }, 40 | 41 | createdAt: { 42 | allowNull: false, 43 | type: DataTypes.DATE 44 | }, 45 | updatedAt: { 46 | allowNull: false, 47 | type: DataTypes.DATE 48 | } 49 | }, 50 | {} 51 | ); 52 | Action.associate = () => { 53 | // associations can be defined here 54 | }; 55 | return Action; 56 | }; 57 | -------------------------------------------------------------------------------- /src/migrations/20190118045800-create-wrap-ccc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const tableName = "WrapCCCs"; 3 | module.exports = { 4 | up: (queryInterface, DataTypes) => { 5 | return queryInterface.createTable(tableName, { 6 | transactionHash: { 7 | allowNull: false, 8 | primaryKey: true, 9 | type: DataTypes.STRING, 10 | onDelete: "CASCADE", 11 | references: { 12 | model: "Transactions", 13 | key: "hash" 14 | } 15 | }, 16 | 17 | shardId: { 18 | allowNull: false, 19 | type: DataTypes.INTEGER 20 | }, 21 | lockScriptHash: { 22 | allowNull: false, 23 | type: DataTypes.STRING 24 | }, 25 | parameters: { 26 | allowNull: false, 27 | type: DataTypes.JSONB 28 | }, 29 | quantity: { 30 | allowNull: false, 31 | type: DataTypes.NUMERIC({ precision: 20, scale: 0 }) 32 | }, 33 | 34 | recipient: { 35 | allowNull: false, 36 | type: DataTypes.STRING 37 | }, 38 | 39 | createdAt: { 40 | allowNull: false, 41 | type: DataTypes.DATE 42 | }, 43 | updatedAt: { 44 | allowNull: false, 45 | type: DataTypes.DATE 46 | } 47 | }); 48 | }, 49 | down: (queryInterface, Sequelize) => { 50 | return queryInterface.dropTable(tableName, { force: true }); 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /src/models/remove.ts: -------------------------------------------------------------------------------- 1 | import * as Sequelize from "sequelize"; 2 | 3 | export interface RemoveAttribute { 4 | transactionHash: string; 5 | textHash: string; 6 | signature: string; 7 | } 8 | 9 | export interface RemoveInstance extends Sequelize.Instance {} 10 | 11 | export default ( 12 | sequelize: Sequelize.Sequelize, 13 | DataTypes: Sequelize.DataTypes 14 | ) => { 15 | return sequelize.define( 16 | "Remove", 17 | { 18 | transactionHash: { 19 | allowNull: false, 20 | primaryKey: true, 21 | type: DataTypes.STRING, 22 | onDelete: "CASCADE", 23 | validate: { 24 | is: ["^[a-f0-9]{64}$"] 25 | }, 26 | references: { 27 | model: "Transactions", 28 | key: "hash" 29 | } 30 | }, 31 | 32 | textHash: { 33 | allowNull: false, 34 | type: DataTypes.STRING, 35 | validate: { 36 | is: ["^[a-f0-9]{64}$"] 37 | } 38 | }, 39 | signature: { 40 | allowNull: false, 41 | type: DataTypes.STRING, 42 | validate: { 43 | is: ["^[a-f0-9]{130}$"] 44 | } 45 | }, 46 | 47 | createdAt: { 48 | allowNull: false, 49 | type: DataTypes.DATE 50 | }, 51 | updatedAt: { 52 | allowNull: false, 53 | type: DataTypes.DATE 54 | } 55 | }, 56 | {} 57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as http from "http"; 2 | import { run as runChecker } from "./checker"; 3 | import { IndexerConfig } from "./config"; 4 | import { IndexerContext } from "./context"; 5 | import log from "./log"; 6 | import { createServer } from "./server"; 7 | 8 | async function runServer() { 9 | process.env.NODE_ENV = process.env.NODE_ENV || "dev"; 10 | const options = require("config") as IndexerConfig; 11 | const context = IndexerContext.newInstance(options); 12 | 13 | const rpcNetworkId = await context.sdk.rpc.chain 14 | .getNetworkId() 15 | .catch(() => "unavailable"); 16 | if (rpcNetworkId !== options.codechain.networkId) { 17 | console.error("Error: The network ID does not match."); 18 | console.error(`- Your configuration: ${options.codechain.networkId}`); 19 | console.error( 20 | `- Response from ${options.codechain.host}: ${rpcNetworkId}` 21 | ); 22 | console.error("Aborted."); 23 | return; 24 | } 25 | 26 | process.on("SIGINT", async () => { 27 | console.log("Caught interrupt signal."); 28 | await context.destroy(); 29 | process.exit(); 30 | }); 31 | const app = createServer(context); 32 | 33 | const httpServer = http.createServer(app); 34 | httpServer.listen(options.httpPort, () => { 35 | log.info( 36 | `HTTP server is listening on ${options.httpPort} in ${ 37 | process.env.NODE_ENV 38 | } mode` 39 | ); 40 | }); 41 | context.worker.run(); 42 | 43 | if (process.env.ENABLE_CCC_CHANGES_CHECK) { 44 | runChecker(context.sdk, context.options).catch(console.error); 45 | } 46 | } 47 | 48 | runServer(); 49 | -------------------------------------------------------------------------------- /src/models/logic/addressLog.ts: -------------------------------------------------------------------------------- 1 | import { SignedTransaction } from "codechain-sdk/lib/core/classes"; 2 | import { Transaction } from "sequelize"; 3 | import models from ".."; 4 | import { AddressLogInstance, AddressLogType } from "../addressLog"; 5 | import { getTracker } from "./utils/transaction"; 6 | 7 | export async function createAddressLog( 8 | transaction: SignedTransaction, 9 | address: string, 10 | type: AddressLogType, 11 | options: { transaction?: Transaction } = {} 12 | ): Promise { 13 | return models.AddressLog.create( 14 | { 15 | transactionHash: transaction.hash().value, 16 | transactionTracker: getTracker(transaction), 17 | transactionType: transaction.unsigned.type(), 18 | blockNumber: transaction.blockNumber, 19 | transactionIndex: transaction.transactionIndex, 20 | isPending: transaction.blockNumber == null, 21 | address, 22 | type 23 | }, 24 | { transaction: options.transaction } 25 | ); 26 | } 27 | 28 | export async function updateAddressLog( 29 | tx: SignedTransaction, 30 | options: { transaction?: Transaction } = {} 31 | ): Promise { 32 | return models.AddressLog.update( 33 | { 34 | blockNumber: tx.blockNumber, 35 | transactionIndex: tx.transactionIndex, 36 | success: true, 37 | isPending: false 38 | }, 39 | { 40 | where: { 41 | transactionHash: tx.hash().value 42 | }, 43 | returning: false, 44 | transaction: options.transaction 45 | } 46 | ).then(() => { 47 | return; 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /src/models/assetimage.ts: -------------------------------------------------------------------------------- 1 | import * as Sequelize from "sequelize"; 2 | 3 | export interface AssetImageAttribute { 4 | transactionHash: string; 5 | assetType: string; 6 | image: Buffer; 7 | } 8 | 9 | export interface AssetImageInstance 10 | extends Sequelize.Instance {} 11 | 12 | export default ( 13 | sequelize: Sequelize.Sequelize, 14 | DataTypes: Sequelize.DataTypes 15 | ) => { 16 | const AssetImage = sequelize.define( 17 | "AssetImage", 18 | { 19 | transactionHash: { 20 | allowNull: false, 21 | type: DataTypes.STRING, 22 | validate: { 23 | is: ["^[a-f0-9]{64}$"] 24 | } 25 | }, 26 | assetType: { 27 | primaryKey: true, 28 | allowNull: false, 29 | type: DataTypes.STRING, 30 | onDelete: "CASCADE", 31 | references: { 32 | model: "AssetSchemes", 33 | key: "assetType" 34 | }, 35 | validate: { 36 | is: ["^[a-f0-9]{40}$"] 37 | } 38 | }, 39 | image: { 40 | allowNull: false, 41 | type: DataTypes.BLOB 42 | }, 43 | createdAt: { 44 | allowNull: false, 45 | type: DataTypes.DATE 46 | }, 47 | updatedAt: { 48 | allowNull: false, 49 | type: DataTypes.DATE 50 | } 51 | }, 52 | {} 53 | ); 54 | AssetImage.associate = () => { 55 | // associations can be defined here 56 | }; 57 | return AssetImage; 58 | }; 59 | -------------------------------------------------------------------------------- /src/models/logic/assetTypeLog.ts: -------------------------------------------------------------------------------- 1 | import { SignedTransaction } from "codechain-sdk/lib/core/classes"; 2 | import { Transaction } from "sequelize"; 3 | import models from ".."; 4 | import { AssetTypeLogInstance } from "../assetTypeLog"; 5 | import { strip0xPrefix } from "./utils/format"; 6 | import { getTracker } from "./utils/transaction"; 7 | 8 | export async function createAssetTypeLog( 9 | transaction: SignedTransaction, 10 | assetType: string, 11 | options: { transaction?: Transaction } = {} 12 | ): Promise { 13 | return models.AssetTypeLog.create( 14 | { 15 | transactionHash: transaction.hash().value, 16 | transactionTracker: getTracker(transaction), 17 | transactionType: transaction.unsigned.type(), 18 | blockNumber: transaction.blockNumber, 19 | transactionIndex: transaction.transactionIndex, 20 | isPending: transaction.blockNumber == null, 21 | assetType: strip0xPrefix(assetType) 22 | }, 23 | { 24 | transaction: options.transaction 25 | } 26 | ); 27 | } 28 | 29 | export async function updateAssetTypeLog( 30 | tx: SignedTransaction, 31 | options: { transaction?: Transaction } = {} 32 | ): Promise { 33 | return models.AssetTypeLog.update( 34 | { 35 | blockNumber: tx.blockNumber, 36 | transactionIndex: tx.transactionIndex, 37 | isPending: false 38 | }, 39 | { 40 | where: { 41 | transactionHash: tx.hash().value 42 | }, 43 | returning: false, 44 | transaction: options.transaction 45 | } 46 | ).then(() => { 47 | return; 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /src/models/store.ts: -------------------------------------------------------------------------------- 1 | import * as Sequelize from "sequelize"; 2 | 3 | export interface StoreAttribute { 4 | transactionHash: string; 5 | content: string; 6 | certifier: string; 7 | signature: string; 8 | } 9 | 10 | export interface StoreInstance extends Sequelize.Instance {} 11 | 12 | export default ( 13 | sequelize: Sequelize.Sequelize, 14 | DataTypes: Sequelize.DataTypes 15 | ) => { 16 | return sequelize.define( 17 | "Store", 18 | { 19 | transactionHash: { 20 | allowNull: false, 21 | primaryKey: true, 22 | type: DataTypes.STRING, 23 | onDelete: "CASCADE", 24 | validate: { 25 | is: ["^[a-f0-9]{64}$"] 26 | }, 27 | references: { 28 | model: "Transactions", 29 | key: "hash" 30 | } 31 | }, 32 | 33 | content: { 34 | allowNull: false, 35 | type: DataTypes.STRING 36 | }, 37 | certifier: { 38 | allowNull: false, 39 | type: DataTypes.STRING 40 | }, 41 | signature: { 42 | allowNull: false, 43 | type: DataTypes.STRING, 44 | validate: { 45 | is: ["^[a-f0-9]{130}$"] 46 | } 47 | }, 48 | 49 | createdAt: { 50 | allowNull: false, 51 | type: DataTypes.DATE 52 | }, 53 | updatedAt: { 54 | allowNull: false, 55 | type: DataTypes.DATE 56 | } 57 | }, 58 | {} 59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /script/fillTransactionsCount.ts: -------------------------------------------------------------------------------- 1 | import models from "../src/models"; 2 | 3 | async function main() { 4 | const numbers = await models.Block.findAll({ 5 | attributes: ["number"], 6 | where: { 7 | transactionsCount: { 8 | [models.Sequelize.Op.eq]: null 9 | } 10 | } 11 | }).then(blocks => blocks.map(b => b.get({ plain: true }).number)); 12 | 13 | const result = ((await models.Transaction.count({ 14 | attributes: ["blockNumber"], 15 | where: { 16 | blockNumber: { 17 | [models.Sequelize.Op.in]: numbers 18 | } 19 | }, 20 | group: "blockNumber" 21 | })) as unknown) as { blockNumber: number; count: string }[]; 22 | 23 | let updated = 0; 24 | for (const { blockNumber, count } of result) { 25 | await models.Block.findOne({ 26 | where: { 27 | number: blockNumber 28 | } 29 | }).then(instance => { 30 | if (instance == null) { 31 | throw Error(`Block ${blockNumber} not found`); 32 | } 33 | updated++; 34 | return instance.update({ 35 | transactionsCount: parseInt(count, 10) 36 | }); 37 | }); 38 | } 39 | 40 | const [updated0] = await models.Block.update( 41 | { 42 | transactionsCount: 0 43 | }, 44 | { 45 | where: { 46 | transactionsCount: { 47 | [models.Sequelize.Op.eq]: null 48 | } 49 | } 50 | } 51 | ); 52 | 53 | console.log(`Updated ${updated} + ${updated0} rows`); 54 | } 55 | 56 | main() 57 | .catch(console.error) 58 | .finally(() => models.sequelize.close()); 59 | -------------------------------------------------------------------------------- /src/models/logic/account.ts: -------------------------------------------------------------------------------- 1 | import { U64 } from "codechain-sdk/lib/core/classes"; 2 | import { Transaction } from "sequelize"; 3 | import models from ".."; 4 | import * as Exception from "../../exception"; 5 | import { AccountInstance } from "../account"; 6 | 7 | export async function updateAccountOrCreate( 8 | address: string, 9 | params: { 10 | balance: U64; 11 | seq: number; 12 | }, 13 | options: { 14 | transaction?: Transaction; 15 | } = {} 16 | ): Promise { 17 | try { 18 | await models.Account.upsert( 19 | { 20 | address, 21 | balance: params.balance.value.toString(10), 22 | seq: params.seq 23 | }, 24 | { 25 | transaction: options.transaction 26 | } 27 | ); 28 | } catch (err) { 29 | console.error(err); 30 | throw Exception.DBError(); 31 | } 32 | } 33 | 34 | export async function getByAddress( 35 | address: string 36 | ): Promise { 37 | try { 38 | return await models.Account.findOne({ 39 | where: { 40 | address 41 | } 42 | }); 43 | } catch (err) { 44 | console.error(err); 45 | throw Exception.DBError(); 46 | } 47 | } 48 | 49 | export async function getAccounts(params: { 50 | page?: number | null; 51 | itemsPerPage?: number | null; 52 | }) { 53 | const { page = 1, itemsPerPage = 15 } = params; 54 | try { 55 | return await models.Account.findAll({ 56 | limit: itemsPerPage!, 57 | offset: (page! - 1) * itemsPerPage! 58 | }); 59 | } catch (err) { 60 | console.error(err); 61 | throw Exception.DBError(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/models/logic/utils/transaction.ts: -------------------------------------------------------------------------------- 1 | import { SignedTransaction } from "codechain-sdk/lib/core/classes"; 2 | import { AssetTransaction } from "codechain-sdk/lib/core/Transaction"; 3 | import { ChangeAssetSchemeActionJSON } from "codechain-sdk/lib/core/transaction/ChangeAssetScheme"; 4 | import { IncreaseAssetSupplyActionJSON } from "codechain-sdk/lib/core/transaction/IncreaseAssetSupply"; 5 | import { MintAssetActionJSON } from "codechain-sdk/lib/core/transaction/MintAsset"; 6 | import { TransferAssetActionJSON } from "codechain-sdk/lib/core/transaction/TransferAsset"; 7 | 8 | export function isAssetTransactionType(type: string) { 9 | return ( 10 | type === "mintAsset" || 11 | type === "transferAsset" || 12 | type === "increaseAssetSupply" || 13 | type === "changeAssetScheme" || 14 | type === "wrapCCC" || 15 | type === "unwrapCCC" 16 | ); 17 | } 18 | 19 | export function getTracker(transaction: SignedTransaction): string | null { 20 | return isAssetTransactionType(transaction.unsigned.type()) 21 | ? ((transaction.unsigned as unknown) as AssetTransaction) 22 | .tracker() 23 | .toString() 24 | : null; 25 | } 26 | 27 | export function getApprovals(transaction: SignedTransaction): string[] | null { 28 | const { action } = transaction.unsigned.toJSON(); 29 | switch (action.type) { 30 | case "mintAsset": 31 | return (action as MintAssetActionJSON).approvals; 32 | case "transferAsset": 33 | return (action as TransferAssetActionJSON).approvals; 34 | case "increaseAssetSupply": 35 | return (action as IncreaseAssetSupplyActionJSON).approvals; 36 | case "changeAssetScheme": 37 | return (action as ChangeAssetSchemeActionJSON).approvals; 38 | default: 39 | return null; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/sdk-examples/store-and-remove.js: -------------------------------------------------------------------------------- 1 | const SDK = require("codechain-sdk"); 2 | 3 | const sdk = new SDK({ 4 | server: process.env.CODECHAIN_RPC_HTTP || "http://localhost:8080", 5 | networkId: process.env.CODECHAIN_NETWORK_ID || "tc" 6 | }); 7 | 8 | const ACCOUNT_ADDRESS = 9 | process.env.ACCOUNT_ADDRESS || 10 | "tccq9h7vnl68frvqapzv3tujrxtxtwqdnxw6yamrrgd"; 11 | const ACCOUNT_PASSPHRASE = process.env.ACCOUNT_PASSPHRASE || "satoshi"; 12 | 13 | const privForStore = sdk.util.generatePrivateKey(); 14 | 15 | (async () => { 16 | // Store the text with a secret (= private key) 17 | const store = sdk.core.createStoreTransaction({ 18 | content: "CodeChain", 19 | secret: privForStore 20 | }); 21 | const storeResult = await sdk.rpc.account.sendTransaction({ 22 | tx: store, 23 | account: ACCOUNT_ADDRESS, 24 | passphrase: ACCOUNT_PASSPHRASE 25 | }); 26 | const storeHash = storeResult.hash; 27 | const result1 = await sdk.rpc.chain.containsTransaction(storeHash); 28 | console.log(result1); // true 29 | 30 | // To get the text, use hash of signed tx 31 | const text = await sdk.rpc.chain.getText(storeHash); 32 | console.log(text); 33 | // Text { 34 | // content: 'CodeChain', 35 | // certifier: PlatformAddress from privForStore 36 | // } 37 | 38 | // When remove, hash of signed tx is needed 39 | const remove = sdk.core.createRemoveTransaction({ 40 | hash: storeHash, 41 | secret: privForStore 42 | }); 43 | const removeResult = await sdk.rpc.account.sendTransaction({ 44 | tx: remove, 45 | account: ACCOUNT_ADDRESS, 46 | passphrase: ACCOUNT_PASSPHRASE 47 | }); 48 | const removeHash = removeResult.hash; 49 | const result2 = await sdk.rpc.chain.containsTransaction(removeHash); 50 | console.log(result2); // true 51 | })().catch(console.error); 52 | -------------------------------------------------------------------------------- /test/store-and-remove.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { Remove, Store, U64 } from "codechain-sdk/lib/core/classes"; 3 | import "mocha"; 4 | import { createBlock } from "../src/models/logic/block"; 5 | import * as TransactionModel from "../src/models/logic/transaction"; 6 | import * as Helper from "./helper"; 7 | 8 | async function check(blockNumber: number, typeName: string, typeInstance: any) { 9 | const blockResponse = (await Helper.sdk.rpc.chain.getBlock(blockNumber))!; 10 | 11 | expect(blockResponse).not.null; 12 | expect(blockResponse.transactions.length).equal(1); 13 | 14 | const blockInst = await createBlock( 15 | blockResponse, 16 | Helper.sdk, 17 | new U64("1000") 18 | ); 19 | 20 | const blockDoc = blockInst.get({ plain: true }); 21 | expect(blockDoc.hash).equal(blockResponse.hash.value); 22 | 23 | const signed = blockResponse.transactions[0]!; 24 | const unsigned = signed.unsigned; 25 | expect(unsigned instanceof typeInstance).be.true; 26 | 27 | const txInst = (await TransactionModel.getByHash(signed.hash()))!; 28 | expect(txInst).not.null; 29 | const tx = txInst.get({ plain: true }); 30 | expect(tx.hash).equal(signed.hash().value); 31 | expect(tx.type).equal(typeName); 32 | } 33 | 34 | describe("store-and-remove", function() { 35 | before(async function() { 36 | await Helper.runExample("import-test-account"); 37 | await Helper.runExample("store-and-remove"); 38 | }); 39 | 40 | it("store", async function() { 41 | const bestBlockNumber = await Helper.sdk.rpc.chain.getBestBlockNumber(); 42 | await check(bestBlockNumber - 1, "store", Store); 43 | }); 44 | 45 | it("remove", async function() { 46 | const bestBlockNumber = await Helper.sdk.rpc.chain.getBestBlockNumber(); 47 | await check(bestBlockNumber, "remove", Remove); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/models/logic/assetAddressLog.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { SignedTransaction } from "codechain-sdk/lib/core/classes"; 3 | import { Transaction } from "sequelize"; 4 | import models from ".."; 5 | import { AssetAddressLogInstance } from "../assetAddressLog"; 6 | import { strip0xPrefix } from "./utils/format"; 7 | import { getTracker, isAssetTransactionType } from "./utils/transaction"; 8 | 9 | export async function createAssetAddressLog( 10 | transaction: SignedTransaction, 11 | address: string, 12 | assetType: string, 13 | options: { transaction?: Transaction } = {} 14 | ): Promise { 15 | assert(isAssetTransactionType(transaction.unsigned.type())); 16 | return models.AssetAddressLog.create( 17 | { 18 | transactionHash: transaction.hash().value, 19 | transactionTracker: getTracker(transaction)!, 20 | transactionType: transaction.unsigned.type(), 21 | blockNumber: transaction.blockNumber, 22 | transactionIndex: transaction.transactionIndex, 23 | isPending: transaction.blockNumber == null, 24 | address, 25 | assetType: strip0xPrefix(assetType) 26 | }, 27 | { transaction: options.transaction } 28 | ); 29 | } 30 | 31 | export async function updateAssetAddressLog( 32 | transaction: SignedTransaction, 33 | options: { transaction?: Transaction } = {} 34 | ): Promise { 35 | const { blockNumber, transactionIndex } = transaction; 36 | await models.AssetAddressLog.update( 37 | { 38 | blockNumber, 39 | transactionIndex, 40 | isPending: false 41 | }, 42 | { 43 | where: { 44 | transactionHash: transaction.hash().value 45 | }, 46 | returning: false, 47 | transaction: options.transaction 48 | } 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/models/aggsUTXO.ts: -------------------------------------------------------------------------------- 1 | import * as Sequelize from "sequelize"; 2 | 3 | export interface AggsUTXOAttribute { 4 | id?: string; 5 | address: string; 6 | assetType: string; 7 | totalAssetQuantity: string; 8 | utxoQuantity: string; 9 | } 10 | 11 | export interface AggsUTXOInstance 12 | extends Sequelize.Instance {} 13 | 14 | export default ( 15 | sequelize: Sequelize.Sequelize, 16 | DataTypes: Sequelize.DataTypes 17 | ) => { 18 | const AggsUTXO = sequelize.define( 19 | "AggsUTXO", 20 | { 21 | id: { 22 | allowNull: false, 23 | autoIncrement: true, 24 | primaryKey: true, 25 | type: DataTypes.BIGINT 26 | }, 27 | address: { 28 | allowNull: false, 29 | type: DataTypes.STRING 30 | }, 31 | assetType: { 32 | allowNull: false, 33 | type: DataTypes.STRING, 34 | validate: { 35 | is: ["^[a-f0-9]{40}$"] 36 | } 37 | }, 38 | totalAssetQuantity: { 39 | allowNull: false, 40 | type: DataTypes.BIGINT 41 | }, 42 | utxoQuantity: { 43 | allowNull: false, 44 | type: DataTypes.BIGINT 45 | }, 46 | createdAt: { 47 | allowNull: false, 48 | type: DataTypes.DATE 49 | }, 50 | updatedAt: { 51 | allowNull: false, 52 | type: DataTypes.DATE 53 | } 54 | }, 55 | {} 56 | ); 57 | 58 | AggsUTXO.associate = models => { 59 | AggsUTXO.belongsTo(models.AssetScheme, { 60 | foreignKey: "assetType", 61 | as: "assetScheme", 62 | onDelete: "CASCADE" 63 | }); 64 | }; 65 | 66 | return AggsUTXO; 67 | }; 68 | -------------------------------------------------------------------------------- /src/migrations/20190212000000-create-change-asset-scheme.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const tableName = "ChangeAssetSchemes"; 3 | module.exports = { 4 | up: (queryInterface, DataTypes) => { 5 | return queryInterface.createTable(tableName, { 6 | transactionHash: { 7 | allowNull: false, 8 | primaryKey: true, 9 | type: DataTypes.STRING, 10 | onDelete: "CASCADE", 11 | references: { 12 | model: "Transactions", 13 | key: "hash" 14 | } 15 | }, 16 | assetType: { 17 | allowNull: false, 18 | type: DataTypes.STRING 19 | }, 20 | networkId: { 21 | allowNull: false, 22 | type: DataTypes.STRING 23 | }, 24 | shardId: { 25 | allowNull: false, 26 | type: DataTypes.INTEGER 27 | }, 28 | metadata: { 29 | allowNull: false, 30 | type: DataTypes.TEXT 31 | }, 32 | approver: { 33 | type: DataTypes.STRING 34 | }, 35 | registrar: { 36 | type: DataTypes.STRING 37 | }, 38 | allowedScriptHashes: { 39 | allowNull: false, 40 | type: DataTypes.JSONB 41 | }, 42 | approvals: { 43 | allowNull: false, 44 | type: DataTypes.JSONB 45 | }, 46 | createdAt: { 47 | allowNull: false, 48 | type: DataTypes.DATE 49 | }, 50 | updatedAt: { 51 | allowNull: false, 52 | type: DataTypes.DATE 53 | } 54 | }); 55 | }, 56 | down: (queryInterface, Sequelize) => { 57 | return queryInterface.dropTable(tableName, { force: true }); 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /src/migrations/20191114103355-change-utxo-table-index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | up: async (queryInterface, Sequelize) => { 5 | await queryInterface.removeIndex("UTXOs", "u_t_x_os_address_id"); 6 | await queryInterface.removeIndex( 7 | "UTXOs", 8 | "UTXOs_assetType_usedBlockNumber" 9 | ); 10 | 11 | await queryInterface.addIndex("UTXOs", [ 12 | "address", 13 | "blockNumber", 14 | "transactionIndex", 15 | "transactionOutputIndex" 16 | ]); 17 | await queryInterface.addIndex("UTXOs", [ 18 | "assetType", 19 | "blockNumber", 20 | "transactionIndex", 21 | "transactionOutputIndex" 22 | ]); 23 | await queryInterface.addIndex("UTXOs", [ 24 | "address", 25 | "assetType", 26 | "blockNumber", 27 | "transactionIndex", 28 | "transactionOutputIndex" 29 | ]); 30 | }, 31 | 32 | down: async (queryInterface, Sequelize) => { 33 | await queryInterface.removeIndex("UTXOs", [ 34 | "address", 35 | "blockNumber", 36 | "transactionIndex", 37 | "transactionOutputIndex" 38 | ]); 39 | await queryInterface.removeIndex("UTXOs", [ 40 | "assetType", 41 | "blockNumber", 42 | "transactionIndex", 43 | "transactionOutputIndex" 44 | ]); 45 | await queryInterface.removeIndex("UTXOs", [ 46 | "address", 47 | "assetType", 48 | "blockNumber", 49 | "transactionIndex", 50 | "transactionOutputIndex" 51 | ]); 52 | await queryInterface.addIndex("UTXOs", ["address", "id"]); 53 | await queryInterface.addIndex("UTXOs", { 54 | fields: ["assetType", "usedBlockNumber"], 55 | name: "UTXOs_assetType_usedBlockNumber" 56 | }); 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /src/migrations/20190116201000-create-transfer-asset.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const tableName = "TransferAssets"; 3 | module.exports = { 4 | up: (queryInterface, DataTypes) => { 5 | return queryInterface.createTable(tableName, { 6 | transactionHash: { 7 | allowNull: false, 8 | primaryKey: true, 9 | type: DataTypes.STRING, 10 | onDelete: "CASCADE", 11 | references: { 12 | model: "Transactions", 13 | key: "hash" 14 | } 15 | }, 16 | 17 | networkId: { 18 | allowNull: false, 19 | type: DataTypes.STRING 20 | }, 21 | metadata: { 22 | allowNull: false, 23 | type: DataTypes.STRING 24 | }, 25 | approvals: { 26 | allowNull: false, 27 | type: DataTypes.JSONB 28 | }, 29 | expiration: { 30 | allowNull: true, 31 | type: DataTypes.NUMERIC({ precision: 20, scale: 0 }) 32 | }, 33 | inputs: { 34 | allowNull: false, 35 | type: DataTypes.JSONB 36 | }, 37 | burns: { 38 | allowNull: false, 39 | type: DataTypes.JSONB 40 | }, 41 | outputs: { 42 | allowNull: false, 43 | type: DataTypes.JSONB 44 | }, 45 | orders: { 46 | allowNull: false, 47 | type: DataTypes.JSONB 48 | }, 49 | 50 | createdAt: { 51 | allowNull: false, 52 | type: DataTypes.DATE 53 | }, 54 | updatedAt: { 55 | allowNull: false, 56 | type: DataTypes.DATE 57 | } 58 | }); 59 | }, 60 | down: (queryInterface, Sequelize) => { 61 | return queryInterface.dropTable(tableName, { force: true }); 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /script/fillTransactionsCountByType.ts: -------------------------------------------------------------------------------- 1 | import { SDK } from "codechain-sdk"; 2 | import { Block } from "codechain-sdk/lib/core/Block"; 3 | 4 | import models from "../src/models"; 5 | 6 | async function main() { 7 | const sdk = new SDK({ server: "http://localhost:8080", networkId: "cc" }); 8 | const numbers = await models.Block.findAll({ 9 | attributes: ["number"], 10 | where: { 11 | transactionsCountByType: { 12 | [models.Sequelize.Op.eq]: null 13 | } 14 | } 15 | }).then(blocks => blocks.map(b => b.get({ plain: true }).number)); 16 | 17 | console.log(`${numbers.length} rows to update found`); 18 | 19 | let updated = 0; 20 | for (const n of numbers) { 21 | await models.Block.findOne({ 22 | where: { 23 | number: n 24 | } 25 | }).then(async instance => { 26 | if (instance == null) { 27 | throw Error(`Block ${n} not found`); 28 | } 29 | const block = await sdk.rpc.chain.getBlock(n); 30 | if (block == null) { 31 | throw Error(`Failed to get Block ${n}`); 32 | } 33 | updated++; 34 | if (updated % 500 === 0) { 35 | console.log(updated); 36 | } 37 | return instance.update({ 38 | transactionsCountByType: getTransactionsCountByType(block!) 39 | }); 40 | }); 41 | } 42 | 43 | console.log(`Updated ${updated} rows`); 44 | } 45 | 46 | main() 47 | .catch(console.error) 48 | .finally(() => models.sequelize.close()); 49 | 50 | function getTransactionsCountByType(block: Block) { 51 | const transactionsCountByType: { [type: string]: number } = {}; 52 | block.transactions.forEach(t => { 53 | if (transactionsCountByType[t.unsigned.type()] == null) { 54 | transactionsCountByType[t.unsigned.type()] = 0; 55 | } 56 | transactionsCountByType[t.unsigned.type()] += 1; 57 | }); 58 | return transactionsCountByType; 59 | } 60 | -------------------------------------------------------------------------------- /src/migrations/20190116201000-create-increase-asset-supply.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const tableName = "IncreaseAssetSupplies"; 3 | module.exports = { 4 | up: (queryInterface, DataTypes) => { 5 | return queryInterface.createTable(tableName, { 6 | transactionHash: { 7 | allowNull: false, 8 | primaryKey: true, 9 | type: DataTypes.STRING, 10 | onDelete: "CASCADE", 11 | references: { 12 | model: "Transactions", 13 | key: "hash" 14 | } 15 | }, 16 | assetType: { 17 | allowNull: false, 18 | type: DataTypes.STRING 19 | }, 20 | networkId: { 21 | allowNull: false, 22 | type: DataTypes.STRING 23 | }, 24 | shardId: { 25 | allowNull: false, 26 | type: DataTypes.INTEGER 27 | }, 28 | approvals: { 29 | allowNull: false, 30 | type: DataTypes.JSONB 31 | }, 32 | lockScriptHash: { 33 | allowNull: false, 34 | type: DataTypes.STRING 35 | }, 36 | parameters: { 37 | allowNull: false, 38 | type: DataTypes.JSONB 39 | }, 40 | supply: { 41 | allowNull: false, 42 | type: DataTypes.NUMERIC({ precision: 20, scale: 0 }) 43 | }, 44 | recipient: { 45 | allowNull: false, 46 | type: DataTypes.STRING 47 | }, 48 | 49 | createdAt: { 50 | allowNull: false, 51 | type: DataTypes.DATE 52 | }, 53 | updatedAt: { 54 | allowNull: false, 55 | type: DataTypes.DATE 56 | } 57 | }); 58 | }, 59 | down: (queryInterface, Sequelize) => { 60 | return queryInterface.dropTable(tableName, { force: true }); 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /src/models/wrapCCC.ts: -------------------------------------------------------------------------------- 1 | import * as Sequelize from "sequelize"; 2 | 3 | export interface WrapCCCAttribute { 4 | transactionHash: string; 5 | shardId: number; 6 | lockScriptHash: string; 7 | parameters: string[]; 8 | quantity: string; 9 | recipient: string; 10 | } 11 | 12 | export interface WrapCCCInstance extends Sequelize.Instance {} 13 | 14 | export default ( 15 | sequelize: Sequelize.Sequelize, 16 | DataTypes: Sequelize.DataTypes 17 | ) => { 18 | return sequelize.define( 19 | "WrapCCC", 20 | { 21 | transactionHash: { 22 | allowNull: false, 23 | primaryKey: true, 24 | type: DataTypes.STRING, 25 | onDelete: "CASCADE", 26 | validate: { 27 | is: ["^[a-f0-9]{64}$"] 28 | }, 29 | references: { 30 | model: "Transactions", 31 | key: "hash" 32 | } 33 | }, 34 | 35 | shardId: { 36 | allowNull: false, 37 | type: DataTypes.INTEGER 38 | }, 39 | lockScriptHash: { 40 | allowNull: false, 41 | type: Sequelize.STRING, 42 | validate: { 43 | is: ["^[a-f0-9]{40}$"] 44 | } 45 | }, 46 | parameters: { 47 | allowNull: false, 48 | type: Sequelize.JSONB 49 | }, 50 | quantity: { 51 | allowNull: false, 52 | type: Sequelize.NUMERIC({ precision: 20, scale: 0 }) 53 | }, 54 | 55 | recipient: { 56 | allowNull: false, 57 | type: DataTypes.STRING 58 | }, 59 | 60 | createdAt: { 61 | allowNull: false, 62 | type: DataTypes.DATE 63 | }, 64 | updatedAt: { 65 | allowNull: false, 66 | type: DataTypes.DATE 67 | } 68 | }, 69 | {} 70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import * as bodyParser from "body-parser"; 2 | import * as express from "express"; 3 | import swaggerJSDoc = require("swagger-jsdoc"); 4 | import * as swaggerUi from "swagger-ui-express"; 5 | import { createApiRouter } from "./api"; 6 | import { IndexerConfig } from "./config"; 7 | import { IndexerContext } from "./context"; 8 | import log from "./log"; 9 | 10 | const config = require("config") as IndexerConfig; 11 | const { publicAddress, httpPort } = config; 12 | const swaggerDefinition = { 13 | info: { 14 | title: "CodeChain Indexer API", 15 | version: "1.0.0" 16 | }, 17 | host: `${publicAddress}:${httpPort}`, 18 | basePath: "/api" 19 | }; 20 | 21 | const options = { 22 | swaggerDefinition, 23 | apis: ["src/routers/*.ts"] 24 | }; 25 | 26 | const swaggerSpec = swaggerJSDoc(options); 27 | 28 | export function createServer(context: IndexerContext) { 29 | const app = express(); 30 | 31 | // Enable reverse proxy support in Express. This causes the 32 | // the "X-Forwarded-Proto" header field to be trusted so its 33 | // value can be used to determine the protocol. See 34 | // http://expressjs.com/api#app-settings for more details. 35 | app.enable("trust proxy"); 36 | app.use(bodyParser.urlencoded({ extended: true })); 37 | app.use( 38 | bodyParser.json({ 39 | type: () => true // Treat all other content types as application/json 40 | }) 41 | ); 42 | 43 | if (process.env.NODE_ENV === "dev") { 44 | app.use( 45 | "/api-docs", 46 | swaggerUi.serve, 47 | swaggerUi.setup(swaggerSpec, { explorer: true }) 48 | ); 49 | } 50 | 51 | app.use("/api", createApiRouter(context, true)); 52 | app.use(handleErrors); 53 | 54 | return app; 55 | } 56 | 57 | const handleErrors: express.ErrorRequestHandler = (err, _R, res, next) => { 58 | if (err.status >= 400 && err.status < 500) { 59 | if (process.env.NODE_ENV === "production") { 60 | return res.status(err.status).send(err.statusText); 61 | } else { 62 | return res.status(err.status).send(err); 63 | } 64 | } 65 | 66 | log.error(err); 67 | next(err); 68 | }; 69 | -------------------------------------------------------------------------------- /script/fixBalanceAndSeq.ts: -------------------------------------------------------------------------------- 1 | import { SDK } from "codechain-sdk"; 2 | 3 | import models from "../src/models"; 4 | 5 | async function main() { 6 | const sdk = new SDK({ server: "http://localhost:8080", networkId: "cc" }); 7 | 8 | await models.Account.findAll({ 9 | attributes: ["address", "balance", "seq"] 10 | }).then(async accounts => { 11 | console.log(`Total # of accounts: ${accounts.length}`); 12 | 13 | let counter = 0; 14 | let countOfWrongBalance = 0; 15 | let countOfWrongSeq = 0; 16 | let countOfWrongAccount = 0; 17 | for (const account of accounts) { 18 | const { address, balance, seq } = account.get(); 19 | const update: { balance?: string; seq?: number } = {}; 20 | 21 | let flag = false; 22 | 23 | const getBalanceResult = await sdk.rpc.chain.getBalance(address); 24 | if (balance !== getBalanceResult.toString()) { 25 | countOfWrongBalance++; 26 | update.balance = getBalanceResult.toString(); 27 | flag = true; 28 | } 29 | 30 | const getSeqResult = await sdk.rpc.chain.getSeq(address); 31 | if (seq !== getSeqResult) { 32 | countOfWrongSeq++; 33 | update.seq = getSeqResult; 34 | flag = true; 35 | } 36 | 37 | if (flag) { 38 | countOfWrongAccount++; 39 | await account.update(update); 40 | } 41 | 42 | counter++; 43 | if (counter % 500 === 0) { 44 | console.log(`counter: ${counter}`); 45 | console.log(`# of wrong balance: ${countOfWrongBalance}`); 46 | console.log(`# of wrong seq: ${countOfWrongSeq}`); 47 | console.log(`# of wrong account: ${countOfWrongAccount}`); 48 | } 49 | } 50 | 51 | console.log("========================================"); 52 | console.log(`# of wrong balance: ${countOfWrongBalance}`); 53 | console.log(`# of wrong seq: ${countOfWrongSeq}`); 54 | console.log(`# of wrong account: ${countOfWrongAccount}`); 55 | }); 56 | } 57 | 58 | main() 59 | .catch(console.error) 60 | .finally(() => models.sequelize.close()); 61 | -------------------------------------------------------------------------------- /src/migrations/20181220093812-create-asset-scheme.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => { 4 | return queryInterface 5 | .createTable("AssetSchemes", { 6 | transactionHash: { 7 | allowNull: false, 8 | type: Sequelize.STRING, 9 | onDelete: "CASCADE", 10 | references: { 11 | model: "Transactions", 12 | key: "hash" 13 | } 14 | }, 15 | assetType: { 16 | allowNull: false, 17 | primaryKey: true, 18 | type: Sequelize.STRING 19 | }, 20 | shardId: { 21 | allowNull: false, 22 | type: Sequelize.INTEGER 23 | }, 24 | metadata: { 25 | allowNull: false, 26 | type: Sequelize.TEXT 27 | }, 28 | approver: { 29 | type: Sequelize.STRING 30 | }, 31 | registrar: { 32 | type: Sequelize.STRING 33 | }, 34 | allowedScriptHashes: { 35 | allowNull: false, 36 | type: Sequelize.JSONB 37 | }, 38 | 39 | supply: { 40 | type: Sequelize.NUMERIC({ precision: 20, scale: 0 }) 41 | }, 42 | networkId: { 43 | type: Sequelize.STRING 44 | }, 45 | createdAt: { 46 | allowNull: false, 47 | type: Sequelize.DATE 48 | }, 49 | updatedAt: { 50 | allowNull: false, 51 | type: Sequelize.DATE 52 | } 53 | }) 54 | .then(() => 55 | queryInterface.addIndex("AssetSchemes", { 56 | fields: ["transactionHash"], 57 | indexType: "Hash" 58 | }) 59 | ); 60 | }, 61 | down: (queryInterface, Sequelize) => { 62 | return queryInterface.dropTable("AssetSchemes", { force: true }); 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /src/models/logic/changeAssetScheme.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ChangeAssetScheme, 3 | SignedTransaction 4 | } from "codechain-sdk/lib/core/classes"; 5 | import { ChangeAssetSchemeActionJSON } from "codechain-sdk/lib/core/transaction/ChangeAssetScheme"; 6 | import { Transaction } from "sequelize"; 7 | import models from ".."; 8 | import { ChangeAssetSchemeInstance } from "../changeAssetScheme"; 9 | import * as AssetImageModel from "./assetimage"; 10 | import { createAssetTypeLog } from "./assetTypeLog"; 11 | import { strip0xPrefix } from "./utils/format"; 12 | 13 | export async function createChangeAssetScheme( 14 | transaction: SignedTransaction, 15 | options: { transaction?: Transaction } = {} 16 | ): Promise { 17 | const transactionHash = transaction.hash().value; 18 | const changeAssetScheme = transaction.unsigned as ChangeAssetScheme; 19 | const { 20 | assetType, 21 | networkId, 22 | shardId, 23 | metadata, 24 | approver, 25 | registrar, 26 | allowedScriptHashes, 27 | approvals, 28 | seq 29 | } = changeAssetScheme.toJSON().action as ChangeAssetSchemeActionJSON; 30 | const inst = await models.ChangeAssetScheme.create( 31 | { 32 | transactionHash: strip0xPrefix(transactionHash), 33 | assetType: strip0xPrefix(assetType), 34 | networkId, 35 | shardId, 36 | metadata, 37 | approver, 38 | registrar, 39 | allowedScriptHashes: allowedScriptHashes.map(hash => 40 | strip0xPrefix(hash) 41 | ), 42 | approvals, 43 | seq 44 | }, 45 | { transaction: options.transaction } 46 | ); 47 | let metadataObj; 48 | try { 49 | metadataObj = JSON.parse(metadata); 50 | } catch (e) { 51 | // The metadata can be non-JSON. 52 | } 53 | if (metadataObj && metadataObj.icon_url) { 54 | // NOTE: No await here because the URL might be unreachable or slow 55 | AssetImageModel.createAssetImage( 56 | transactionHash, 57 | strip0xPrefix(assetType), 58 | metadataObj.icon_url 59 | ); 60 | } 61 | await createAssetTypeLog(transaction, assetType, options); 62 | return inst; 63 | } 64 | -------------------------------------------------------------------------------- /src/models/log.ts: -------------------------------------------------------------------------------- 1 | import * as Sequelize from "sequelize"; 2 | 3 | export interface LogAttribute { 4 | id: string; 5 | date: string; 6 | count: number; 7 | type: LogType; 8 | value?: string | null; 9 | } 10 | 11 | export enum LogType { 12 | BLOCK_COUNT = "BLOCK_COUNT", 13 | BLOCK_MINING_COUNT = "BLOCK_MINING_COUNT", 14 | TX_COUNT = "TX_COUNT", 15 | PAY_COUNT = "PAY_COUNT", 16 | SET_REGULAR_KEY_COUNT = "SET_REGULAR_KEY_COUNT", 17 | SET_SHARD_OWNER_COUNT = "SET_SHARD_OWNER_COUNT", 18 | SET_SHARD_USER_COUNT = "SET_SHARD_USER_COUNT", 19 | CREATE_SHARD_COUNT = "CREATE_SHARD_COUNT", 20 | MINT_ASSET_COUNT = "MINT_ASSET_COUNT", 21 | TRANSFER_ASSET_COUNT = "TRANSFER_ASSET_COUNT", 22 | CHANGE_ASSET_SCHEME_COUNT = "CHANGE_ASSET_SCHEME_COUNT", 23 | INCREASE_ASSET_SUPPLY_COUNT = "INCREASE_ASSET_SUPPLY_COUNT", 24 | STORE_COUNT = "STORE_COUNT", 25 | REMOVE_COUNT = "REMOVE_COUNT", 26 | CUSTOM_COUNT = "CUSTOM_COUNT", 27 | UNWRAP_CCC_COUNT = "UNWRAP_CCC_COUNT", 28 | WRAP_CCC_COUNT = "WRAP_CCC_COUNT" 29 | } 30 | 31 | export interface LogInstance extends Sequelize.Instance {} 32 | 33 | export default ( 34 | sequelize: Sequelize.Sequelize, 35 | DataTypes: Sequelize.DataTypes 36 | ) => { 37 | const Log = sequelize.define( 38 | "Log", 39 | { 40 | id: { 41 | allowNull: false, 42 | primaryKey: true, 43 | type: DataTypes.STRING 44 | }, 45 | date: { 46 | allowNull: false, 47 | type: DataTypes.STRING 48 | }, 49 | count: { 50 | allowNull: false, 51 | type: DataTypes.INTEGER 52 | }, 53 | type: { 54 | allowNull: false, 55 | type: DataTypes.STRING 56 | }, 57 | value: { 58 | type: DataTypes.STRING 59 | }, 60 | createdAt: { 61 | allowNull: false, 62 | type: DataTypes.DATE 63 | }, 64 | updatedAt: { 65 | allowNull: false, 66 | type: DataTypes.DATE 67 | } 68 | }, 69 | {} 70 | ); 71 | Log.associate = () => { 72 | // associations can be defined here 73 | }; 74 | return Log; 75 | }; 76 | -------------------------------------------------------------------------------- /src/models/logic/utils/address.ts: -------------------------------------------------------------------------------- 1 | import { AssetAddress, H160 } from "codechain-sdk/lib/core/classes"; 2 | import { TransactionAttribute } from "../../transaction"; 3 | 4 | const P2PKH = "5f5960a7bca6ceeeb0c97bc717562914e7a1de04"; 5 | const P2PKHBURN = "37572bdcc22d39a59c0d12d301f6271ba3fdd451"; 6 | 7 | export function getOwner( 8 | lockScriptHash: H160, 9 | parameters: string[], 10 | networkId: string 11 | ) { 12 | let owner = ""; 13 | if (lockScriptHash.value === P2PKH) { 14 | owner = AssetAddress.fromTypeAndPayload(1, new H160(parameters[0]), { 15 | networkId 16 | }).value; 17 | } else if (lockScriptHash.value === P2PKHBURN) { 18 | owner = AssetAddress.fromTypeAndPayload(2, new H160(parameters[0]), { 19 | networkId 20 | }).value; 21 | } else if (parameters.length === 0) { 22 | owner = AssetAddress.fromTypeAndPayload(0, lockScriptHash, { 23 | networkId 24 | }).value; 25 | } 26 | return owner; 27 | } 28 | 29 | export function getOwnerFromTransaction( 30 | tx: TransactionAttribute, 31 | outputIndex: number 32 | ) { 33 | if (tx.mintAsset && outputIndex === 0) { 34 | const { lockScriptHash, parameters, recipient } = tx.mintAsset; 35 | return { 36 | lockScriptHash, 37 | parameters, 38 | owner: recipient 39 | }; 40 | } else if ( 41 | tx.transferAsset && 42 | outputIndex < tx.transferAsset.outputs!.length 43 | ) { 44 | const { lockScriptHash, parameters, owner } = tx.transferAsset.outputs![ 45 | outputIndex 46 | ]; 47 | return { lockScriptHash, parameters, owner }; 48 | } else if (tx.increaseAssetSupply) { 49 | const { 50 | lockScriptHash, 51 | parameters, 52 | recipient 53 | } = tx.increaseAssetSupply; 54 | return { 55 | lockScriptHash, 56 | parameters, 57 | owner: recipient 58 | }; 59 | } else if (tx.wrapCCC) { 60 | const { lockScriptHash, parameters, recipient } = tx.wrapCCC; 61 | return { 62 | lockScriptHash, 63 | parameters, 64 | owner: recipient 65 | }; 66 | } 67 | return { 68 | lockScriptHash: null, 69 | parameters: null, 70 | owner: null 71 | }; 72 | } 73 | -------------------------------------------------------------------------------- /src/models/logic/utils/custom.ts: -------------------------------------------------------------------------------- 1 | import { PlatformAddress, U64 } from "codechain-primitives/lib"; 2 | import { SDK } from "codechain-sdk"; 3 | import { 4 | getBanned, 5 | getCandidates, 6 | getJailed, 7 | getTermMetadata 8 | } from "codechain-stakeholder-sdk"; 9 | 10 | interface BecomeEligible { 11 | address: PlatformAddress; 12 | deposit: U64; 13 | } 14 | 15 | export async function getBecomeEligible( 16 | sdk: SDK, 17 | blockNumber: number 18 | ): Promise { 19 | const termMetadata = await getTermMetadata(sdk, blockNumber); 20 | if ( 21 | !termMetadata || 22 | termMetadata.lastTermFinishedBlockNumber === 0 || 23 | termMetadata.lastTermFinishedBlockNumber !== blockNumber 24 | ) { 25 | return []; 26 | } 27 | 28 | const result: BecomeEligible[] = []; 29 | 30 | const currentCandidateOrJailedOrBanned = new Set([ 31 | ...(await getCandidates(sdk, blockNumber)).map( 32 | ({ pubkey }) => 33 | PlatformAddress.fromPublic(pubkey, { networkId: sdk.networkId }) 34 | .value 35 | ), 36 | ...(await getJailed(sdk, blockNumber)).map( 37 | ({ address }) => address.value 38 | ), 39 | ...(await getBanned(sdk, blockNumber)).map(address => address.value) 40 | ]); 41 | 42 | // candidates -> eligible 43 | const previousCandidates = await getCandidates(sdk, blockNumber - 1); 44 | for (const { pubkey, deposit } of previousCandidates) { 45 | const address = PlatformAddress.fromPublic(pubkey, { 46 | networkId: sdk.networkId 47 | }); 48 | if (currentCandidateOrJailedOrBanned.has(address.value)) { 49 | continue; 50 | } 51 | if (deposit.gt(0)) { 52 | result.push({ 53 | address, 54 | deposit 55 | }); 56 | } 57 | } 58 | 59 | // jailed -> eligible 60 | const previousJailed = await getJailed(sdk, blockNumber - 1); 61 | for (const { address, deposit } of previousJailed) { 62 | if (currentCandidateOrJailedOrBanned.has(address.value)) { 63 | continue; 64 | } 65 | if (deposit.gt(0)) { 66 | result.push({ 67 | address, 68 | deposit 69 | }); 70 | } 71 | } 72 | return result; 73 | } 74 | -------------------------------------------------------------------------------- /test/pending-tx.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { H256 } from "codechain-sdk/lib/core/classes"; 3 | import "mocha"; 4 | import * as TxModel from "../src/models/logic/transaction"; 5 | import * as Helper from "./helper"; 6 | 7 | describe("pending-tx", function() { 8 | let p: Promise | null = null; 9 | 10 | before(async function() { 11 | await Helper.runExample("import-test-account"); 12 | }); 13 | 14 | after(async function() { 15 | await Helper.sdk.rpc.devel.startSealing(); 16 | if (p !== null) { 17 | await p; 18 | } 19 | }); 20 | 21 | it("Check pending transactions", async function() { 22 | await Helper.sdk.rpc.devel.stopSealing(); 23 | let pendingTx; 24 | try { 25 | p = Helper.runExample("send-signed-tx"); 26 | await waitForSecond(2); 27 | await Helper.worker.sync(); 28 | 29 | const pendingTransactionsInst = await TxModel.getPendingTransactions( 30 | { 31 | itemsPerPage: 15 32 | } 33 | ); 34 | expect(pendingTransactionsInst.length).equal(1); 35 | 36 | pendingTx = await pendingTransactionsInst![0]!.get(); 37 | expect(pendingTx.isPending).be.true; 38 | } finally { 39 | await Helper.sdk.rpc.devel.startSealing(); 40 | } 41 | 42 | while ( 43 | (await Helper.sdk.rpc.chain.getPendingTransactions()).transactions 44 | .length !== 0 45 | ) { 46 | await waitForSecond(1); 47 | console.log("waiting ..."); 48 | } 49 | await Helper.worker.sync(); 50 | 51 | const newPendingTransactions = await TxModel.getPendingTransactions({ 52 | itemsPerPage: 15 53 | }); 54 | expect(newPendingTransactions.length).equal(0); 55 | 56 | const indexedTransactionInst = await TxModel.getByHash( 57 | new H256(pendingTx.hash) 58 | ); 59 | expect(indexedTransactionInst).not.null; 60 | const indexedTransaction = indexedTransactionInst!.get(); 61 | expect(indexedTransaction.isPending).be.false; 62 | }); 63 | }); 64 | 65 | function waitForSecond(second: number) { 66 | return new Promise(resolve => { 67 | setTimeout(() => { 68 | resolve(); 69 | }, second * 1000); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /test/sdk-examples/wrap-ccc-and-unwrap-ccc.js: -------------------------------------------------------------------------------- 1 | const SDK = require("codechain-sdk"); 2 | 3 | const sdk = new SDK({ 4 | server: process.env.CODECHAIN_RPC_HTTP || "http://localhost:8080", 5 | networkId: process.env.CODECHAIN_NETWORK_ID || "tc" 6 | }); 7 | 8 | const ACCOUNT_ADDRESS = 9 | process.env.ACCOUNT_ADDRESS || 10 | "tccq9h7vnl68frvqapzv3tujrxtxtwqdnxw6yamrrgd"; 11 | const ACCOUNT_PASSPHRASE = process.env.ACCOUNT_PASSPHRASE || "satoshi"; 12 | 13 | (async () => { 14 | const address = await sdk.key.createAssetAddress({ 15 | type: "P2PKHBurn" 16 | }); 17 | const quantity = 100; 18 | 19 | const balanceStart = await sdk.rpc.chain.getBalance(ACCOUNT_ADDRESS); 20 | 21 | // Wrap 100 CCC into the wrapped CCC asset type. 22 | const wrapCCC = sdk.core.createWrapCCCTransaction({ 23 | shardId: 0, 24 | recipient: address, 25 | quantity, 26 | payer: ACCOUNT_ADDRESS 27 | }); 28 | const wrapCCCSignedHash = await sdk.rpc.chain.sendTransaction(wrapCCC, { 29 | account: ACCOUNT_ADDRESS, 30 | passphrase: ACCOUNT_PASSPHRASE 31 | }); 32 | const wrapCCCResult = await sdk.rpc.chain.containsTransaction( 33 | wrapCCCSignedHash 34 | ); 35 | if (!wrapCCCResult) { 36 | throw Error("WrapCCC failed"); 37 | } 38 | const balanceAfterWrapCCC = await sdk.rpc.chain.getBalance(ACCOUNT_ADDRESS); 39 | console.log("Wrap finish"); 40 | 41 | // Unwrap the wrapped CCC asset created before. 42 | const unwrapCCCTx = sdk.core.createUnwrapCCCTransaction({ 43 | burn: wrapCCC.getAsset(), // After sendTransaction, the fee and seq field of wrapCCC is filled. 44 | receiver: ACCOUNT_ADDRESS 45 | }); 46 | await sdk.key.signTransactionBurn(unwrapCCCTx, 0); 47 | const hash = await sdk.rpc.chain.sendTransaction(unwrapCCCTx, { 48 | account: ACCOUNT_ADDRESS, 49 | passphrase: ACCOUNT_PASSPHRASE 50 | }); 51 | const unwrapCCCTxResult = await sdk.rpc.chain.containsTransaction(hash); 52 | if (!unwrapCCCTxResult) { 53 | throw Error("AssetUnwrapCCCTransaction failed"); 54 | } 55 | const balanceAfterUnwrapCCC = await sdk.rpc.chain.getBalance( 56 | ACCOUNT_ADDRESS 57 | ); 58 | 59 | console.log(balanceStart.toString()); 60 | console.log(balanceAfterWrapCCC.toString()); 61 | console.log(balanceAfterUnwrapCCC.toString()); 62 | })().catch(console.error); 63 | -------------------------------------------------------------------------------- /src/migrations/20190326000001-create-asset-type-log.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const tableName = "AssetTypeLogs"; 3 | module.exports = { 4 | up: (queryInterface, DataTypes) => { 5 | return queryInterface 6 | .createTable(tableName, { 7 | id: { 8 | allowNull: false, 9 | autoIncrement: true, 10 | primaryKey: true, 11 | type: DataTypes.BIGINT 12 | }, 13 | transactionHash: { 14 | allowNull: false, 15 | type: DataTypes.STRING, 16 | onDelete: "CASCADE", 17 | references: { 18 | model: "Transactions", 19 | key: "hash" 20 | } 21 | }, 22 | transactionType: { 23 | allowNull: false, 24 | type: DataTypes.STRING 25 | }, 26 | transactionTracker: { 27 | type: DataTypes.STRING 28 | }, 29 | blockNumber: { 30 | type: DataTypes.INTEGER 31 | }, 32 | transactionIndex: { 33 | type: DataTypes.INTEGER 34 | }, 35 | isPending: { 36 | allowNull: false, 37 | type: DataTypes.BOOLEAN 38 | }, 39 | assetType: { 40 | allowNull: false, 41 | type: DataTypes.STRING, 42 | validate: { 43 | is: ["^[a-f0-9]{40}$"] 44 | } 45 | }, 46 | createdAt: { 47 | allowNull: false, 48 | type: DataTypes.DATE 49 | }, 50 | updatedAt: { 51 | allowNull: false, 52 | type: DataTypes.DATE 53 | } 54 | }) 55 | .then(() => 56 | queryInterface.addIndex(tableName, { 57 | fields: ["transactionHash"], 58 | indexType: "Hash" 59 | }) 60 | ) 61 | .then(() => queryInterface.addIndex(tableName, ["assetType"])); 62 | }, 63 | down: (queryInterface, Sequelize) => { 64 | return queryInterface.dropTable(tableName, { force: true }); 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /src/migrations/20190326000000-create-address-log.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const tableName = "AddressLogs"; 3 | module.exports = { 4 | up: (queryInterface, DataTypes) => { 5 | return queryInterface 6 | .createTable(tableName, { 7 | id: { 8 | allowNull: false, 9 | autoIncrement: true, 10 | primaryKey: true, 11 | type: DataTypes.BIGINT 12 | }, 13 | transactionHash: { 14 | allowNull: false, 15 | type: DataTypes.STRING, 16 | onDelete: "CASCADE", 17 | references: { 18 | model: "Transactions", 19 | key: "hash" 20 | } 21 | }, 22 | transactionType: { 23 | allowNull: false, 24 | type: DataTypes.STRING 25 | }, 26 | transactionTracker: { 27 | type: DataTypes.STRING 28 | }, 29 | blockNumber: { 30 | type: DataTypes.INTEGER 31 | }, 32 | transactionIndex: { 33 | type: DataTypes.INTEGER 34 | }, 35 | success: { 36 | type: DataTypes.BOOLEAN 37 | }, 38 | isPending: { 39 | allowNull: false, 40 | type: DataTypes.BOOLEAN 41 | }, 42 | address: { 43 | allowNull: false, 44 | type: DataTypes.STRING 45 | }, 46 | type: { 47 | allowNull: false, 48 | type: DataTypes.STRING 49 | }, 50 | createdAt: { 51 | allowNull: false, 52 | type: DataTypes.DATE 53 | }, 54 | updatedAt: { 55 | allowNull: false, 56 | type: DataTypes.DATE 57 | } 58 | }) 59 | .then(() => 60 | queryInterface.addIndex(tableName, { 61 | fields: ["transactionHash"], 62 | indexType: "Hash" 63 | }) 64 | ) 65 | .then(() => queryInterface.addIndex(tableName, ["address"])); 66 | }, 67 | down: (queryInterface, Sequelize) => { 68 | return queryInterface.dropTable(tableName, { force: true }); 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /src/migrations/20190116201000-create-mint-asset.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const tableName = "MintAssets"; 3 | module.exports = { 4 | up: (queryInterface, DataTypes) => { 5 | return queryInterface.createTable(tableName, { 6 | transactionHash: { 7 | allowNull: false, 8 | primaryKey: true, 9 | type: DataTypes.STRING, 10 | onDelete: "CASCADE", 11 | references: { 12 | model: "Transactions", 13 | key: "hash" 14 | } 15 | }, 16 | 17 | networkId: { 18 | allowNull: false, 19 | type: DataTypes.STRING 20 | }, 21 | shardId: { 22 | allowNull: false, 23 | type: DataTypes.INTEGER 24 | }, 25 | metadata: { 26 | allowNull: false, 27 | type: DataTypes.TEXT 28 | }, 29 | 30 | approver: { 31 | type: DataTypes.STRING 32 | }, 33 | registrar: { 34 | type: DataTypes.STRING 35 | }, 36 | allowedScriptHashes: { 37 | allowNull: false, 38 | type: DataTypes.JSONB 39 | }, 40 | 41 | approvals: { 42 | allowNull: false, 43 | type: DataTypes.JSONB 44 | }, 45 | 46 | lockScriptHash: { 47 | allowNull: false, 48 | type: DataTypes.STRING 49 | }, 50 | parameters: { 51 | allowNull: false, 52 | type: DataTypes.JSONB 53 | }, 54 | supply: { 55 | allowNull: false, 56 | type: DataTypes.NUMERIC({ precision: 20, scale: 0 }) 57 | }, 58 | 59 | assetName: { 60 | type: DataTypes.STRING 61 | }, 62 | recipient: { 63 | allowNull: false, 64 | type: DataTypes.STRING 65 | }, 66 | assetType: { 67 | allowNull: false, 68 | type: DataTypes.STRING 69 | }, 70 | 71 | createdAt: { 72 | allowNull: false, 73 | type: DataTypes.DATE 74 | }, 75 | updatedAt: { 76 | allowNull: false, 77 | type: DataTypes.DATE 78 | } 79 | }); 80 | }, 81 | down: (queryInterface, Sequelize) => { 82 | return queryInterface.dropTable(tableName, { force: true }); 83 | } 84 | }; 85 | -------------------------------------------------------------------------------- /src/models/assetTypeLog.ts: -------------------------------------------------------------------------------- 1 | import * as Sequelize from "sequelize"; 2 | 3 | export interface AssetTypeLogAttribute { 4 | id?: number; 5 | transactionHash: string; 6 | transactionTracker?: string | null; 7 | transactionType: string | null; 8 | blockNumber?: number | null; 9 | transactionIndex?: number | null; 10 | isPending: boolean; 11 | assetType: string; 12 | } 13 | 14 | export type AssetTypeLogInstance = Sequelize.Instance; 15 | 16 | export default ( 17 | sequelize: Sequelize.Sequelize, 18 | DataTypes: Sequelize.DataTypes 19 | ) => { 20 | const AssetTypeLog = sequelize.define( 21 | "AssetTypeLog", 22 | { 23 | id: { 24 | allowNull: false, 25 | autoIncrement: true, 26 | primaryKey: true, 27 | type: DataTypes.BIGINT 28 | }, 29 | transactionHash: { 30 | allowNull: false, 31 | type: DataTypes.STRING, 32 | onDelete: "CASCADE", 33 | validate: { 34 | is: ["^[a-f0-9]{64}$"] 35 | }, 36 | references: { 37 | model: "Transactions", 38 | key: "hash" 39 | } 40 | }, 41 | transactionType: { 42 | allowNull: false, 43 | type: DataTypes.STRING 44 | }, 45 | transactionTracker: { 46 | type: DataTypes.STRING, 47 | validate: { 48 | is: ["^[a-f0-9]{64}$"] 49 | } 50 | }, 51 | blockNumber: { 52 | type: DataTypes.INTEGER 53 | }, 54 | transactionIndex: { 55 | type: DataTypes.INTEGER 56 | }, 57 | isPending: { 58 | allowNull: false, 59 | type: DataTypes.BOOLEAN 60 | }, 61 | assetType: { 62 | allowNull: false, 63 | type: DataTypes.STRING, 64 | validate: { 65 | is: ["^[a-f0-9]{40}$"] 66 | } 67 | }, 68 | createdAt: { 69 | allowNull: false, 70 | type: DataTypes.DATE 71 | }, 72 | updatedAt: { 73 | allowNull: false, 74 | type: DataTypes.DATE 75 | } 76 | }, 77 | {} 78 | ); 79 | AssetTypeLog.associate = () => { 80 | // associations can be defined here 81 | }; 82 | return AssetTypeLog; 83 | }; 84 | -------------------------------------------------------------------------------- /src/models/logic/unwrapCCC.ts: -------------------------------------------------------------------------------- 1 | import { SignedTransaction, U64 } from "codechain-sdk/lib/core/classes"; 2 | import { 3 | UnwrapCCC, 4 | UnwrapCCCActionJSON 5 | } from "codechain-sdk/lib/core/transaction/UnwrapCCC"; 6 | import { Transaction } from "sequelize"; 7 | import models from "../index"; 8 | import { UnwrapCCCInstance } from "../unwrapCCC"; 9 | import { createAddressLog } from "./addressLog"; 10 | import { createAssetAddressLog } from "./assetAddressLog"; 11 | import { getOutputOwner } from "./assettransferoutput"; 12 | import { createAssetTypeLog } from "./assetTypeLog"; 13 | import { strip0xPrefix } from "./utils/format"; 14 | 15 | export async function createUnwrapCCC( 16 | transaction: SignedTransaction, 17 | options: { transaction?: Transaction } = {} 18 | ): Promise { 19 | const transactionHash = transaction.hash().value; 20 | const unwrap = transaction.unsigned as UnwrapCCC; 21 | const { receiver, burn } = unwrap.toJSON().action as UnwrapCCCActionJSON; 22 | const { owner, lockScriptHash, parameters } = await getOutputOwner( 23 | burn.prevOut.tracker, 24 | burn.prevOut.index, 25 | options 26 | ); 27 | const instance = models.UnwrapCCC.create( 28 | { 29 | transactionHash: strip0xPrefix(transactionHash), 30 | receiver, 31 | burn: { 32 | index: 0, 33 | prevOut: { 34 | tracker: strip0xPrefix(burn.prevOut.tracker), 35 | index: burn.prevOut.index, 36 | assetType: strip0xPrefix(burn.prevOut.assetType), 37 | shardId: burn.prevOut.shardId, 38 | quantity: new U64(burn.prevOut.quantity).toString(), 39 | owner, 40 | lockScriptHash, 41 | parameters 42 | }, 43 | timelock: burn.timelock, 44 | assetType: strip0xPrefix(burn.prevOut.assetType), 45 | shardId: burn.prevOut.shardId, 46 | lockScript: Buffer.from(burn.lockScript), 47 | unlockScript: Buffer.from(burn.unlockScript), 48 | owner 49 | } 50 | }, 51 | { transaction: options.transaction } 52 | ); 53 | const assetType = "0000000000000000000000000000000000000000"; 54 | if (owner) { 55 | await createAssetAddressLog(transaction, owner, assetType, options); 56 | } 57 | await createAddressLog(transaction, receiver, "AssetOwner", options); 58 | await createAssetTypeLog(transaction, assetType, options); 59 | return instance; 60 | } 61 | -------------------------------------------------------------------------------- /src/models/changeAssetScheme.ts: -------------------------------------------------------------------------------- 1 | import * as Sequelize from "sequelize"; 2 | 3 | export interface ChangeAssetSchemeAttribute { 4 | transactionHash: string; 5 | assetType: string; 6 | networkId: string; 7 | shardId: number; 8 | metadata: string; 9 | approver?: string | null; 10 | registrar?: string | null; 11 | allowedScriptHashes: string[]; 12 | approvals: string[]; 13 | seq: number; 14 | } 15 | 16 | export interface ChangeAssetSchemeInstance 17 | extends Sequelize.Instance {} 18 | 19 | export default ( 20 | sequelize: Sequelize.Sequelize, 21 | DataTypes: Sequelize.DataTypes 22 | ) => { 23 | return sequelize.define( 24 | "ChangeAssetScheme", 25 | { 26 | transactionHash: { 27 | allowNull: false, 28 | primaryKey: true, 29 | type: Sequelize.STRING, 30 | onDelete: "CASCADE", 31 | validate: { 32 | is: ["^[a-f0-9]{64}$"] 33 | }, 34 | references: { 35 | model: "Transactions", 36 | key: "hash" 37 | } 38 | }, 39 | assetType: { 40 | allowNull: false, 41 | type: DataTypes.STRING, 42 | validate: { 43 | is: ["^[a-f0-9]{40}$"] 44 | } 45 | }, 46 | networkId: { 47 | allowNull: false, 48 | type: DataTypes.STRING 49 | }, 50 | shardId: { 51 | allowNull: false, 52 | type: DataTypes.INTEGER 53 | }, 54 | metadata: { 55 | allowNull: false, 56 | type: DataTypes.TEXT 57 | }, 58 | approver: { 59 | type: DataTypes.STRING 60 | }, 61 | registrar: { 62 | type: DataTypes.STRING 63 | }, 64 | allowedScriptHashes: { 65 | allowNull: false, 66 | type: DataTypes.JSONB 67 | }, 68 | approvals: { 69 | allowNull: false, 70 | type: DataTypes.JSONB 71 | }, 72 | seq: { 73 | allowNull: false, 74 | type: DataTypes.INTEGER 75 | }, 76 | createdAt: { 77 | allowNull: false, 78 | type: DataTypes.DATE 79 | }, 80 | updatedAt: { 81 | allowNull: false, 82 | type: DataTypes.DATE 83 | } 84 | }, 85 | {} 86 | ); 87 | }; 88 | -------------------------------------------------------------------------------- /src/checker/slack.ts: -------------------------------------------------------------------------------- 1 | import { IncomingWebhook, MessageAttachment } from "@slack/client"; 2 | import * as _ from "lodash"; 3 | 4 | export interface Slack { 5 | sendError(msg: string): void; 6 | sendWarning(text: string): void; 7 | sendInfo(title: string, text: string): void; 8 | } 9 | 10 | export class NullSlack implements Slack { 11 | public sendError(__: string) { 12 | // empty 13 | } 14 | public sendWarning(__: string) { 15 | // empty 16 | } 17 | public sendInfo(__: string, ___: string) { 18 | // empty 19 | } 20 | } 21 | 22 | // tslint:disable-next-line:max-classes-per-file 23 | class SlackWebhook implements Slack { 24 | private readonly tag: string; 25 | private readonly webhook: IncomingWebhook; 26 | private unsentAttachments: MessageAttachment[] = []; 27 | 28 | public constructor(tag: string, slackWebhookUrl: string) { 29 | this.tag = tag; 30 | this.webhook = new IncomingWebhook(slackWebhookUrl, {}); 31 | } 32 | 33 | public sendError(text: string) { 34 | const title = `[error]${this.tag} has a problem`; 35 | this.unsentAttachments.push({ title, text, color: "danger" }); 36 | this.send(); 37 | } 38 | 39 | public sendWarning(text: string) { 40 | console.log(`Warning: ${text}`); 41 | this.unsentAttachments.push({ 42 | title: `[warn]${this.tag} finds a problem`, 43 | text, 44 | color: "warning" 45 | }); 46 | this.send(); 47 | } 48 | 49 | public sendInfo(title: string, text: string) { 50 | console.log(`Info: ${text}`); 51 | this.unsentAttachments.push({ 52 | title: `[info]${this.tag} ${title}`, 53 | text, 54 | color: "good" 55 | }); 56 | this.send(); 57 | } 58 | 59 | private send() { 60 | this.webhook 61 | .send({ 62 | attachments: this.unsentAttachments 63 | }) 64 | .catch((err: Error) => { 65 | if (err) { 66 | console.error("IncomingWebhook failed!", err); 67 | return; 68 | } 69 | }); 70 | this.unsentAttachments = []; 71 | } 72 | } 73 | 74 | export function createSlack( 75 | tag: string, 76 | slackWebhookUrl: string | undefined 77 | ): Slack { 78 | if (slackWebhookUrl) { 79 | console.log("Slack connected"); 80 | return new SlackWebhook(tag, slackWebhookUrl); 81 | } else { 82 | console.log("Slack not connected"); 83 | console.log("You can set SLACK_WEBHOOK env variable to connect Slack"); 84 | return new NullSlack(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/migrations/20181217110415-create-block.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => { 4 | return queryInterface 5 | .createTable("Blocks", { 6 | hash: { 7 | primaryKey: true, 8 | allowNull: false, 9 | type: Sequelize.STRING 10 | }, 11 | parentHash: { 12 | allowNull: false, 13 | type: Sequelize.STRING 14 | }, 15 | timestamp: { 16 | allowNull: false, 17 | type: Sequelize.INTEGER 18 | }, 19 | number: { 20 | allowNull: false, 21 | type: Sequelize.INTEGER 22 | }, 23 | author: { 24 | allowNull: false, 25 | type: Sequelize.STRING 26 | }, 27 | extraData: { 28 | allowNull: false, 29 | type: Sequelize.JSONB 30 | }, 31 | transactionsRoot: { 32 | allowNull: false, 33 | type: Sequelize.STRING 34 | }, 35 | stateRoot: { 36 | allowNull: false, 37 | type: Sequelize.STRING 38 | }, 39 | score: { 40 | allowNull: false, 41 | type: Sequelize.STRING 42 | }, 43 | seal: { 44 | allowNull: false, 45 | type: Sequelize.JSONB 46 | }, 47 | miningReward: { 48 | allowNull: false, 49 | type: Sequelize.NUMERIC({ precision: 20, scale: 0 }) 50 | }, 51 | createdAt: { 52 | allowNull: false, 53 | type: Sequelize.DATE 54 | }, 55 | updatedAt: { 56 | allowNull: false, 57 | type: Sequelize.DATE 58 | } 59 | }) 60 | .then(() => { 61 | return queryInterface.sequelize.query( 62 | 'CREATE INDEX blocks_number_idx on "Blocks"("number")' 63 | ); 64 | }) 65 | .then(() => { 66 | return queryInterface.sequelize.query( 67 | 'CREATE INDEX blocks_timestamp_idx on "Blocks"("timestamp")' 68 | ); 69 | }); 70 | }, 71 | down: (queryInterface, Sequelize) => { 72 | return queryInterface.dropTable("Blocks", { force: true }); 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /src/migrations/20190116201000-create-compose-asset.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | const tableName = "ComposeAssets"; 3 | module.exports = { 4 | up: (queryInterface, DataTypes) => { 5 | return queryInterface.createTable(tableName, { 6 | transactionHash: { 7 | allowNull: false, 8 | primaryKey: true, 9 | type: DataTypes.STRING, 10 | onDelete: "CASCADE", 11 | references: { 12 | model: "Transactions", 13 | key: "hash" 14 | } 15 | }, 16 | 17 | networkId: { 18 | allowNull: false, 19 | type: DataTypes.STRING 20 | }, 21 | shardId: { 22 | allowNull: false, 23 | type: DataTypes.INTEGER 24 | }, 25 | metadata: { 26 | allowNull: false, 27 | type: DataTypes.TEXT 28 | }, 29 | approver: { 30 | type: DataTypes.STRING 31 | }, 32 | registrar: { 33 | type: DataTypes.STRING 34 | }, 35 | allowedScriptHashes: { 36 | allowNull: false, 37 | type: DataTypes.JSONB 38 | }, 39 | 40 | approvals: { 41 | allowNull: false, 42 | type: DataTypes.JSONB 43 | }, 44 | 45 | lockScriptHash: { 46 | allowNull: false, 47 | type: DataTypes.STRING 48 | }, 49 | parameters: { 50 | allowNull: false, 51 | type: DataTypes.JSONB 52 | }, 53 | supply: { 54 | allowNull: false, 55 | type: DataTypes.NUMERIC({ precision: 20, scale: 0 }) 56 | }, 57 | 58 | assetName: { 59 | type: DataTypes.STRING 60 | }, 61 | assetType: { 62 | allowNull: false, 63 | type: DataTypes.STRING 64 | }, 65 | recipient: { 66 | allowNull: false, 67 | type: DataTypes.STRING 68 | }, 69 | inputs: { 70 | allowNull: false, 71 | type: DataTypes.JSONB 72 | }, 73 | 74 | createdAt: { 75 | allowNull: false, 76 | type: DataTypes.DATE 77 | }, 78 | updatedAt: { 79 | allowNull: false, 80 | type: DataTypes.DATE 81 | } 82 | }); 83 | }, 84 | down: (queryInterface, Sequelize) => { 85 | return queryInterface.dropTable(tableName, { force: true }); 86 | } 87 | }; 88 | -------------------------------------------------------------------------------- /src/models/assetscheme.ts: -------------------------------------------------------------------------------- 1 | import * as Sequelize from "sequelize"; 2 | 3 | export interface AssetSchemeAttribute { 4 | transactionHash: string; 5 | assetType: string; 6 | shardId: number; 7 | metadata: string; 8 | approver?: string | null; 9 | registrar?: string | null; 10 | allowedScriptHashes: string[]; 11 | supply?: string | null; 12 | networkId?: string | null; 13 | seq: number; 14 | } 15 | 16 | export interface AssetSchemeInstance 17 | extends Sequelize.Instance {} 18 | 19 | export default ( 20 | sequelize: Sequelize.Sequelize, 21 | DataTypes: Sequelize.DataTypes 22 | ) => { 23 | const AssetScheme = sequelize.define( 24 | "AssetScheme", 25 | { 26 | transactionHash: { 27 | allowNull: false, 28 | type: DataTypes.STRING, 29 | onDelete: "CASCADE", 30 | validate: { 31 | is: ["^[a-f0-9]{64}$"] 32 | }, 33 | references: { 34 | model: "Transactions", 35 | key: "hash" 36 | } 37 | }, 38 | assetType: { 39 | allowNull: false, 40 | primaryKey: true, 41 | type: DataTypes.STRING, 42 | validate: { 43 | is: ["^[a-f0-9]{40}$"] 44 | } 45 | }, 46 | shardId: { 47 | allowNull: false, 48 | type: DataTypes.INTEGER 49 | }, 50 | metadata: { 51 | allowNull: false, 52 | type: DataTypes.TEXT 53 | }, 54 | approver: { 55 | type: DataTypes.STRING 56 | }, 57 | registrar: { 58 | type: DataTypes.STRING 59 | }, 60 | allowedScriptHashes: { 61 | allowNull: false, 62 | type: DataTypes.JSONB 63 | }, 64 | supply: { 65 | type: DataTypes.NUMERIC({ precision: 20, scale: 0 }) 66 | }, 67 | networkId: { 68 | type: DataTypes.STRING 69 | }, 70 | seq: { 71 | allowNull: false, 72 | type: DataTypes.STRING 73 | }, 74 | createdAt: { 75 | allowNull: false, 76 | type: DataTypes.DATE 77 | }, 78 | updatedAt: { 79 | allowNull: false, 80 | type: DataTypes.DATE 81 | } 82 | }, 83 | {} 84 | ); 85 | AssetScheme.associate = () => { 86 | // associations can be defined here 87 | }; 88 | return AssetScheme; 89 | }; 90 | -------------------------------------------------------------------------------- /src/models/assetAddressLog.ts: -------------------------------------------------------------------------------- 1 | import * as Sequelize from "sequelize"; 2 | 3 | export interface AssetAddressLogAttribute { 4 | id?: number; 5 | transactionHash: string; 6 | transactionTracker: string; 7 | transactionType: string; 8 | blockNumber?: number | null; 9 | transactionIndex?: number | null; 10 | isPending: boolean; 11 | address: string; 12 | assetType: string; 13 | } 14 | 15 | export type AssetAddressLogInstance = Sequelize.Instance< 16 | AssetAddressLogAttribute 17 | >; 18 | 19 | export default ( 20 | sequelize: Sequelize.Sequelize, 21 | DataTypes: Sequelize.DataTypes 22 | ) => { 23 | const AssetAddressLog = sequelize.define( 24 | "AssetAddressLog", 25 | { 26 | id: { 27 | allowNull: false, 28 | autoIncrement: true, 29 | primaryKey: true, 30 | type: DataTypes.BIGINT 31 | }, 32 | transactionHash: { 33 | allowNull: false, 34 | type: DataTypes.STRING, 35 | onDelete: "CASCADE", 36 | validate: { 37 | is: ["^[a-f0-9]{64}$"] 38 | }, 39 | references: { 40 | model: "Transactions", 41 | key: "hash" 42 | } 43 | }, 44 | transactionTracker: { 45 | type: DataTypes.STRING, 46 | validate: { 47 | is: ["^[a-f0-9]{64}$"] 48 | } 49 | }, 50 | transactionType: { 51 | allowNull: false, 52 | type: DataTypes.STRING 53 | }, 54 | blockNumber: { 55 | type: DataTypes.INTEGER 56 | }, 57 | transactionIndex: { 58 | type: DataTypes.INTEGER 59 | }, 60 | isPending: { 61 | allowNull: false, 62 | type: DataTypes.BOOLEAN 63 | }, 64 | address: { 65 | allowNull: false, 66 | type: DataTypes.STRING 67 | }, 68 | assetType: { 69 | allowNull: false, 70 | type: DataTypes.STRING, 71 | validate: { 72 | is: ["^[a-f0-9]{40}$"] 73 | } 74 | }, 75 | createdAt: { 76 | allowNull: false, 77 | type: DataTypes.DATE 78 | }, 79 | updatedAt: { 80 | allowNull: false, 81 | type: DataTypes.DATE 82 | } 83 | }, 84 | {} 85 | ); 86 | AssetAddressLog.associate = () => { 87 | // associations can be defined here 88 | }; 89 | return AssetAddressLog; 90 | }; 91 | -------------------------------------------------------------------------------- /src/migrations/20181223012324-create-asset-transfer-output.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => { 4 | return queryInterface 5 | .createTable("AssetTransferOutputs", { 6 | id: { 7 | allowNull: false, 8 | autoIncrement: true, 9 | primaryKey: true, 10 | type: Sequelize.BIGINT 11 | }, 12 | transactionHash: { 13 | allowNull: false, 14 | type: Sequelize.STRING, 15 | onDelete: "CASCADE", 16 | references: { 17 | model: "Transactions", 18 | key: "hash" 19 | } 20 | }, 21 | transactionTracker: { 22 | allowNull: false, 23 | type: Sequelize.STRING 24 | }, 25 | lockScriptHash: { 26 | allowNull: false, 27 | type: Sequelize.STRING 28 | }, 29 | parameters: { 30 | allowNull: false, 31 | type: Sequelize.JSONB 32 | }, 33 | assetType: { 34 | allowNull: false, 35 | type: Sequelize.STRING 36 | }, 37 | shardId: { 38 | allowNull: false, 39 | type: Sequelize.INTEGER 40 | }, 41 | quantity: { 42 | allowNull: false, 43 | type: Sequelize.NUMERIC({ precision: 20, scale: 0 }) 44 | }, 45 | index: { 46 | allowNull: false, 47 | type: Sequelize.INTEGER 48 | }, 49 | owner: { 50 | type: Sequelize.STRING 51 | }, 52 | createdAt: { 53 | allowNull: false, 54 | type: Sequelize.DATE 55 | }, 56 | updatedAt: { 57 | allowNull: false, 58 | type: Sequelize.DATE 59 | } 60 | }) 61 | .then(() => 62 | queryInterface.addIndex("AssetTransferOutputs", { 63 | fields: ["transactionHash"], 64 | indexType: "Hash" 65 | }) 66 | ) 67 | .then(() => 68 | queryInterface.addIndex("AssetTransferOutputs", [ 69 | "transactionTracker", 70 | "index" 71 | ]) 72 | ); 73 | }, 74 | down: (queryInterface, Sequelize) => { 75 | return queryInterface.dropTable("AssetTransferOutputs", { 76 | force: true 77 | }); 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /src/models/increaseAssetSupply.ts: -------------------------------------------------------------------------------- 1 | import * as Sequelize from "sequelize"; 2 | 3 | export interface IncreaseAssetSupplyAttribute { 4 | transactionHash: string; 5 | networkId: string; 6 | shardId: number; 7 | approvals: string[]; 8 | seq: number; 9 | 10 | lockScriptHash: string; 11 | parameters: string[]; 12 | supply: string; 13 | 14 | assetType: string; 15 | recipient: string; 16 | } 17 | 18 | export interface IncreaseAssetSupplyInstance 19 | extends Sequelize.Instance {} 20 | 21 | export default ( 22 | sequelize: Sequelize.Sequelize, 23 | DataTypes: Sequelize.DataTypes 24 | ) => { 25 | return sequelize.define( 26 | "IncreaseAssetSupply", 27 | { 28 | transactionHash: { 29 | allowNull: false, 30 | primaryKey: true, 31 | type: Sequelize.STRING, 32 | onDelete: "CASCADE", 33 | validate: { 34 | is: ["^[a-f0-9]{64}$"] 35 | }, 36 | references: { 37 | model: "Transactions", 38 | key: "hash" 39 | } 40 | }, 41 | 42 | networkId: { 43 | allowNull: false, 44 | type: DataTypes.STRING 45 | }, 46 | shardId: { 47 | allowNull: false, 48 | type: DataTypes.INTEGER 49 | }, 50 | assetType: { 51 | allowNull: false, 52 | type: DataTypes.STRING, 53 | validate: { 54 | is: ["^[a-f0-9]{40}$"] 55 | } 56 | }, 57 | approvals: { 58 | allowNull: false, 59 | type: DataTypes.JSONB 60 | }, 61 | seq: { 62 | allowNull: false, 63 | type: DataTypes.STRING 64 | }, 65 | 66 | lockScriptHash: { 67 | allowNull: false, 68 | type: DataTypes.STRING, 69 | validate: { 70 | is: ["^[a-f0-9]{40}$"] 71 | } 72 | }, 73 | parameters: { 74 | allowNull: false, 75 | type: DataTypes.JSONB 76 | }, 77 | supply: { 78 | allowNull: false, 79 | type: DataTypes.NUMERIC({ precision: 20, scale: 0 }) 80 | }, 81 | recipient: { 82 | allowNull: false, 83 | type: DataTypes.STRING 84 | }, 85 | 86 | createdAt: { 87 | allowNull: false, 88 | type: DataTypes.DATE 89 | }, 90 | updatedAt: { 91 | allowNull: false, 92 | type: DataTypes.DATE 93 | } 94 | }, 95 | {} 96 | ); 97 | }; 98 | -------------------------------------------------------------------------------- /src/models/logic/assetimage.ts: -------------------------------------------------------------------------------- 1 | import { H160 } from "codechain-sdk/lib/core/classes"; 2 | import * as request from "request"; 3 | import { Transaction } from "sequelize"; 4 | import * as sharp from "sharp"; 5 | import models from ".."; 6 | import * as Exception from "../../exception"; 7 | import { strip0xPrefix } from "./utils/format"; 8 | 9 | export async function createAssetImage( 10 | transactionHash: string, 11 | assetType: string, 12 | assetURL: string, 13 | options: { transaction?: Transaction } = {} 14 | ): Promise { 15 | let imageDataBuffer; 16 | try { 17 | const imageBuffer = await getImageBuffer(assetURL); 18 | if (imageBuffer) { 19 | imageDataBuffer = await sharp(imageBuffer) 20 | .resize(100, 100) 21 | .png() 22 | .toBuffer(); 23 | } 24 | } catch (e) { 25 | // assetURL can be an invalid form or unable to download. 26 | } 27 | if (imageDataBuffer) { 28 | try { 29 | await models.AssetImage.upsert( 30 | { 31 | transactionHash: strip0xPrefix(transactionHash), 32 | assetType: strip0xPrefix(assetType), 33 | image: imageDataBuffer 34 | }, 35 | { transaction: options.transaction } 36 | ); 37 | } catch (err) { 38 | console.error(err); 39 | throw Exception.DBError(); 40 | } 41 | } 42 | } 43 | 44 | export async function createAssetImageOfWCCC( 45 | transactionHash: string, 46 | options: { transaction?: Transaction } = {} 47 | ): Promise { 48 | try { 49 | const image = await sharp("./resources/wCCC.png") 50 | .resize(100, 100) 51 | .png() 52 | .toBuffer(); 53 | await models.AssetImage.create( 54 | { 55 | transactionHash, 56 | assetType: H160.zero().value, 57 | image 58 | }, 59 | { transaction: options.transaction } 60 | ); 61 | } catch (err) { 62 | console.error(err); 63 | throw Exception.DBError(); 64 | } 65 | } 66 | 67 | export async function getByAssetType(assetType: H160) { 68 | try { 69 | return await models.AssetImage.findOne({ 70 | where: { 71 | assetType: strip0xPrefix(assetType.value) 72 | } 73 | }); 74 | } catch (err) { 75 | console.error(err); 76 | throw Exception.DBError(); 77 | } 78 | } 79 | 80 | function getImageBuffer(url: string): Promise { 81 | return new Promise((resolve, reject) => { 82 | request({ url, encoding: null }, (err, _R, buffer) => { 83 | if (err) { 84 | reject(err); 85 | return; 86 | } 87 | resolve(buffer); 88 | }); 89 | }); 90 | } 91 | -------------------------------------------------------------------------------- /src/models/cccChanges.ts: -------------------------------------------------------------------------------- 1 | import * as Sequelize from "sequelize"; 2 | 3 | export type Reason = 4 | | "fee" 5 | | "author" 6 | | "stake" 7 | | "tx" 8 | | "initial_distribution" 9 | | "deposit" 10 | | "validator" 11 | | "report"; 12 | 13 | export const defaultAllReasons = [ 14 | "fee", 15 | "author", 16 | "stake", 17 | "tx", 18 | "initial_distribution", 19 | "deposit", 20 | "validator", 21 | "report" 22 | ]; 23 | 24 | export interface CCCChangeAttribute { 25 | id?: string; 26 | address: string; 27 | change: string; 28 | blockNumber: number; 29 | reason: Reason; 30 | transactionHash?: string; 31 | createdAt?: string; 32 | updatedAt?: string; 33 | } 34 | 35 | export interface CCCChangeInstance 36 | extends Sequelize.Instance {} 37 | 38 | export default ( 39 | sequelize: Sequelize.Sequelize, 40 | DataTypes: Sequelize.DataTypes 41 | ) => { 42 | const CCCChange = sequelize.define( 43 | "CCCChange", 44 | { 45 | id: { 46 | allowNull: false, 47 | autoIncrement: true, 48 | primaryKey: true, 49 | type: DataTypes.BIGINT 50 | }, 51 | address: { 52 | allowNull: false, 53 | type: DataTypes.STRING 54 | }, 55 | change: { 56 | allowNull: false, 57 | type: DataTypes.NUMERIC({ precision: 20, scale: 0 }) 58 | }, 59 | blockNumber: { 60 | allowNull: false, 61 | type: DataTypes.INTEGER, 62 | onDelete: "CASCADE", 63 | references: { 64 | model: "Blocks", 65 | key: "number" 66 | } 67 | }, 68 | reason: { 69 | allowNull: false, 70 | type: DataTypes.ENUM(...defaultAllReasons), 71 | validate: { 72 | isIn: [defaultAllReasons] 73 | } 74 | }, 75 | transactionHash: { 76 | allowNull: true, 77 | type: DataTypes.STRING, 78 | validate: { 79 | is: ["^[a-f0-9]{64}$"] 80 | } 81 | }, 82 | createdAt: { 83 | allowNull: false, 84 | type: DataTypes.DATE 85 | }, 86 | updatedAt: { 87 | allowNull: false, 88 | type: DataTypes.DATE 89 | } 90 | }, 91 | {} 92 | ); 93 | CCCChange.associate = models => { 94 | CCCChange.belongsTo(models.Transaction, { 95 | foreignKey: "transactionHash", 96 | as: "transaction" 97 | }); 98 | }; 99 | return CCCChange; 100 | }; 101 | -------------------------------------------------------------------------------- /src/models/logic/increaseassetsupply.ts: -------------------------------------------------------------------------------- 1 | import { H160 } from "codechain-primitives/lib"; 2 | import { 3 | AssetTransferOutput, 4 | SignedTransaction 5 | } from "codechain-sdk/lib/core/classes"; 6 | import { AssetMintOutput } from "codechain-sdk/lib/core/transaction/AssetMintOutput"; 7 | import { 8 | IncreaseAssetSupply, 9 | IncreaseAssetSupplyActionJSON 10 | } from "codechain-sdk/lib/core/transaction/IncreaseAssetSupply"; 11 | import { Transaction } from "sequelize"; 12 | import { IncreaseAssetSupplyInstance } from "../increaseAssetSupply"; 13 | import models from "../index"; 14 | import { createAssetAddressLog } from "./assetAddressLog"; 15 | import { createAssetTransferOutput } from "./assettransferoutput"; 16 | import { createAssetTypeLog } from "./assetTypeLog"; 17 | import { getOwner } from "./utils/address"; 18 | import { strip0xPrefix } from "./utils/format"; 19 | 20 | export async function createIncreaseAssetSupply( 21 | transaction: SignedTransaction, 22 | options: { transaction?: Transaction } = {} 23 | ): Promise { 24 | const transactionHash = transaction.hash().value; 25 | const increaseAssetSupply = transaction.unsigned as IncreaseAssetSupply; 26 | const { networkId, action } = increaseAssetSupply.toJSON(); 27 | const { 28 | shardId, 29 | assetType, 30 | output, 31 | approvals, 32 | seq 33 | } = action as IncreaseAssetSupplyActionJSON; 34 | 35 | const incSupplyOutput = AssetMintOutput.fromJSON(output); 36 | const { lockScriptHash, parameters } = output; 37 | const supply = incSupplyOutput.supply!.toString(10); 38 | const recipient = getOwner(new H160(lockScriptHash), parameters, networkId); 39 | const inst = await models.IncreaseAssetSupply.create( 40 | { 41 | transactionHash: strip0xPrefix(transactionHash), 42 | networkId, 43 | shardId, 44 | assetType: strip0xPrefix(assetType), 45 | approvals, 46 | seq, 47 | lockScriptHash: strip0xPrefix(lockScriptHash), 48 | parameters: parameters.map(p => strip0xPrefix(p)), 49 | recipient, 50 | supply 51 | }, 52 | { transaction: options.transaction } 53 | ); 54 | await createAssetTransferOutput( 55 | transactionHash, 56 | increaseAssetSupply.tracker().toString(), 57 | new AssetTransferOutput({ 58 | lockScriptHash: incSupplyOutput.lockScriptHash, 59 | parameters: incSupplyOutput.parameters, 60 | quantity: incSupplyOutput.supply, 61 | shardId, 62 | assetType: new H160(assetType) 63 | }), 64 | 0, 65 | { networkId }, 66 | options 67 | ); 68 | if (recipient) { 69 | await createAssetAddressLog(transaction, recipient, assetType, options); 70 | } 71 | await createAssetTypeLog(transaction, assetType, options); 72 | return inst; 73 | } 74 | -------------------------------------------------------------------------------- /src/models/addressLog.ts: -------------------------------------------------------------------------------- 1 | import * as Sequelize from "sequelize"; 2 | 3 | export type AddressLogType = 4 | | "TransactionSigner" 5 | | "TransactionApprover" 6 | | "AssetOwner" 7 | | "Approver" 8 | | "Registrar"; 9 | 10 | export interface AddressLogAttribute { 11 | id?: number; 12 | transactionHash: string; 13 | transactionTracker?: string | null; 14 | transactionType: string | null; 15 | blockNumber?: number | null; 16 | transactionIndex?: number | null; 17 | success?: boolean | null; 18 | isPending: boolean; 19 | address: string; 20 | // NOTE: It can be removed it's not used for the API. 21 | type: AddressLogType; 22 | } 23 | 24 | export type AddressLogInstance = Sequelize.Instance; 25 | 26 | export default ( 27 | sequelize: Sequelize.Sequelize, 28 | DataTypes: Sequelize.DataTypes 29 | ) => { 30 | const AddressLog = sequelize.define( 31 | "AddressLog", 32 | { 33 | id: { 34 | allowNull: false, 35 | autoIncrement: true, 36 | primaryKey: true, 37 | type: DataTypes.BIGINT 38 | }, 39 | transactionHash: { 40 | allowNull: false, 41 | type: DataTypes.STRING, 42 | onDelete: "CASCADE", 43 | validate: { 44 | is: ["^[a-f0-9]{64}$"] 45 | }, 46 | references: { 47 | model: "Transactions", 48 | key: "hash" 49 | } 50 | }, 51 | transactionType: { 52 | allowNull: false, 53 | type: DataTypes.STRING 54 | }, 55 | transactionTracker: { 56 | type: DataTypes.STRING, 57 | validate: { 58 | is: ["^[a-f0-9]{64}$"] 59 | } 60 | }, 61 | blockNumber: { 62 | type: DataTypes.INTEGER 63 | }, 64 | transactionIndex: { 65 | type: DataTypes.INTEGER 66 | }, 67 | success: { 68 | type: DataTypes.BOOLEAN 69 | }, 70 | isPending: { 71 | allowNull: false, 72 | type: DataTypes.BOOLEAN 73 | }, 74 | address: { 75 | allowNull: false, 76 | type: DataTypes.STRING 77 | }, 78 | type: { 79 | allowNull: false, 80 | type: DataTypes.STRING 81 | }, 82 | createdAt: { 83 | allowNull: false, 84 | type: DataTypes.DATE 85 | }, 86 | updatedAt: { 87 | allowNull: false, 88 | type: DataTypes.DATE 89 | } 90 | }, 91 | {} 92 | ); 93 | AddressLog.associate = () => { 94 | // associations can be defined here 95 | }; 96 | return AddressLog; 97 | }; 98 | -------------------------------------------------------------------------------- /src/checker/email.ts: -------------------------------------------------------------------------------- 1 | import * as sendgrid from "@sendgrid/mail"; 2 | 3 | export interface Email { 4 | sendError(msg: string): void; 5 | sendWarning(text: string): void; 6 | sendInfo(title: string, msg: string): void; 7 | } 8 | 9 | class NullEmail implements Email { 10 | public sendError(_: string): void { 11 | // empty 12 | } 13 | public sendWarning(_: string): void { 14 | // empty 15 | } 16 | public sendInfo(_: string, __: string): void { 17 | // empty 18 | } 19 | } 20 | 21 | const from = "no-reply+indexer-CCCChanges-checker@devop.codechain.io"; 22 | 23 | function createTitle(params: { 24 | title: string; 25 | tag: string; 26 | level: string; 27 | }): string { 28 | const { title, tag, level } = params; 29 | return `[${level}]${tag} ${title}`; 30 | } 31 | 32 | // tslint:disable-next-line:max-classes-per-file 33 | class Sendgrid implements Email { 34 | private readonly tag: string; 35 | private readonly to: string; 36 | 37 | public constructor(params: { 38 | tag: string; 39 | sendgridApiKey: string; 40 | to: string; 41 | }) { 42 | const { tag, sendgridApiKey, to } = params; 43 | this.tag = tag; 44 | sendgrid.setApiKey(sendgridApiKey); 45 | this.to = to; 46 | } 47 | 48 | public sendError(text: string): void { 49 | const subject = createTitle({ 50 | tag: this.tag, 51 | title: "has a problem.", 52 | level: "error" 53 | }); 54 | this.send(subject, text); 55 | } 56 | 57 | public sendWarning(text: string): void { 58 | const subject = createTitle({ 59 | tag: this.tag, 60 | title: "finds a problem.", 61 | level: "warn" 62 | }); 63 | this.send(subject, text); 64 | } 65 | 66 | public sendInfo(title: string, text: string): void { 67 | const subject = createTitle({ tag: this.tag, title, level: "info" }); 68 | this.send(subject, text); 69 | } 70 | 71 | private send(subject: string, value: string): void { 72 | sendgrid 73 | .send({ 74 | subject, 75 | from, 76 | to: this.to, 77 | content: [{ type: "text/html", value }] 78 | }) 79 | .catch(console.error); 80 | } 81 | } 82 | 83 | export function createEmail(params: { 84 | tag: string; 85 | to?: string; 86 | sendgridApiKey?: string; 87 | }): Email { 88 | const { tag, to, sendgridApiKey } = params; 89 | if (sendgridApiKey != null) { 90 | if (to == null) { 91 | throw Error("The email destination is not set"); 92 | } 93 | console.log("Sendgrid key is set"); 94 | return new Sendgrid({ tag, sendgridApiKey, to }); 95 | } else { 96 | console.log("Donot use sendgrid"); 97 | return new NullEmail(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/migrations/20181220022044-create-transaction.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | module.exports = { 3 | up: (queryInterface, Sequelize) => { 4 | return queryInterface 5 | .createTable("Transactions", { 6 | hash: { 7 | allowNull: false, 8 | primaryKey: true, 9 | type: Sequelize.STRING 10 | }, 11 | blockNumber: { 12 | type: Sequelize.INTEGER 13 | }, 14 | blockHash: { 15 | type: Sequelize.STRING, 16 | onDelete: "CASCADE", 17 | references: { 18 | model: "Blocks", 19 | key: "hash" 20 | } 21 | }, 22 | 23 | tracker: { 24 | type: Sequelize.STRING 25 | }, 26 | 27 | transactionIndex: { 28 | type: Sequelize.INTEGER 29 | }, 30 | type: { 31 | allowNull: false, 32 | type: Sequelize.STRING 33 | }, 34 | seq: { 35 | allowNull: false, 36 | type: Sequelize.NUMERIC({ precision: 20, scale: 0 }) 37 | }, 38 | fee: { 39 | allowNull: false, 40 | type: Sequelize.STRING 41 | }, 42 | networkId: { 43 | allowNull: false, 44 | type: Sequelize.STRING 45 | }, 46 | 47 | sig: { 48 | allowNull: false, 49 | type: Sequelize.STRING 50 | }, 51 | signer: { 52 | allowNull: false, 53 | type: Sequelize.STRING 54 | }, 55 | errorHint: { 56 | type: Sequelize.TEXT("MEDIUM") 57 | }, 58 | timestamp: { 59 | type: Sequelize.INTEGER 60 | }, 61 | isPending: { 62 | allowNull: false, 63 | type: Sequelize.BOOLEAN 64 | }, 65 | pendingTimestamp: { 66 | type: Sequelize.INTEGER 67 | }, 68 | 69 | createdAt: { 70 | allowNull: false, 71 | type: Sequelize.DATE 72 | }, 73 | updatedAt: { 74 | allowNull: false, 75 | type: Sequelize.DATE 76 | } 77 | }) 78 | .then(() => queryInterface.addIndex("Transactions", ["blockHash"])) 79 | .then(() => 80 | queryInterface.addIndex("Transactions", ["blockNumber"]) 81 | ); 82 | }, 83 | down: (queryInterface, Sequelize) => { 84 | return queryInterface.dropTable("Transactions", { force: true }); 85 | } 86 | }; 87 | -------------------------------------------------------------------------------- /src/models/logic/wrapCCC.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AssetTransferOutput, 3 | H160, 4 | SignedTransaction, 5 | U64 6 | } from "codechain-sdk/lib/core/classes"; 7 | import { 8 | WrapCCC, 9 | WrapCCCActionJSON 10 | } from "codechain-sdk/lib/core/transaction/WrapCCC"; 11 | import { Transaction } from "sequelize"; 12 | import models from "../index"; 13 | import { WrapCCCInstance } from "../wrapCCC"; 14 | import { createAddressLog } from "./addressLog"; 15 | import { createAssetAddressLog } from "./assetAddressLog"; 16 | import { createAssetSchemeOfWCCC } from "./assetscheme"; 17 | import { createAssetTransferOutput } from "./assettransferoutput"; 18 | import { createAssetTypeLog } from "./assetTypeLog"; 19 | import { getOwner } from "./utils/address"; 20 | import { strip0xPrefix } from "./utils/format"; 21 | 22 | export async function createWrapCCC( 23 | transaction: SignedTransaction, 24 | options: { transaction?: Transaction } = {} 25 | ): Promise { 26 | const transactionHash = transaction.hash().value; 27 | const wrapCCC = transaction.unsigned as WrapCCC; 28 | const { 29 | shardId, 30 | lockScriptHash, 31 | parameters, 32 | quantity, 33 | payer 34 | } = transaction.toJSON().action as WrapCCCActionJSON; 35 | const networkId = transaction.unsigned.networkId(); 36 | 37 | const recipient = getOwner(new H160(lockScriptHash), parameters, networkId); 38 | 39 | const result = await models.WrapCCC.create( 40 | { 41 | transactionHash: strip0xPrefix(transactionHash), 42 | shardId, 43 | lockScriptHash: strip0xPrefix(lockScriptHash), 44 | parameters: parameters.map(p => strip0xPrefix(p)), 45 | quantity: new U64(quantity).toString(), 46 | recipient 47 | }, 48 | { transaction: options.transaction } 49 | ); 50 | const existing = await models.AssetScheme.findByPk(H160.zero().toString(), { 51 | transaction: options.transaction 52 | }); 53 | if (existing == null) { 54 | await createAssetSchemeOfWCCC( 55 | transactionHash, 56 | { 57 | networkId, 58 | shardId 59 | }, 60 | options 61 | ); 62 | } 63 | await createAssetTransferOutput( 64 | transactionHash, 65 | wrapCCC.tracker().toString(), 66 | AssetTransferOutput.fromJSON({ 67 | lockScriptHash, 68 | parameters, 69 | quantity, 70 | shardId, 71 | assetType: H160.zero().toString() 72 | }), 73 | 0, 74 | { networkId }, 75 | options 76 | ); 77 | await createAddressLog(transaction, payer, "AssetOwner", options); 78 | const assetType = "0000000000000000000000000000000000000000"; 79 | if (recipient) { 80 | await createAssetAddressLog(transaction, recipient, assetType, options); 81 | } 82 | await createAssetTypeLog(transaction, assetType, options); 83 | return result; 84 | } 85 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "codechain-indexer", 3 | "version": "2.0.1", 4 | "description": "Data synchronizing tool between CodeChain and DB", 5 | "repository": "https://github.com/CodeChain-io/codechain-indexer", 6 | "author": "CodeChain Team ", 7 | "license": "Apache-2.0", 8 | "engines": { 9 | "node": ">=10", 10 | "yarn": "^1.10.0" 11 | }, 12 | "dependencies": { 13 | "@sendgrid/mail": "^6.4.0", 14 | "@slack/client": "^5.0.1", 15 | "async-lock": "^1.1.4", 16 | "bignumber.js": "^7.2.1", 17 | "buffer": "^5.2.1", 18 | "codechain-sdk": "^2.0.0-alpha.2", 19 | "codechain-stakeholder-sdk": "^2.0.0-alpha.6", 20 | "cors": "^2.8.4", 21 | "dotenv": "^6.0.0", 22 | "express": "^4.16.3", 23 | "express-validation": "^1.0.2", 24 | "fmt": "^1.1.0", 25 | "http-status-codes": "^1.3.0", 26 | "joi": "^14.3.1", 27 | "lodash": "^4.17.19", 28 | "moment": "^2.22.2", 29 | "node-schedule": "^1.3.0", 30 | "pg": "^7.7.1", 31 | "pg-hstore": "^2.3.2", 32 | "request": "^2.88.0", 33 | "rlp": "^2.2.2", 34 | "sequelize": "^4.42.0", 35 | "sharp": "^0.23.0", 36 | "sinon": "^7.2.7", 37 | "ts-node": "^8.3.0", 38 | "typescript": "^3.5.2", 39 | "winston": "^2.4.0", 40 | "workerpool": "^3.1.1" 41 | }, 42 | "scripts": { 43 | "build": "tsc --incremental -p .", 44 | "reset": "sequelize db:migrate:undo:all && sequelize db:migrate", 45 | "drop": "sequelize db:migrate:undo:all", 46 | "migrate": "sequelize db:migrate", 47 | "start": "ts-node src/index.ts", 48 | "del": "ts-node script/deleteBlock.ts", 49 | "test": "NODE_ENV=test mocha --exit -r ts-node/register --timeout 60000 --recursive \"test/**/*.spec.ts\"", 50 | "lint": "tslint -p . && prettier '{src,test,script}/**/*.{ts,js,json}' -l", 51 | "fmt": "tslint -p . --fix && prettier '{src,test,script}/**/*.{ts,js,json}' --write" 52 | }, 53 | "devDependencies": { 54 | "@types/async-lock": "^1.1.1", 55 | "@types/body-parser": "^1.17.0", 56 | "@types/chai": "^4.1.7", 57 | "@types/chai-as-promised": "^7.1.0", 58 | "@types/cors": "^2.8.4", 59 | "@types/dotenv": "^4.0.3", 60 | "@types/express": "^4.16.0", 61 | "@types/http-status-codes": "^1.2.0", 62 | "@types/joi": "^14.3.2", 63 | "@types/lodash": "^4.14.116", 64 | "@types/mocha": "^5.2.6", 65 | "@types/node-schedule": "^1.2.2", 66 | "@types/pg": "^7.4.11", 67 | "@types/request": "^2.48.1", 68 | "@types/sequelize": "^4.27.33", 69 | "@types/sharp": "^0.17.10", 70 | "@types/sinon": "^7.0.8", 71 | "@types/supertest": "^2.0.7", 72 | "@types/swagger-jsdoc": "^0.0.2", 73 | "@types/swagger-ui-express": "^3.0.0", 74 | "@types/workerpool": "^2.3.0", 75 | "chai": "^4.2.0", 76 | "chai-as-promised": "^7.1.1", 77 | "mocha": "^6.1.4", 78 | "prettier": "^1.14.2", 79 | "sequelize-cli": "^5.4.1", 80 | "supertest": "^3.4.2", 81 | "swagger-jsdoc": "^3.2.9", 82 | "swagger-ui-express": "^4.0.2", 83 | "tslint": "^5.11.0", 84 | "tslint-config-prettier": "^1.15.0" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /test/wrap-unwrap.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "chai"; 2 | import { Block, U64 } from "codechain-sdk/lib/core/classes"; 3 | import "mocha"; 4 | import * as BlockModel from "../src/models/logic/block"; 5 | import * as TransactionModel from "../src/models/logic/transaction"; 6 | import * as UTXOModel from "../src/models/logic/utxo"; 7 | import * as Helper from "./helper"; 8 | 9 | async function check(blockResponse: Block, type: string) { 10 | expect(blockResponse).not.null; 11 | 12 | const blockInst = await BlockModel.createBlock( 13 | blockResponse, 14 | Helper.sdk, 15 | new U64("1000") 16 | ); 17 | const blockDoc = blockInst.get({ plain: true }); 18 | expect(blockDoc.hash).equal(blockResponse.hash.value); 19 | 20 | const signed = blockResponse.transactions[0]; 21 | expect(signed).not.null; 22 | const txByHashInst = (await TransactionModel.getByHash(signed.hash()))!; 23 | expect(txByHashInst).not.null; 24 | const tx = txByHashInst.get({ plain: true }); 25 | expect(tx.hash).equal(signed.hash().value); 26 | expect(tx.type).equal(type); 27 | expect(tx.tracker).equal((signed.unsigned as any).tracker().value); 28 | } 29 | 30 | describe("wrap-unwrap", function() { 31 | let bestBlockNumber: number; 32 | let wrapBlock: Block; 33 | let unwrapBlock: Block; 34 | 35 | before(async function() { 36 | await Helper.runExample("import-test-account"); 37 | await Helper.runExample("wrap-ccc-and-unwrap-ccc"); 38 | bestBlockNumber = await Helper.sdk.rpc.chain.getBestBlockNumber(); 39 | wrapBlock = (await Helper.sdk.rpc.chain.getBlock(bestBlockNumber - 1))!; 40 | unwrapBlock = (await Helper.sdk.rpc.chain.getBlock(bestBlockNumber))!; 41 | }); 42 | 43 | it("wrap", async function() { 44 | await check(wrapBlock, "wrapCCC"); 45 | }); 46 | 47 | it("unwrap", async function() { 48 | await check(unwrapBlock, "unwrapCCC"); 49 | }); 50 | 51 | it("Check duplicated block", async function() { 52 | expect(unwrapBlock).not.null; 53 | 54 | // Duplicated error test 55 | try { 56 | await BlockModel.createBlock( 57 | unwrapBlock, 58 | Helper.sdk, 59 | new U64("1000") 60 | ); 61 | expect.fail(); 62 | } catch (e) { 63 | expect(e).not.null; 64 | expect(e.message).equal("AlreadyExist"); 65 | } 66 | }); 67 | 68 | it("Check utxo", async function() { 69 | const wrapHash = wrapBlock.transactions[0].hash(); 70 | const txInst = (await TransactionModel.getByHash(wrapHash))!; 71 | 72 | expect(await txInst.get("type")).equal("wrapCCC"); 73 | const wcccOwner = (await txInst.getWrapCCC())!.get("recipient"); 74 | 75 | const utxoOfWcccOwner = await UTXOModel.getByAddress(wcccOwner); 76 | expect(utxoOfWcccOwner.length).equal(0); 77 | 78 | const utxoOfWcccInst = await UTXOModel.getByTxHashIndex(wrapHash, 0); 79 | expect(utxoOfWcccInst!.get("usedTransactionHash")).not.null; 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /src/models/logic/utils/includeArray.ts: -------------------------------------------------------------------------------- 1 | import models from "../.."; 2 | 3 | export const includeArray = [ 4 | { 5 | attributes: [], 6 | as: "mintAsset", 7 | model: models.MintAsset 8 | }, 9 | { 10 | attributes: [], 11 | as: "transferAsset", 12 | model: models.TransferAsset 13 | }, 14 | { 15 | attributes: [], 16 | as: "changeAssetScheme", 17 | model: models.ChangeAssetScheme 18 | }, 19 | { 20 | attributes: [], 21 | as: "increaseAssetSupply", 22 | model: models.IncreaseAssetSupply 23 | }, 24 | { 25 | attributes: [], 26 | as: "wrapCCC", 27 | model: models.WrapCCC 28 | }, 29 | { 30 | attributes: [], 31 | as: "unwrapCCC", 32 | model: models.UnwrapCCC 33 | }, 34 | { 35 | attributes: [], 36 | as: "pay", 37 | model: models.Pay 38 | }, 39 | { 40 | attributes: [], 41 | as: "setRegularKey", 42 | model: models.SetRegularKey 43 | }, 44 | { 45 | attributes: [], 46 | as: "createShard", 47 | model: models.CreateShard 48 | }, 49 | { 50 | attributes: [], 51 | as: "setShardOwners", 52 | model: models.SetShardOwners 53 | }, 54 | { 55 | attributes: [], 56 | as: "setShardUsers", 57 | model: models.SetShardUsers 58 | }, 59 | { 60 | attributes: [], 61 | as: "store", 62 | model: models.Store 63 | }, 64 | { 65 | attributes: [], 66 | as: "remove", 67 | model: models.Remove 68 | }, 69 | { 70 | attributes: [], 71 | as: "custom", 72 | model: models.Custom 73 | } 74 | ]; 75 | 76 | export const fullIncludeArray = [ 77 | { 78 | as: "mintAsset", 79 | model: models.MintAsset 80 | }, 81 | { 82 | as: "transferAsset", 83 | model: models.TransferAsset 84 | }, 85 | { 86 | as: "changeAssetScheme", 87 | model: models.ChangeAssetScheme 88 | }, 89 | { 90 | as: "increaseAssetSupply", 91 | model: models.IncreaseAssetSupply 92 | }, 93 | { 94 | as: "wrapCCC", 95 | model: models.WrapCCC 96 | }, 97 | { 98 | as: "unwrapCCC", 99 | model: models.UnwrapCCC 100 | }, 101 | { 102 | as: "pay", 103 | model: models.Pay 104 | }, 105 | { 106 | as: "setRegularKey", 107 | model: models.SetRegularKey 108 | }, 109 | { 110 | as: "createShard", 111 | model: models.CreateShard 112 | }, 113 | { 114 | as: "setShardOwners", 115 | model: models.SetShardOwners 116 | }, 117 | { 118 | as: "setShardUsers", 119 | model: models.SetShardUsers 120 | }, 121 | { 122 | as: "store", 123 | model: models.Store 124 | }, 125 | { 126 | as: "remove", 127 | model: models.Remove 128 | }, 129 | { 130 | as: "custom", 131 | model: models.Custom 132 | } 133 | ]; 134 | -------------------------------------------------------------------------------- /script/20190415112715-createCCCChange.ts: -------------------------------------------------------------------------------- 1 | import { SDK } from "codechain-sdk"; 2 | import { Transaction } from "sequelize"; 3 | import models from "../src/models"; 4 | import { updateCCCChange } from "../src/worker/cccChange"; 5 | 6 | async function createTransaction(): Promise { 7 | return await models.sequelize.transaction({ 8 | isolationLevel: 9 | models.Sequelize.Transaction.ISOLATION_LEVELS.SERIALIZABLE, 10 | deferrable: models.Sequelize.Deferrable.SET_DEFERRED 11 | }); 12 | } 13 | 14 | async function main() { 15 | const networkId: string = process.env.CODECHAIN_NETWORK_ID!; 16 | if (networkId == null) { 17 | throw Error("Set CODECHAIN_NETWORK_ID"); 18 | } 19 | const server: string = process.env.CODECHAIN_RPC_SERVER!; 20 | if (server == null) { 21 | throw Error("Set CODECHAIN_RPC_SERVER"); 22 | } 23 | 24 | const sdk = new SDK({ server, networkId }); 25 | 26 | const cccChange = await models.CCCChange.findOne({ 27 | order: [["blockNumber", "DESC"]] 28 | }); 29 | let blockNumber = cccChange == null ? 0 : cccChange.get("blockNumber") + 1; 30 | console.log(`Start sync from ${blockNumber}`); 31 | 32 | let transaction = await createTransaction(); 33 | try { 34 | let queries = []; 35 | while (true) { 36 | const blockProj = await models.Block.findOne({ 37 | order: [["number", "DESC"]] 38 | }); 39 | const bestBlock = blockProj == null ? 0 : blockProj.get("number"); 40 | 41 | if (bestBlock <= blockNumber) { 42 | break; 43 | } 44 | for (; blockNumber <= bestBlock; blockNumber += 1) { 45 | const block = await sdk.rpc.chain.getBlock(blockNumber); 46 | if (block == null) { 47 | throw Error( 48 | `${blockNumber} is an invalid block number: no block` 49 | ); 50 | } 51 | const miningReward = await sdk.rpc.chain.getMiningReward( 52 | block.number 53 | ); 54 | if (miningReward == null) { 55 | throw Error( 56 | `${blockNumber} is an invalid block number: no mining reward` 57 | ); 58 | } 59 | queries.push( 60 | updateCCCChange(sdk, block, miningReward, transaction) 61 | ); 62 | if (queries.length >= 500) { 63 | await Promise.all(queries); 64 | await transaction.commit(); 65 | console.log(`#${blockNumber} block has been synchronized`); 66 | queries = []; 67 | transaction = await createTransaction(); 68 | } 69 | } 70 | } 71 | if (queries.length !== 0) { 72 | await Promise.all(queries); 73 | await transaction.commit(); 74 | } 75 | console.log("Migration finished"); 76 | } catch (err) { 77 | console.error(err); 78 | await transaction.rollback(); 79 | } 80 | 81 | await models.sequelize.close(); 82 | } 83 | 84 | main().catch(console.error); 85 | -------------------------------------------------------------------------------- /src/models/assettransferoutput.ts: -------------------------------------------------------------------------------- 1 | import * as Sequelize from "sequelize"; 2 | 3 | export interface AssetTransferOutputAttribute { 4 | id?: string; 5 | transactionHash: string; 6 | transactionTracker: string; 7 | lockScriptHash: string; 8 | parameters: string[]; 9 | assetType: string; 10 | shardId: number; 11 | quantity: string; 12 | index: number; 13 | owner?: string | null; 14 | } 15 | 16 | export interface AssetTransferOutputInstance 17 | extends Sequelize.Instance {} 18 | 19 | export default ( 20 | sequelize: Sequelize.Sequelize, 21 | DataTypes: Sequelize.DataTypes 22 | ) => { 23 | const AssetTransferOutput = sequelize.define( 24 | "AssetTransferOutput", 25 | { 26 | id: { 27 | allowNull: false, 28 | autoIncrement: true, 29 | primaryKey: true, 30 | type: DataTypes.BIGINT 31 | }, 32 | transactionHash: { 33 | allowNull: false, 34 | type: DataTypes.STRING, 35 | onDelete: "CASCADE", 36 | validate: { 37 | is: ["^[a-f0-9]{64}$"] 38 | }, 39 | references: { 40 | model: "Transactions", 41 | key: "hash" 42 | } 43 | }, 44 | transactionTracker: { 45 | allowNull: false, 46 | type: DataTypes.STRING 47 | }, 48 | lockScriptHash: { 49 | allowNull: false, 50 | type: DataTypes.STRING, 51 | validate: { 52 | is: ["^[a-f0-9]{40}$"] 53 | } 54 | }, 55 | parameters: { 56 | allowNull: false, 57 | type: DataTypes.JSONB 58 | }, 59 | assetType: { 60 | allowNull: false, 61 | type: DataTypes.STRING, 62 | validate: { 63 | is: ["^[a-f0-9]{40}$"] 64 | } 65 | }, 66 | shardId: { 67 | allowNull: false, 68 | type: DataTypes.INTEGER 69 | }, 70 | quantity: { 71 | allowNull: false, 72 | type: DataTypes.NUMERIC({ precision: 20, scale: 0 }) 73 | }, 74 | index: { 75 | allowNull: false, 76 | type: DataTypes.INTEGER 77 | }, 78 | owner: { 79 | type: DataTypes.STRING 80 | }, 81 | createdAt: { 82 | allowNull: false, 83 | type: DataTypes.DATE 84 | }, 85 | updatedAt: { 86 | allowNull: false, 87 | type: DataTypes.DATE 88 | } 89 | }, 90 | {} 91 | ); 92 | AssetTransferOutput.associate = models => { 93 | AssetTransferOutput.belongsTo(models.AssetScheme, { 94 | foreignKey: "assetType", 95 | as: "assetScheme" 96 | }); 97 | // associations can be defined here 98 | }; 99 | return AssetTransferOutput; 100 | }; 101 | --------------------------------------------------------------------------------