diff --git a/.env.test b/.env.test index db601a70..ab8cc5bd 100644 --- a/.env.test +++ b/.env.test @@ -19,3 +19,11 @@ RSK_NODE_HOST='https://public-node.testnet.rsk.co' LOCAL_TEST=1 TEST_FEDERATION_ADDRESS='2ND7Zf42GPg1JJb5TQGYXkM4Ygz74spV8MR' TTL_SESSIONDB_EXPIRE_MILLISECONDS=172800000 +METRICS_ENABLED=false; + +# MONGODB CONNECTION +RSK_DB_USER='api-user' +RSK_DB_PASS='pwd' +RSK_DB_URL='localhost' +RSK_DB_PORT=27017 +RSK_DB_NAME=rsk diff --git a/.gitignore b/.gitignore index 79cf158e..0c790024 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ # Logs logs -*.log +*.log* npm-debug.log* yarn-debug.log* yarn-error.log* @@ -70,3 +70,6 @@ data/* SessionDB/data/redis-data sonar-project.properties .scannerwork + +# rsk db data +rsk-database/db diff --git a/.vscode/settings.json b/.vscode/settings.json index 07313667..0f0139d4 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -28,5 +28,6 @@ "eslint.validate": [ "javascript", "typescript" - ] + ], + "js/ts.implicitProjectConfig.experimentalDecorators": true } diff --git a/README.md b/README.md index ece70c61..07cc9405 100644 --- a/README.md +++ b/README.md @@ -19,16 +19,34 @@ npm ci Move to the `SessionDB` folder and run: ```sh -docker-compose up +docker-compose up -d ``` +### RSK DB +Move to the `rsk-database` folder, copy your `.env` file in it and then run: +```sh +docker-compose up -d +``` + +For some reason passing `--env-file` argument to docker-compose doesn't seem to be working fine. That's why we need to copy the `.env` file here too. + ## Run the application +If you want to start the API alongside the daemon run: ```sh npm start ``` +If you prefer to execute just the API run: +```sh +npm run start-api +``` + +Open http://127.0.0.1:3000 in your browser to discover the API capabilities -Open http://127.0.0.1:3000 in your browser. +If you prefer to execute just the daemon run: +```sh +npm run start-daemon +``` ## Fix code style and formatting issues diff --git a/package-lock.json b/package-lock.json index 16412942..c4430b80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1346,6 +1346,23 @@ } } }, + "@types/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-yfAgiWgVLjFCmRv8zAcOIHywYATEwiTVccTLnRp6UxTNavT55M9d/uhK3T03St/+8/z/wW+CRjGKUNmEqoHHCA==", + "dev": true, + "requires": { + "base-x": "^3.0.6" + } + }, + "@types/bson": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.4.tgz", + "integrity": "sha512-awqorHvQS0DqxkHQ/FxcPX9E+H7Du51Qw/2F+5TBMSaE3G0hm+8D3eXJ6MAzFw75nE8V7xF0QvzUSdxIjJb/GA==", + "requires": { + "@types/node": "*" + } + }, "@types/connect": { "version": "3.4.34", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz", @@ -1456,11 +1473,27 @@ "integrity": "sha512-Lwh0lzzqT5Pqh6z61P3c3P5nm6fzQK/MMHl9UKeneAeInVflBSz1O2EkX6gM6xfJd7FBXBY5purtLx7fUiZ7Hw==", "dev": true }, + "@types/mongodb": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.6.20.tgz", + "integrity": "sha512-WcdpPJCakFzcWWD9juKoZbRtQxKIMYF/JIAM4JrNHrMcnJL6/a2NWjXxW7fo9hxboxxkg+icff8d7+WIEvKgYQ==", + "requires": { + "@types/bson": "*", + "@types/node": "*" + } + }, + "@types/mongoose": { + "version": "5.11.97", + "resolved": "https://registry.npmjs.org/@types/mongoose/-/mongoose-5.11.97.tgz", + "integrity": "sha512-cqwOVYT3qXyLiGw7ueU2kX9noE8DPGRY6z8eUxudhXY8NZ7DMKYAxyZkLSevGfhCX3dO/AoX5/SO9lAzfjon0Q==", + "requires": { + "mongoose": "*" + } + }, "@types/node": { "version": "10.17.60", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", - "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", - "dev": true + "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==" }, "@types/on-finished": { "version": "2.3.1", @@ -5088,6 +5121,11 @@ } } }, + "js-sha256": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", + "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==" + }, "js-sha3": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", @@ -5219,6 +5257,11 @@ "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", "dev": true }, + "kareem": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.2.tgz", + "integrity": "sha512-STHz9P7X2L4Kwn72fA4rGyqyXdmrMSdxqHx9IXon/FXluXieaFA6KJ2upcHAHxQPQ0LeM/OjLrhFxifHewOALQ==" + }, "keccak": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.1.tgz", @@ -6282,6 +6325,93 @@ "saslprep": "^1.0.0" } }, + "mongoose": { + "version": "5.13.2", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-5.13.2.tgz", + "integrity": "sha512-sBUKJGpdwZCq9102Lj6ZOaLcW4z/T4TI9aGWrNX5ZlICwChKWG4Wo5qriLImdww3H7bETPW9vYtSiADNlA4wSQ==", + "requires": { + "@types/mongodb": "^3.5.27", + "@types/node": "14.x || 15.x", + "bson": "^1.1.4", + "kareem": "2.3.2", + "mongodb": "3.6.8", + "mongoose-legacy-pluralize": "1.0.2", + "mpath": "0.8.3", + "mquery": "3.2.5", + "ms": "2.1.2", + "regexp-clone": "1.0.0", + "safe-buffer": "5.2.1", + "sift": "13.5.2", + "sliced": "1.0.1" + }, + "dependencies": { + "@types/node": { + "version": "15.14.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-15.14.2.tgz", + "integrity": "sha512-dvMUE/m2LbXPwlvVuzCyslTEtQ2ZwuuFClDrOQ6mp2CenCg971719PTILZ4I6bTP27xfFFc+o7x2TkLuun/MPw==" + }, + "mongodb": { + "version": "3.6.8", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.8.tgz", + "integrity": "sha512-sDjJvI73WjON1vapcbyBD3Ao9/VN3TKYY8/QX9EPbs22KaCSrQ5rXo5ZZd44tWJ3wl3FlnrFZ+KyUtNH6+1ZPQ==", + "requires": { + "bl": "^2.2.1", + "bson": "^1.1.4", + "denque": "^1.4.1", + "optional-require": "^1.0.3", + "safe-buffer": "^5.1.2", + "saslprep": "^1.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } + } + }, + "mongoose-legacy-pluralize": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz", + "integrity": "sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ==" + }, + "mpath": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.8.3.tgz", + "integrity": "sha512-eb9rRvhDltXVNL6Fxd2zM9D4vKBxjVVQNLNijlj7uoXUy19zNDsIif5zR+pWmPCWNKwAtqyo4JveQm4nfD5+eA==" + }, + "mquery": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-3.2.5.tgz", + "integrity": "sha512-VjOKHHgU84wij7IUoZzFRU07IAxd5kWJaDmyUzQlbjHjyoeK5TNeeo8ZsFDtTYnSgpW6n/nMNIHvE3u8Lbrf4A==", + "requires": { + "bluebird": "3.5.1", + "debug": "3.1.0", + "regexp-clone": "^1.0.0", + "safe-buffer": "5.1.2", + "sliced": "1.0.1" + }, + "dependencies": { + "bluebird": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -7082,15 +7212,15 @@ } }, "pegin-address-verificator": { - "version": "git+ssh://git@github.com/rsksmart/pegin-address-verifier.git#840ee617889d9e103efbd2626f84c382db379dba", - "from": "pegin-address-verificator@github:rsksmart/pegin-address-verifier#master", + "version": "github:rsksmart/pegin-address-verifier#840ee617889d9e103efbd2626f84c382db379dba", + "from": "github:rsksmart/pegin-address-verifier#master", "requires": { "jssha": "^3.1.0" } }, "pegin-cap-evaluator": { - "version": "git+ssh://git@github.com/rsksmart/pegin-cap-evaluator.git#c5fc572ebb80e231e75c0b8970bb08a9bcb2681b", - "from": "pegin-cap-evaluator@github:rsksmart/pegin-cap-evaluator#master", + "version": "github:rsksmart/pegin-cap-evaluator#c5fc572ebb80e231e75c0b8970bb08a9bcb2681b", + "from": "github:rsksmart/pegin-cap-evaluator#master", "requires": { "@rsksmart/rsk-precompiled-abis": "^3.0.0-PAPYRUS", "jssha": "^3.1.0", @@ -7425,6 +7555,11 @@ "integrity": "sha512-Yvz9NH8uFHzD/AXX82Li1GdAP6FzDBxEZw+njerNBBQv/XHihqsWAjNfXtaq4QD2l4TEZVnp4UbktdYSegAM3g==", "dev": true }, + "regexp-clone": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-1.0.0.tgz", + "integrity": "sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw==" + }, "regexp.prototype.flags": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz", @@ -7798,6 +7933,11 @@ "object-inspect": "^1.9.0" } }, + "sift": { + "version": "13.5.2", + "resolved": "https://registry.npmjs.org/sift/-/sift-13.5.2.tgz", + "integrity": "sha512-+gxdEOMA2J+AI+fVsCqeNn7Tgx3M9ZN9jdi95939l1IJ8cZsqS8sqpJyOkic2SJk+1+98Uwryt/gL6XDaV+UZA==" + }, "signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", @@ -7907,6 +8047,11 @@ } } }, + "sliced": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", + "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" + }, "slugify": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.5.3.tgz", diff --git a/package.json b/package.json index 80fe663c..10a97e12 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,8 @@ "rebuild": "npm run clean && npm run build", "prestart": "npm run rebuild", "start": "node -r source-map-support/register .", + "start-daemon": "npm run prestart && node -r source-map-support/register . --appmode=DAEMON", + "start-api": "npm run prestart && node -r source-map-support/register . --appmode=API", "clean": "lb-clean dist *.tsbuildinfo .eslintcache" }, "repository": { @@ -57,12 +59,15 @@ "@loopback/rest-explorer": "^3.0.5", "@loopback/service-proxy": "^3.0.5", "@rsksmart/rsk-precompiled-abis": "^3.0.0-PAPYRUS", - "dotenv": "^8.2.0", + "@types/mongoose": "^5.11.97", + "dotenv": "^8.6.0", + "js-sha256": "^0.9.0", "log4js": "^6.3.0", "loopback-connector-kv-redis": "^3.0.3", "loopback-connector-mongodb": "^5.5.0", "loopback-connector-redis": "^3.0.0", "loopback-connector-rest": "^3.7.0", + "mongoose": "^5.13.2", "pegin-address-verificator": "github:rsksmart/pegin-address-verifier#master", "pegin-cap-evaluator": "github:rsksmart/pegin-cap-evaluator#master", "tslib": "^2.0.0", @@ -74,6 +79,7 @@ "@loopback/build": "^6.2.8", "@loopback/eslint-config": "^10.0.4", "@loopback/testlab": "^3.2.10", + "@types/bs58": "^4.0.1", "@types/node": "^10.17.48", "eslint": "^7.15.0", "sonarqube-scanner": "^2.8.0", diff --git a/rsk-database/docker-compose.yml b/rsk-database/docker-compose.yml new file mode 100644 index 00000000..563bc4c5 --- /dev/null +++ b/rsk-database/docker-compose.yml @@ -0,0 +1,20 @@ +version: '3.1' + +services: + + mongo: + image: mongo + container_name: '2wp-rsk-mongo-database' + restart: always + environment: + MONGO_INITDB_ROOT_USERNAME: ${RSK_DB_ROOT_USER} + MONGO_INITDB_ROOT_PASSWORD: ${RSK_DB_ROOT_PASS} + MONGO_INITDB_DATABASE: ${RSK_DB_NAME} + MONGO_INITDB_USER: ${RSK_DB_USER} + MONGO_INITDB_PWD: ${RSK_DB_PASS} + volumes: + - ./mongo-init.sh/:/docker-entrypoint-initdb.d/mongo-init.sh:ro + # - ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro + - ./db:/data/db + ports: + - '27017-27019:27017-27019' diff --git a/rsk-database/mongo-init.sh b/rsk-database/mongo-init.sh new file mode 100644 index 00000000..aea9097f --- /dev/null +++ b/rsk-database/mongo-init.sh @@ -0,0 +1,18 @@ +set -e +DB=rsk # $MONGO_INITDB_DATABASE +USER=api-user # $MONGO_INITDB_USER +PASS=pwd # $MONGO_INITDB_PASS +# TODO: for some reason the sh file is not using the env variables set in the yml file so I'm hardcoding them + +mongo < { + let app: TwpapiApplication; + let client: Client; + + before('setupApplication', async () => { + ({app, client} = await setupApplication()); + }); + + after(async () => { + await app.stop(); + }); + + it('invokes GET /get-pegin-status with a txId', async () => { + const response = await client + .get('/get-pegin-status') + .send({ + txId: 'btcTxId', + }) + .expect(200); + console.log(response.text); + }); +}); diff --git a/src/controllers/index.ts b/src/controllers/index.ts index 6d48d730..a50ea90e 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,6 +1,8 @@ -export * from './pegin-configuration.controller'; export * from './balance.controller'; +export * from './broadcast.controller'; +export * from './pegin-configuration.controller'; +export * from './pegin-status.controller'; export * from './pegin-tx.controller'; export * from './tx-fee.controller'; -export * from './broadcast.controller'; export * from './tx.controller'; + diff --git a/src/controllers/pegin-status.controller.ts b/src/controllers/pegin-status.controller.ts new file mode 100644 index 00000000..37132a4c --- /dev/null +++ b/src/controllers/pegin-status.controller.ts @@ -0,0 +1,40 @@ +import {inject} from '@loopback/core'; +import {get, getModelSchemaRef} from '@loopback/rest'; +import {PeginStatus} from '../models'; +import {PeginStatusService, TxV2Service} from '../services'; +import {PeginStatusMongoDbDataService} from '../services/pegin-status-data-services/peg-status.mongodb.service'; +import {PeginStatusDataService} from '../services/pegin-status-data-services/pegin-status-data.service'; +import {BitcoinService} from '../services/pegin-status/bitcoin.service'; + +export class PeginStatusController { + private peginStatusService: PeginStatusService; + + constructor( + @inject('services.TxV2Service') + protected txV2Service: TxV2Service, + protected bitcoinService: BitcoinService = new BitcoinService(txV2Service), + ) { + const MONGO_DB_URI: string = `mongodb://${process.env.RSK_DB_USER}:${process.env.RSK_DB_PASS}@${process.env.RSK_DB_URL}:${process.env.RSK_DB_PORT}/${process.env.RSK_DB_NAME}`; + const rskDataService: PeginStatusDataService = new PeginStatusMongoDbDataService(MONGO_DB_URI); + this.peginStatusService = new PeginStatusService(bitcoinService, rskDataService); + } + + @get('/pegin-status', { + parameters: [{name: 'txId', schema: {type: 'string'}, in: 'query'}], + responses: { + '200': { + description: 'return informartion for Pegin Status', + content: { + 'application/json': { + schema: getModelSchemaRef(PeginStatus, {includeRelations: true}), + }, + }, + }, + }, + }) + getTx(txId: string): Promise { + //FIXME: filter request incorrect and return our errors and not loopback error + return this.peginStatusService.getPeginSatusInfo(txId); + } +} + diff --git a/src/controllers/tx.controller.ts b/src/controllers/tx.controller.ts index fd96edb5..20837d9a 100644 --- a/src/controllers/tx.controller.ts +++ b/src/controllers/tx.controller.ts @@ -1,17 +1,18 @@ -// Uncomment these imports to begin using these cool features! - -// import {inject} from '@loopback/core'; - import {inject} from '@loopback/core'; -import {TxService} from '../services'; import {get, getModelSchemaRef} from '@loopback/rest'; +import {getLogger, Logger} from 'log4js'; import {Tx} from '../models'; +import {TxService} from '../services'; export class TxController { + logger: Logger; + constructor( @inject('services.TxService') protected txService: TxService, - ) {} + ) { + this.logger = getLogger('tx-controller'); + } @get('/tx', { parameters: [{name: 'tx', schema: {type: 'string'}, in: 'query'}], diff --git a/src/daemon-runner.ts b/src/daemon-runner.ts new file mode 100644 index 00000000..518a2172 --- /dev/null +++ b/src/daemon-runner.ts @@ -0,0 +1,26 @@ +import {DaemonService} from './services/daemon.service'; +import {NodeBridgeDataProvider} from './services/node-bridge-data.provider'; +import {PeginStatusMongoDbDataService} from './services/pegin-status-data-services/peg-status.mongodb.service'; +import {SyncStatusMongoService} from './services/sync-status-mongo.service'; + +export class DaemonRunner { + daemonService: DaemonService; + + constructor() { + const MONGO_DB_URI = `mongodb://${process.env.RSK_DB_USER}:${process.env.RSK_DB_PASS}@${process.env.RSK_DB_URL}:${process.env.RSK_DB_PORT}/${process.env.RSK_DB_NAME}`; + // TODO: The provider should be injected + this.daemonService = new DaemonService( + new NodeBridgeDataProvider(), + new PeginStatusMongoDbDataService(MONGO_DB_URI), + new SyncStatusMongoService(MONGO_DB_URI) + ); + } + + start(): Promise { + return this.daemonService.start(); + } + + stop(): Promise { + return this.daemonService.stop(); + } +} diff --git a/src/datasources/index.ts b/src/datasources/index.ts index fb220bcf..43dfe11a 100644 --- a/src/datasources/index.ts +++ b/src/datasources/index.ts @@ -1,6 +1,8 @@ export * from './db.datasource'; -export * from './utxo-provider.datasource'; export * from './redis.datasource'; +export * from './tx-broadcast.datasource'; export * from './tx-fee-provider.datasource'; export * from './tx-provider.datasource'; -export * from './tx-broadcast.datasource'; +export * from './tx-v2-provider.datasource'; +export * from './utxo-provider.datasource'; + diff --git a/src/datasources/tx-provider.datasource.ts b/src/datasources/tx-provider.datasource.ts index a07383a9..51a580d5 100644 --- a/src/datasources/tx-provider.datasource.ts +++ b/src/datasources/tx-provider.datasource.ts @@ -37,8 +37,7 @@ const confg = { @lifeCycleObserver('datasource') export class TxProviderDataSource extends juggler.DataSource - implements LifeCycleObserver -{ + implements LifeCycleObserver { static dataSourceName = 'txProvider'; static readonly defaultConfig = confg; diff --git a/src/datasources/tx-v2-provider.datasource.ts b/src/datasources/tx-v2-provider.datasource.ts new file mode 100644 index 00000000..4c828f3b --- /dev/null +++ b/src/datasources/tx-v2-provider.datasource.ts @@ -0,0 +1,50 @@ +import {inject, lifeCycleObserver, LifeCycleObserver} from '@loopback/core'; +import {juggler} from '@loopback/repository'; +import {config} from 'dotenv'; + +config(); + +const blockBookUrl = + process.env.BLOCKBOOK_URL ?? 'https://blockbook.trugroup.tech:19130'; + +const confg = { + name: 'txV2Provider', + connector: 'rest', + options: { + headers: { + accept: 'application/json', + 'content-type': 'application/json', + }, + }, + operations: [ + { + template: { + method: 'GET', + url: `${blockBookUrl}/api/v2/tx/{txId}`, + responsePath: '$', + }, + functions: { + txV2Provider: ['txId'], + }, + }, + ], +}; + +// Observe application's life cycle to disconnect the datasource when +// application is stopped. This allows the application to be shut down +// gracefully. The `stop()` method is inherited from `juggler.DataSource`. +// Learn more at https://loopback.io/doc/en/lb4/Life-cycle.html +@lifeCycleObserver('datasource') +export class TxV2ProviderDataSource + extends juggler.DataSource + implements LifeCycleObserver { + static dataSourceName = 'txV2Provider'; + static readonly defaultConfig = confg; + + constructor( + @inject('datasources.config.txV2Provider', {optional: true}) + dsConfig: object = confg, + ) { + super(dsConfig); + } +} diff --git a/src/datasources/utxo-provider.datasource.ts b/src/datasources/utxo-provider.datasource.ts index e553a1ba..9e95c811 100644 --- a/src/datasources/utxo-provider.datasource.ts +++ b/src/datasources/utxo-provider.datasource.ts @@ -37,8 +37,7 @@ const confg = { @lifeCycleObserver('datasource') export class UtxoProviderDataSource extends juggler.DataSource - implements LifeCycleObserver -{ + implements LifeCycleObserver { static dataSourceName = 'utxoProvider'; static readonly defaultConfig = confg; diff --git a/src/index.ts b/src/index.ts index 85909d9e..e5aba091 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,18 +1,46 @@ +import {config} from 'dotenv'; import {configure, getLogger} from 'log4js'; import {ApplicationConfig, TwpapiApplication} from './application'; +import {DaemonRunner} from './daemon-runner'; export * from './application'; -export async function main( - options: ApplicationConfig = {}, -): Promise { +enum APP_MODE { + API, + DAEMON, + ALL +}; + +const searchAppMode = (): APP_MODE => { + let arg = process.argv.find(a => a.startsWith('--appmode=')); + if (arg) { + let value: string = arg.split('=')[1]; + let parsedEnum = APP_MODE[value as keyof typeof APP_MODE]; + return parsedEnum !== undefined ? parsedEnum : APP_MODE.ALL; + } + return APP_MODE.ALL; +}; + +export async function main(options: ApplicationConfig = {}): Promise { configure('./log-config.json'); const logger = getLogger('app'); + let api: TwpapiApplication; + let daemon: DaemonRunner; + + let shuttingDown = false; async function shutdown() { - logger.info('Shutting down'); - await app.stop(); + if (!shuttingDown) { + shuttingDown = true; + if (api) { + await api.stop(); + } + if (daemon) { + await daemon.stop(); + } + logger.info('Shutting down'); + } } //catches ctrl+c event @@ -23,14 +51,21 @@ export async function main( // eslint-disable-next-line @typescript-eslint/no-misused-promises process.on('uncaughtException', shutdown.bind(null)); - const app = new TwpapiApplication(options); - await app.boot(); - await app.start(); + let appMode = searchAppMode(); - const url = app.restServer.url; - logger.info(`Server is running at ${url}`); + config(); + if (appMode == APP_MODE.API || appMode == APP_MODE.ALL) { + api = new TwpapiApplication(options); + await api.boot(); + await api.start(); - return app; + const url = api.restServer.url; + logger.info(`Server is running at ${url}`); + } + if (appMode == APP_MODE.DAEMON || appMode == APP_MODE.ALL) { + daemon = new DaemonRunner(); + await daemon.start(); + } } if (require.main === module) { diff --git a/src/models/bitcoin-tx.model.ts b/src/models/bitcoin-tx.model.ts new file mode 100644 index 00000000..a090fe9b --- /dev/null +++ b/src/models/bitcoin-tx.model.ts @@ -0,0 +1,92 @@ +import {Model, model, property} from '@loopback/repository'; +import {Vin} from './vin.model'; +import {Vout} from './vout.model'; + + +@model({settings: {strict: false}}) +export class BitcoinTx extends Model { + + @property({ + type: 'string', + }) + txId: string; + + @property({ + type: 'number', + }) + version: number; + + @property({ + type: 'array', + itemType: 'object', + }) + vin: Vin[]; + + @property({ + type: 'array', + itemType: 'object', + }) + vout: Vout[]; + + @property({ + type: 'string', + }) + blockHash: string; + + @property({ + type: 'number', + }) + blockHeight: number; + + @property({ + type: 'number', + }) + confirmations: number; + + @property({ + type: 'number', + }) + time: number; + + @property({ + type: 'number', + }) + blockTime: number; + + @property({ + type: 'string', + }) + valueOut?: string; + + @property({ + type: 'string', + }) + valueIn?: string; + + @property({ + type: 'string', + }) + fees?: string; + + @property({ + type: 'string', + required: true, + }) + hex: string; + + // Define well-known properties here + + // Indexer property to allow additional data + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [prop: string]: any; + + constructor() { + super(); + } +} + +export interface BitcoinTxRelations { + // describe navigational properties here +} + +export type BitcoinTxWithRelations = BitcoinTx & BitcoinTxRelations; diff --git a/src/models/bridge-data-filter.model.ts b/src/models/bridge-data-filter.model.ts new file mode 100644 index 00000000..695fcfd2 --- /dev/null +++ b/src/models/bridge-data-filter.model.ts @@ -0,0 +1,12 @@ +export class BridgeDataFilterModel { + abiEncodedSignature: string; + + constructor(abiEncodedSignature: string) { + this.abiEncodedSignature = abiEncodedSignature; + } + + isMethodCall(callData: string) { + return callData.startsWith(this.abiEncodedSignature) || + callData.startsWith('0x' + this.abiEncodedSignature); + } +} diff --git a/src/models/index.ts b/src/models/index.ts index 48439c40..07a5a6bb 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,17 +1,19 @@ -export * from './pegin-configuration.model'; export * from './account-balance.model'; -export * from './wallet-address.model'; -export * from './utxo.model'; export * from './address-balance.model'; -export * from './session.model'; -export * from './get-balance.model'; -export * from './fee-amount-data.model'; +export * from './broadcast-request.model'; +export * from './broadcast-response.model'; export * from './create-pegin-tx-data.model'; +export * from './fee-amount-data.model'; +export * from './fee-request-data.model'; +export * from './get-balance.model'; +export * from './normalized-tx.model'; +export * from './pegin-configuration.model'; +export * from './pegin-status.model'; +export * from './session.model'; export * from './tx-input.model'; export * from './tx-output.model'; -export * from './normalized-tx.model'; -export * from './fee-request-data.model'; -export * from './broadcast-response.model'; -export * from './broadcast-request.model'; -export * from './tx.model'; export * from './tx-request.model'; +export * from './tx.model'; +export * from './utxo.model'; +export * from './wallet-address.model'; + diff --git a/src/models/pegin-status.model.ts b/src/models/pegin-status.model.ts new file mode 100644 index 00000000..196e52ad --- /dev/null +++ b/src/models/pegin-status.model.ts @@ -0,0 +1,139 @@ +import {Model, model, property} from '@loopback/repository'; + +@model() +export class BtcPeginStatus extends Model { + @property({ + type: 'string', + }) + txId: string; + + @property({ + type: 'date', + defaultFn: 'now' + }) + creationDate: Date; + + @property({ + type: 'string', + }) + federationAddress: string; + + @property({ + type: 'number', + }) + amountTransferred: number; + + @property({ + type: 'string', + }) + refundAddress: string; + + @property({ + type: 'number', + }) + confirmations: number; + + @property({ + type: 'number', + }) + requiredConfirmation: number; + + constructor(btcTxId: string) { + super(); + this.txId = btcTxId; + } +} + +@model() +export class RskPeginStatus extends Model { + @property({ + type: 'string', + }) + recipientAddress: string; + + @property({ + type: 'number', + }) + confirmations: number; + + @property({ + type: 'Date', + }) + createOn: Date; + + @property({ + type: 'string', + }) + status: string; + + constructor() { + super(); + } +} + +@model() +export class PeginStatus extends Model { + @property({ + type: 'object', + required: true, + }) + btc: BtcPeginStatus; + + @property({ + type: 'object', + }) + rsk: RskPeginStatus; + + @property({ + type: 'object', + }) + status: Status; + + constructor(btc: BtcPeginStatus, rsk?: RskPeginStatus) { + super(); + this.status = Status.NOT_IN_BTC_YET; + if (rsk) { + this.rsk = rsk; + } else { + this.rsk = new RskPeginStatus(); + } + this.btc = btc; + } + + public setRskPeginStatus(rskData: RskPeginStatus) { + this.rsk = rskData; + // if (rskData.status == 'REJECTED' || rskData.status == 'INVALID') { + // this.status = Status.REJECTED_REFUND; //TODO: Maybe is without REFUND, Resolve when we can get this info + // } else if (rskData.confirmations >= (Number(process.env.RSK_MINIMUM_CONFIRMATION) ?? 6)) { //TODO: Verify number of confirmations needed + // this.status = Status.CONFIRMED; + // } else { + // this.status = Status.NOT_IN_RSK_YET; //Not confirmed, not rejected, not invalid. Only not saved in database yet. + // } + switch (rskData.status) { + case 'REJECTED': { + this.status = Status.REJECTED_REFUND; //TODO: Verify type or REJECTED + break; + } + case 'LOCKED': { + this.status = Status.CONFIRMED; + break; + } + case 'INVALID': { + throw new Error(`{this.rsk.txId} Invalid tx for RSK nodes. `); + } + default: { + this.status = Status.NOT_IN_RSK_YET; + } + } + } +} +export enum Status { + NOT_IN_BTC_YET = 'NOT_IN_BTC_YET', + WAITING_CONFIRMATIONS = 'WAITING_CONFIRMATIONS', + NOT_IN_RSK_YET = 'NOT_IN_RSK_YET', + CONFIRMED = 'CONFIRMED', + REJECTED_NO_REFUND = 'REJECTED_NO_REFUND', + REJECTED_REFUND = 'REJECTED_REFUND' +} + +export type PeginStatusWithRelations = PeginStatus; diff --git a/src/models/rsk/bridge-data.model.ts b/src/models/rsk/bridge-data.model.ts new file mode 100644 index 00000000..19cbb8e1 --- /dev/null +++ b/src/models/rsk/bridge-data.model.ts @@ -0,0 +1,9 @@ +import {Transaction} from './transaction.model'; +export class BridgeData { + data: Array; + maxBlockHeight: number; // TODO: This should be a complete block + + constructor() { + this.data = []; + } +} diff --git a/src/models/rsk/log.model.ts b/src/models/rsk/log.model.ts new file mode 100644 index 00000000..7e69e067 --- /dev/null +++ b/src/models/rsk/log.model.ts @@ -0,0 +1,8 @@ +export class Log { + topics: Array; + data: string; + + constructor() { + this.topics = []; + } +} diff --git a/src/models/rsk/pegin-status-data.model.ts b/src/models/rsk/pegin-status-data.model.ts new file mode 100644 index 00000000..8abed775 --- /dev/null +++ b/src/models/rsk/pegin-status-data.model.ts @@ -0,0 +1,9 @@ +export class PeginStatusDataModel { + btcTxId: string; + status: string; // TODO: this should be an enum + rskBlockHeight: number; + rskTxId: string; + rskRecipient: string; + createdOn: Date; + // TODO: add value field => value: BigInt; +} diff --git a/src/models/rsk/searchable-model.ts b/src/models/rsk/searchable-model.ts new file mode 100644 index 00000000..1f5ffe08 --- /dev/null +++ b/src/models/rsk/searchable-model.ts @@ -0,0 +1,4 @@ +export interface SearchableModel { + getId(): any; + getIdFieldName(): string; +} diff --git a/src/models/rsk/sync-status.model.ts b/src/models/rsk/sync-status.model.ts new file mode 100644 index 00000000..4327fdf5 --- /dev/null +++ b/src/models/rsk/sync-status.model.ts @@ -0,0 +1,15 @@ +import {SearchableModel} from './searchable-model'; + +export class SyncStatusModel implements SearchableModel { + syncId: number; + rskBlockHeight: number; + lastSyncedOn: Date; + + getId(): any { + return this.syncId; + } + + getIdFieldName(): string { + return 'syncId'; + } +} diff --git a/src/models/rsk/transaction.model.ts b/src/models/rsk/transaction.model.ts new file mode 100644 index 00000000..06a07f4b --- /dev/null +++ b/src/models/rsk/transaction.model.ts @@ -0,0 +1,14 @@ +import {Log} from './log.model'; + +export class Transaction { + hash: Buffer; + blockHash: Buffer; + blockHeight: number; + data: Buffer; + logs: Array; + createdOn: Date; + + constructor() { + this.logs = []; + } +} diff --git a/src/models/tx-output.model.ts b/src/models/tx-output.model.ts index 8a8bb579..42af50e8 100644 --- a/src/models/tx-output.model.ts +++ b/src/models/tx-output.model.ts @@ -33,6 +33,8 @@ export class TxOutput extends Model { // eslint-disable-next-line @typescript-eslint/naming-convention op_return_data?: string; + [prop: string]: any; + constructor(data?: Partial) { super(data); } diff --git a/src/models/tx-v2.model.ts b/src/models/tx-v2.model.ts new file mode 100644 index 00000000..bb406576 --- /dev/null +++ b/src/models/tx-v2.model.ts @@ -0,0 +1,91 @@ +import {Model, model, property} from '@loopback/repository'; +import {Vin} from './vin.model'; +import {Vout} from './vout.model'; + +@model({settings: {strict: false}}) +export class TxV2 extends Model { + @property({ + type: 'string', + }) + txid: string; + + @property({ + type: 'number', + }) + version: number; + + @property({ + type: 'number', + }) + locktime?: number; + + @property({ + type: 'array', + itemType: 'object', + }) + vin: Vin[]; + + @property({ + type: 'array', + itemType: 'object', + }) + vout: Vout[]; + + @property({ + type: 'string', + }) + blockhash?: string; + + @property({ + type: 'number', + }) + blockheight: number; + + @property({ + type: 'number', + }) + confirmations: number; + + @property({ + type: 'number', + }) + blocktime: number; + + @property({ + type: 'number', + }) + size: number; + + @property({ + type: 'string', + }) + valueOutSat: string; + + @property({ + type: 'string', + }) + valueInSat?: string; + + @property({ + type: 'string', + }) + feesSat?: string; + + @property({ + type: 'string', + required: true, + }) + hex: string; + + [prop: string]: any; + + constructor(data?: Partial) { + super(data); + } +} + +export interface TxV2Relations { + // describe navigational properties here +} + +export type TxV2WithRelations = TxV2 & TxV2Relations; diff --git a/src/models/vin.model.ts b/src/models/vin.model.ts new file mode 100644 index 00000000..19e43870 --- /dev/null +++ b/src/models/vin.model.ts @@ -0,0 +1,59 @@ +import {Model, model, property} from '@loopback/repository'; + +@model({settings: {strict: false}}) +export class Vin extends Model { + + @property({ + type: 'string', + }) + txid: string; + + @property({ + type: 'number', + }) + vout: number; + + @property({ + type: 'number', + }) + sequence: number; + + @property({ + type: 'number', + }) + n?: number; + + @property({ + type: 'string', + required: true, + }) + hex: string; + + @property({ + type: 'array', + itemType: 'string', + }) + addresses: string[]; + + @property({ + type: 'number', + }) + value?: number; + + + // Define well-known properties here + + // Indexer property to allow additional data + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [prop: string]: any; + + constructor(data?: Partial) { + super(data); + } +} + +export interface VinRelations { + // describe navigational properties here +} + +export type VintWithRelations = Vin & VinRelations; diff --git a/src/models/vout.model.ts b/src/models/vout.model.ts new file mode 100644 index 00000000..a3b25dd9 --- /dev/null +++ b/src/models/vout.model.ts @@ -0,0 +1,84 @@ +import {Model, model, property} from '@loopback/repository'; + +@model({settings: {strict: false}}) +export class Vout extends Model { + @property({ + type: 'number', + }) + valueSat?: number; + + @property({ + type: 'number', + }) + n: number; + + @property({ + type: 'boolean', + }) + spent?: boolean; + + @property({ + type: 'string', + }) + spentTxID: string; + + @property({ + type: 'number', + }) + spentIndex?: number; + + @property({ + type: 'number', + }) + spentHeight?: number; + + @property({ + type: 'string', + required: true, + }) + hex?: string; + + @property({ + type: 'string', + required: true, + }) + asm?: string; + + @property({ + type: 'string', + }) + addrDesc?: string; + + @property({ + type: 'array', + itemType: 'string', + }) + addresses: string[]; + + @property({ + type: 'boolean', + }) + isAddress: boolean; + + @property({ + type: 'string', + }) + type?: string; + + + // Define well-known properties here + + // Indexer property to allow additional data + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [prop: string]: any; + + constructor(data?: Partial) { + super(data); + } +} + +export interface VoutRelations { + // describe navigational properties here +} + +export type VoutWithRelations = Vout & VoutRelations; diff --git a/src/services/daemon.service.ts b/src/services/daemon.service.ts new file mode 100644 index 00000000..f6a1131a --- /dev/null +++ b/src/services/daemon.service.ts @@ -0,0 +1,154 @@ +import {getLogger, Logger} from 'log4js'; +import {BridgeDataFilterModel} from '../models/bridge-data-filter.model'; +import {SyncStatusModel} from '../models/rsk/sync-status.model'; +import {GenericDataService} from './generic-data-service'; +import {PeginStatusDataService} from './pegin-status-data-services/pegin-status-data.service'; +import {RegisterBtcTransactionDataParser} from './register-btc-transaction-data.parser'; +import {RskBridgeDataProvider} from './rsk-bridge-data.provider'; + +const SYNC_ID: number = 1; + +export class DaemonService implements iDaemonService { + dataProvider: RskBridgeDataProvider; + peginStatusStorageService: PeginStatusDataService; + syncStorageService: GenericDataService; + registerBtcTransactionDataParser: RegisterBtcTransactionDataParser; + + dataFetchInterval: NodeJS.Timer; + started: boolean; + logger: Logger; + lastSyncLog: number = 0; + ticking: boolean; + lastBlock: number; + + intervalTime: number; + defaultLastBlock: number; + minDepthToSync: number; + + constructor( + dataProvider: RskBridgeDataProvider, + peginStatusStorageService: PeginStatusDataService, + syncStorageService: GenericDataService + ) { + this.dataProvider = dataProvider; + this.peginStatusStorageService = peginStatusStorageService; + this.syncStorageService = syncStorageService; + this.registerBtcTransactionDataParser = new RegisterBtcTransactionDataParser(); + + this.started = false; + this.logger = getLogger('daemon-service'); + + // TODO: add configurations via injection/env variables + this.intervalTime = 5000; + this.defaultLastBlock = 1930363; + this.minDepthToSync = 5; + } + + async start(): Promise { + if (this.started) { + return; + } + this.logger.trace('Starting'); + await this.peginStatusStorageService.start(); + await this.syncStorageService.start(); + // Get initial sync status + this.lastBlock = (await this.getSyncStatus()).rskBlockHeight; + + this.configureDataFilters(); + // Start up the daemon + this.dataFetchInterval = setInterval(() => this.fetch(), this.intervalTime); + + this.logger.debug('Started'); + this.started = true; + } + + async stop(): Promise { + if (this.started) { + this.logger.trace('Stopping'); + clearInterval(this.dataFetchInterval); + await this.peginStatusStorageService.stop() + this.started = false; + this.logger.debug('Stopped'); + } + } + + private async fetch(): Promise { + // Avoid processing if the service has not started or was stopped + if (!this.started) { + return; + } + // Avoid processing more fetches if there is a pending fetch + if (this.ticking) { + return; + } + this.ticking = true; + try { + this.lastSyncLog++; + if (this.lastSyncLog >= 10) { + this.lastSyncLog = 0; + this.logger.trace("Sync status => Last block is " + this.lastBlock); + } + let response = await this.dataProvider.getData(this.lastBlock); + this.lastBlock = response.maxBlockHeight + 1; + await this.updateSyncStatus(); + for (let tx of response.data) { + this.logger.debug(`Got tx ${tx.hash}`); + let peginStatus = this.registerBtcTransactionDataParser.parse(tx); + if (!peginStatus) { + this.logger.debug('Transaction is not a registerBtcTransaction or has not registered the peg-in'); + continue; + } + try { + let found = await this.peginStatusStorageService.getPeginStatus(peginStatus.btcTxId); + if (found) { + this.logger.debug(`${tx.hash} already registered`); + } else { + await this.peginStatusStorageService.setPeginStatus(peginStatus); + } + } catch (e) { + this.logger.warn('There was a problem with the storage', e); + } + } + } catch (error) { + this.logger.warn('Got an error fetching data', error.message); + } + this.ticking = false; + } + + private configureDataFilters(): void { + let dataFilters = []; + // registerBtcTransaction data filter + dataFilters.push(new BridgeDataFilterModel('43dc0656')); + this.dataProvider.configure(dataFilters); + } + + private getSyncStatus(): Promise { + return this.syncStorageService.getMany().then(result => { + if (!result || result.length == 0) { + let syncStatusModel = new SyncStatusModel(); + syncStatusModel.syncId = SYNC_ID; + syncStatusModel.rskBlockHeight = this.defaultLastBlock; + syncStatusModel.lastSyncedOn = new Date(); + return syncStatusModel; + } + if (result.length > 1) { + throw new Error('Multiple sync status found!'); + } + return result[0]; + }); + } + + private updateSyncStatus(): Promise { + let data = new SyncStatusModel(); + data.syncId = SYNC_ID; + data.rskBlockHeight = this.lastBlock; + data.lastSyncedOn = new Date(); + return this.syncStorageService.set(data); + } +} + +export interface iDaemonService { + start(): void; + + stop(): void; +} diff --git a/src/services/generic-data-service.ts b/src/services/generic-data-service.ts new file mode 100644 index 00000000..ec4b84f5 --- /dev/null +++ b/src/services/generic-data-service.ts @@ -0,0 +1,9 @@ +import {SearchableModel} from '../models/rsk/searchable-model'; + +export interface GenericDataService { + getById(id: any): Promise; + getMany(query?: any): Promise>; + set(data: Type): Promise; + start(): Promise; + stop(): Promise; +} diff --git a/src/services/index.ts b/src/services/index.ts index 522aabf7..46442e25 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,5 +1,9 @@ -export * from './utxo-provider.service'; +export * from './bridge.service'; +export * from './broadcast.service'; export * from './fee-level.service'; +export * from './pegin-status/bitcoin.service'; +export * from './pegin-status/pegin-status.service'; export * from './tx-service.service'; -export * from './broadcast.service'; -export * from './bridge.service'; +export * from './tx-v2-service.service'; +export * from './utxo-provider.service'; + diff --git a/src/services/mongodb-data.service.ts b/src/services/mongodb-data.service.ts new file mode 100644 index 00000000..0f021e70 --- /dev/null +++ b/src/services/mongodb-data.service.ts @@ -0,0 +1,92 @@ +import {getLogger, Logger} from 'log4js'; +import mongoose from 'mongoose'; +import {SearchableModel} from '../models/rsk/searchable-model'; +import {getMetricLogger} from '../utils/metric-logger'; +import {GenericDataService} from './generic-data-service'; + +export abstract class MongoDbDataService implements GenericDataService { + mongoDbUri: string; + logger: Logger; + db: mongoose.Mongoose; + constructor(mongoDbUri: string) { + this.mongoDbUri = mongoDbUri; + this.logger = getLogger(this.getLoggerName()); + } + + protected abstract getLoggerName(): string; + + protected abstract getConnector(): mongoose.Model; + + protected abstract getByIdFilter(id: any): any; + + protected abstract getManyFilter(filter?: any): any; + + getById(id: any): Promise { + let p = Promise.resolve(); + if (!this.db) { + p.then(() => this.start()); + } + return p.then(() => { + return this.getConnector() + .findOne(this.getByIdFilter(id)) + .exec() + .then((result: any) => (result)); // The db model matches the DTO model so parsing it should do the trick + }); + } + + getMany(query?: any): Promise { + return this.getConnector() + .find(this.getManyFilter(query)) + .exec() + .then(result => result.map((r: any) => (r))); + } + + set(data: Type): Promise { + let metricLogger = getMetricLogger(this.logger, 'set'); + let p = Promise.resolve(); + if (!this.db) { + p.then(() => this.start()); + } + return p.then(() => { + return new Promise((resolve, reject) => { + if (!data) { + let err = 'Data was not provided'; + this.logger.debug(err); + reject(err); + } + let connector = this.getConnector(); + let filter: any = {}; + filter[data.getIdFieldName()] = data.getId(); + connector.findOneAndUpdate(filter, data, {upsert: true}, (err: any) => { + metricLogger(); + if (err) { + this.logger.debug('There was an error trying to save data', err); + reject(err); + } else { + resolve(true); + } + }) + }); + }); + } + + start(): Promise { + return mongoose.connect(this.mongoDbUri, {useUnifiedTopology: true}) + .then( + (connection: mongoose.Mongoose) => { + this.db = connection; + this.logger.debug('connected to mongodb'); + }, + err => { + this.logger.error('there was an error connecting to mongodb', err); + throw err; + } + ); + } + + stop(): Promise { + this.logger.debug('Shutting down'); + return this.db.disconnect(); + } + +} diff --git a/src/services/node-bridge-data.provider.ts b/src/services/node-bridge-data.provider.ts new file mode 100644 index 00000000..192259fa --- /dev/null +++ b/src/services/node-bridge-data.provider.ts @@ -0,0 +1,61 @@ +import {getLogger, Logger} from 'log4js'; +import {BridgeDataFilterModel} from '../models/bridge-data-filter.model'; +import {BridgeData} from '../models/rsk/bridge-data.model'; +import {Log} from '../models/rsk/log.model'; +import {Transaction} from '../models/rsk/transaction.model'; +import {getMetricLogger} from '../utils/metric-logger'; +import {RskBridgeDataProvider} from './rsk-bridge-data.provider'; +import {RskNodeService} from './rsk-node.service'; + +export class NodeBridgeDataProvider implements RskBridgeDataProvider { + rskNodeService: RskNodeService; + logger: Logger; + filters: Array; + constructor() { + this.rskNodeService = new RskNodeService(); + this.filters = []; + this.logger = getLogger('nodeBridgeDataProvider'); + } + async getData(startingBlock: string | number): Promise { + let metricLogger = getMetricLogger(this.logger, 'getData'); + let data: BridgeData = new BridgeData(); + // this.logger.trace(`Fetching data for block ${startingBlock}`); + let lastBlock = await this.rskNodeService.getBlock(startingBlock); + if (lastBlock == null) { + throw new Error(`Block ${startingBlock} doesn't exist`); + } + data.maxBlockHeight = lastBlock.number; + for (let transaction of lastBlock.transactions) { + // TODO: determine why using the precompiled abis reference is not working + if (transaction.to !== '0x0000000000000000000000000000000001000006') { + continue; + } + + this.logger.trace(`Found a bridge tx ${transaction.hash} with signature ${transaction.input.substring(0, 10)}`); + + if (this.filters.length == 0 || + this.filters.some(f => f.isMethodCall(transaction.input)) + ) { + this.logger.debug(`Tx ${transaction.hash} matches filters`); + + let tx = new Transaction(); + tx.blockHeight = lastBlock.number; + tx.blockHash = lastBlock.hash; + tx.createdOn = new Date(lastBlock.timestamp * 1000); + tx.hash = transaction.hash; + tx.data = transaction.input; + let txReceipt = await this.rskNodeService.getTransactionReceipt(tx.hash.toString('hex')); + tx.logs = >txReceipt.logs; + data.data.push(tx); + } + } + + metricLogger(); + return Promise.resolve(data); + } + + configure(filters: Array): void { + this.filters = filters || []; + } + +} diff --git a/src/services/pegin-status-data-services/peg-status.mongodb.service.ts b/src/services/pegin-status-data-services/peg-status.mongodb.service.ts new file mode 100644 index 00000000..e1cad136 --- /dev/null +++ b/src/services/pegin-status-data-services/peg-status.mongodb.service.ts @@ -0,0 +1,90 @@ +import {getLogger, Logger} from 'log4js'; +import mongoose from 'mongoose'; +import {PeginStatusDataModel} from '../../models/rsk/pegin-status-data.model'; +import {PeginStatusDataService} from './pegin-status-data.service'; + +/* +- THESE MODEL INTERFACES AND CLASSES ARE REQUIRED FOR MONGO BUT WE DON'T WANT THEM EXPOSED OUT OF THIS LAYER +*/ +interface PeginStatusMongoModel extends mongoose.Document, PeginStatusDataModel { +} + +const PeginStatusSchema = new mongoose.Schema({ + btcTxId: {type: String, required: true, unique: true}, + status: {type: String, required: true}, + rskBlockHeight: {type: Number, required: true}, + rskTxId: {type: String, required: true, unique: true}, + rskRecipient: {type: String, required: true}, + createdOn: {type: Date, required: true} +}); + +const PeginStatusConnector = mongoose.model("PeginStatus", PeginStatusSchema); + +export class PeginStatusMongoDbDataService implements PeginStatusDataService { + mongoDbUri: string; + logger: Logger; + db: mongoose.Mongoose; + constructor(mongoDbUri: string) { + this.mongoDbUri = mongoDbUri; + this.logger = getLogger('pegin-status-mongo-service'); + } + + getPeginStatus(btcTxId: string): Promise { + let p = Promise.resolve(); + if (!this.db) { + p.then(() => this.start()); + } + return p.then(() => { + return PeginStatusConnector + .findOne({btcTxId: btcTxId}) + .exec() + .then((result: any) => (result)); // The db model matches the DTO model so parsing it should do the trick + }); + } + + setPeginStatus(data: PeginStatusDataModel): Promise { + let p = Promise.resolve(); + if (!this.db) { + p.then(() => this.start()); + } + return p.then(() => { + return new Promise((resolve, reject) => { + if (!data) { + let err = 'data was not provided'; + this.logger.debug(err); + reject(err); + } + let model = new PeginStatusConnector(data); + model.save((err: any) => { + if (err) { + this.logger.debug('there was an error trying to save pegin status with btc tx id ' + data.btcTxId, err); + reject(err); + } else { + this.logger.trace('properly saved item with btc tx id ' + data.btcTxId); + resolve(); + } + }) + }); + }); + } + + start(): Promise { + return mongoose.connect(this.mongoDbUri, {useUnifiedTopology: true}) + .then( + (connection: mongoose.Mongoose) => { + this.db = connection; + this.logger.debug('connected to mongodb'); + }, + err => { + this.logger.error('there was an error connecting to mongodb', err); + throw err; + } + ); + } + + stop(): Promise { + this.logger.debug('Shutting down'); + return this.db.disconnect(); + } + +} diff --git a/src/services/pegin-status-data-services/pegin-status-data.service.ts b/src/services/pegin-status-data-services/pegin-status-data.service.ts new file mode 100644 index 00000000..946662af --- /dev/null +++ b/src/services/pegin-status-data-services/pegin-status-data.service.ts @@ -0,0 +1,45 @@ +import {getLogger, Logger} from 'log4js'; +import {PeginStatusDataModel} from '../../models/rsk/pegin-status-data.model'; + +export interface PeginStatusDataService { + getPeginStatus(btcTxId: string): Promise; + setPeginStatus(data: PeginStatusDataModel): Promise; + start(): Promise; + stop(): Promise; +} + +export class PeginStatusDataServiceMemoryImplementation implements PeginStatusDataService { + + dataSource: Map; + logger: Logger; + constructor() { + this.dataSource = new Map(); + this.logger = getLogger('peginStatusDataService'); + } + + getPeginStatus(btcTxId: string): Promise { + this.logger.trace(`Searching for ${btcTxId}`); + let result = this.dataSource.get(btcTxId); + if (result) { + this.logger.trace('got it'); + return Promise.resolve(result); + } + this.logger.trace('does not exist'); + throw new Error(`tx not found in storage. txid => ${btcTxId}`); + } + + setPeginStatus(data: PeginStatusDataModel) { + this.dataSource.set(data.btcTxId, data); + return Promise.resolve(); + } + + start(): Promise { + this.logger.debug('Starting'); + return Promise.resolve(); + } + + stop(): Promise { + this.logger.debug('Stopping'); + return Promise.resolve(); + } +} diff --git a/src/services/pegin-status/bitcoin.service.ts b/src/services/pegin-status/bitcoin.service.ts new file mode 100644 index 00000000..140f7076 --- /dev/null +++ b/src/services/pegin-status/bitcoin.service.ts @@ -0,0 +1,42 @@ +import {inject} from '@loopback/core'; +import {getLogger, Logger} from 'log4js'; +import {TxV2Service} from '../'; +import {BitcoinTx} from '../../models/bitcoin-tx.model'; + +export class BitcoinService { + logger: Logger; + + constructor( + @inject('services.TxV2Service') + protected txV2Service: TxV2Service, + ) { + this.logger = getLogger('bitcoin-service'); + } + + getTx(txId: string): Promise { + return new Promise((resolve, reject) => { + this.txV2Service + .txV2Provider(txId) + .then((tx: any) => { + + const responseTx = new BitcoinTx(); + responseTx.txid = tx[0].txid; + responseTx.version = tx[0].version; + responseTx.vin = tx[0].vin; + responseTx.vout = tx[0].vout; + responseTx.blockhash = tx[0].blockHash; + responseTx.blockheight = tx[0].blockHeight; + responseTx.confirmations = tx[0].confirmations; + responseTx.time = tx[0].time; + responseTx.blocktime = tx[0].blockTime; + responseTx.valueOut = tx[0].valueOut; + responseTx.valueIn = tx[0].valueIn; + responseTx.fees = tx[0].fees; + responseTx.hex = tx[0].hex; + resolve(responseTx); + }) + + .catch(reject); + }); + } +} diff --git a/src/services/pegin-status/pegin-status.service.ts b/src/services/pegin-status/pegin-status.service.ts new file mode 100644 index 00000000..f1dd32d4 --- /dev/null +++ b/src/services/pegin-status/pegin-status.service.ts @@ -0,0 +1,212 @@ +import {getLogger, Logger} from 'log4js'; +import {BitcoinService, BridgeService} from '..'; +import {BtcPeginStatus, PeginStatus, RskPeginStatus, Status} from '../../models'; +import {BitcoinTx} from '../../models/bitcoin-tx.model'; +import {Vout} from '../../models/vout.model'; +import {BtcAddressUtils} from '../../utils/btc-utils'; +import {ensure0x} from '../../utils/hex-utils'; +import {RskAddressUtils} from '../../utils/rsk-address-utils'; +import {PeginStatusDataService} from '../pegin-status-data-services/pegin-status-data.service'; +import {RskNodeService} from '../rsk-node.service'; + +export class PeginStatusService { + private logger: Logger; + private bridgeService: BridgeService; + private bitcoinService: BitcoinService; + private rskNodeService: RskNodeService; + private destinationAddress: string; + private rskDataService: PeginStatusDataService; + + constructor(bitcoinService: BitcoinService, rskDataService: PeginStatusDataService) { + this.bitcoinService = bitcoinService; + this.bridgeService = new BridgeService( + process.env.BRIDGE_ADDRESS ?? + '0x0000000000000000000000000000000001000006', + ); + this.rskNodeService = new RskNodeService(); + this.logger = getLogger('peginStatusService'); + this.rskDataService = rskDataService; + } + + public getPeginSatusInfo(btcTxId: string): Promise { + this.logger.trace(`Get Pegin information for txId: ${btcTxId}`); + return this.getBtcInfo(btcTxId) + .then((btcStatus) => { + const peginStatusInfo = new PeginStatus(btcStatus); + if (btcStatus.requiredConfirmation <= btcStatus.confirmations) { + return this.getRskInfo(btcTxId) + .then((rskStatus) => { + peginStatusInfo.setRskPeginStatus(rskStatus); + this.logger.debug(`Tx: ${btcTxId} includes rsk info. RskAddress: ${rskStatus.recipientAddress} + Pegin status: ${peginStatusInfo.status}`); + return peginStatusInfo; + }) + .finally(() => { + if (peginStatusInfo.status == Status.NOT_IN_RSK_YET) { + this.logger.debug(`Tx: ${btcTxId} does not include rsk info. Pegin status: ${peginStatusInfo.status}`); + peginStatusInfo.rsk.recipientAddress = this.destinationAddress; + return peginStatusInfo; + } + }) + } else { + this.logger.debug(`Tx: ${btcTxId} does not include rsk info. Pegin status: ${peginStatusInfo.status}`); + const peginRskInfo = new RskPeginStatus(); + peginRskInfo.recipientAddress = this.destinationAddress; + peginStatusInfo.setRskPeginStatus(peginRskInfo); + return peginStatusInfo; + } + }) + }; + + private getBtcInfo(btcTxId: string): Promise { + return this.getBtcTxInfoFromService(btcTxId) + .then(async (btcTxInformation) => { + const minPeginValue = await this.bridgeService.getMinPeginValue(); + if (this.fromSatoshiToBtc(minPeginValue) > btcTxInformation.amountTransferred) { + const errorMessage = `Amount transferred is less than minimum pegin value. + Minimum value accepted: [" + ${this.fromSatoshiToBtc(minPeginValue)}BTC]. Value sent: + [${btcTxInformation.amountTransferred}BTC]`; + this.logger.debug(errorMessage); + throw new Error(errorMessage); + } + return btcTxInformation; + }) + }; + + private getBtcTxInfoFromService(btcTxId: string): Promise { + return this.bitcoinService.getTx(btcTxId) + .then(async (btcTx: BitcoinTx) => { + //TODO: Ask federation to the database. + const federationAddress = await this.bridgeService.getFederationAddress(); + if (!this.isSentToFederationAddress(federationAddress, btcTx.vout)) { + //TODO: Comparing with the last federation. Need to include to comparing federation during the creation of the tx + const errorMessage = `Is not a pegin. Tx is not sending to Powpeg Address: ${federationAddress}`; + this.logger.debug(errorMessage); + throw new Error(errorMessage); + } else { + const btcStatus = new BtcPeginStatus(btcTxId); + const time = btcTx.time ?? btcTx.blocktime; + btcStatus.creationDate = new Date(time * 1000); // We get Timestamp in seconds + btcStatus.amountTransferred = this.fromSatoshiToBtc(this.getTxSentAmountByAddress( + federationAddress, + btcTxId, + btcTx.vout + )); + btcStatus.confirmations = Number(btcTx.confirmations) ?? 0; + btcStatus.requiredConfirmation = Number(process.env.BTC_CONFIRMATIONS) ?? 100; + btcStatus.federationAddress = federationAddress; + btcStatus.refundAddress = this.getTxRefundAddress(btcTx); + this.destinationAddress = this.getxDestinationRskAddress(btcTx); + + return btcStatus; + } + }) + } + + private getRskInfo(btcTxId: string): Promise { + const rskStatus = new RskPeginStatus(); + + return this.rskDataService.getPeginStatus(ensure0x(btcTxId)).then(async (rskData) => { + if (rskData) { + let bestHeight = await this.rskNodeService.getBlockNumber(); + rskStatus.confirmations = bestHeight - rskData.rskBlockHeight; + rskStatus.recipientAddress = rskData.rskRecipient; + rskStatus.createOn = rskData.createdOn; + rskStatus.status = rskData.status; + } + return rskStatus; + }) + } + + //TODO: Move to utils? + private fromSatoshiToBtc(btcValue: number): number { + return (btcValue / 100000000); + } + + private isSentToFederationAddress(federationAddress: string, vout: Vout[]): boolean { + for (let i = 0; vout && i < vout.length; i++) { + if (federationAddress == vout[i].addresses[0]) { + return true; + } + } + return false; + } + + private getTxSentAmountByAddress(federationAddress: string, txId: string, vout: Vout[]): number { + let acummulatedAmount = 0; + for (let i = 0; vout && i < vout.length; i++) { + if (federationAddress == vout[i].addresses[0]) { + acummulatedAmount += Number(vout[i].value!); + } + } + if (acummulatedAmount == 0) { + const errorMessage = `Can not get set amount for address: ${federationAddress} in tx: ${txId}`; + this.logger.warn(errorMessage); + throw new Error(errorMessage); + } + return acummulatedAmount; + } + + private getxDestinationRskAddress(btcTx: BitcoinTx): string { + let returnValue = ''; + let foundOpReturn = false; + const utility = new RskAddressUtils(); + + for (let i = 0; btcTx.vout && i < btcTx.vout.length && !foundOpReturn; i++) { + const voutData = btcTx.vout[i].hex!; + + if (this.hasOpReturn(btcTx.txId, voutData)) { + returnValue = utility.getRskAddressFromOpReturn(voutData.substring(14, 54)); + this.logger.debug(`Destination RSK Address found: ${returnValue}`); + foundOpReturn = true; + } + } + if (!foundOpReturn) { + //FIXME: Derivate RSK address from BTC address sender. + } + return returnValue; + } + + private getTxRefundAddress(btcTx: BitcoinTx): string { + let returnValue = ''; + let foundOpReturn = false; + const utility = new BtcAddressUtils(); + + for (let i = 0; btcTx.vout && i < btcTx.vout.length && !foundOpReturn; i++) { + const voutData = btcTx.vout[i].hex!; + if (this.hasRefundOpReturn(btcTx.txId, voutData)) { + returnValue = utility.getRefundAddress(voutData.substring(54, 96)); + this.logger.debug(`RefundAddress found: ${returnValue}`); + foundOpReturn = true; + } + } + if (!foundOpReturn) { + //FIXME: Derivate RSK address from sender + } + return returnValue; + } + + private hasRefundOpReturn(txId: string, data: string): boolean { + if (this.hasOpReturn(txId, data)) { // Includes version 01 in the same if + if (data.length == 96) { //Contain refund address + return (true); + } + } + return (false); + } + + private hasOpReturn(txId: string, data: string): boolean { + if (data.startsWith('6a') && data.substr(4, 10).startsWith('52534b5401')) { // Includes version 01 in the same if + if (data.length == 96 || data.length == 54) { //Contain refund address + this.logger.debug(`Tx contains OPT_RETURN value: ${txId}`); + return (true); + } else { + const errorMessage = `Can not parse OP_RETURN parameter. Invalid transaction: ${txId}`; + this.logger.warn(errorMessage); + return false; //RSK will return invalid + } + } + return (false); + } + +} diff --git a/src/services/register-btc-transaction-data.parser.ts b/src/services/register-btc-transaction-data.parser.ts new file mode 100644 index 00000000..90fc613c --- /dev/null +++ b/src/services/register-btc-transaction-data.parser.ts @@ -0,0 +1,122 @@ +import Web3 from 'web3'; +import {Log} from '../models/rsk/log.model'; +import {PeginStatusDataModel} from '../models/rsk/pegin-status-data.model'; +import {Transaction} from '../models/rsk/transaction.model'; +import {calculateBtcTxHash} from '../utils/btc-utils'; +import {ensure0x} from '../utils/hex-utils'; + +// TODO: this should be an actual enum +const PEGIN_STATUSES = { + locked: 'LOCKED', + rejected: 'REJECTED', + notRegistered: 'NOT_REGISTERED' +} + +// TODO: instead of hardcoding the signature we should use precompiled abis library +const LOCK_BTC_SIGNATURE = '0xec2232bdbe54a92238ce7a6b45d53fb31f919496c6abe1554be1cc8eddb6600a'; +const REJECTED_PEGIN_SIGNATURE = '0x708ce1ead20561c5894a93be3fee64b326b2ad6c198f8253e4bb56f1626053d6'; + +const BRIDGE_ABI = [{ + "name": "registerBtcTransaction", + "type": "function", + "constant": true, + "inputs": [ + { + "name": "tx", + "type": "bytes" + }, + { + "name": "height", + "type": "int256" + }, + { + "name": "pmt", + "type": "bytes" + } + ], + "outputs": [] +}]; + +class PeginStatus { + log: Log; + status: string; + + constructor(status: string) { + this.status = status; + } +} + +export class RegisterBtcTransactionDataParser { + + private getThisLogIfFound(logSignature: string, logs: Array): Log | null { + for (let log of logs) { + if (log.topics) { + for (let topic of log.topics) { + if (topic == logSignature) { + return log; + } + } + } + } + return null; + } + + private getbtcTxId(data: Buffer): string { + let web3 = new Web3(); + let registerBtcTransactionAbi = BRIDGE_ABI.find(m => m.name == 'registerBtcTransaction'); + if (!registerBtcTransactionAbi) { + throw new Error('registerBtcTransaction can\'t be found in bridge ABI!'); + } + let decodedParameters = web3.eth.abi.decodeParameters( + registerBtcTransactionAbi.inputs, + ensure0x(data.toString('hex').substr(10)) + ); + // Calculate btc tx id + return ensure0x(calculateBtcTxHash(decodedParameters.tx)); + } + + private getPeginStatus(transaction: Transaction): PeginStatus { + let lockBtcLog = this.getLockBtcLogIfExists(transaction.logs); + let status: PeginStatus; + if (lockBtcLog) { + status = new PeginStatus(PEGIN_STATUSES.locked); + status.log = lockBtcLog; + } else if (this.hasRejectedPeginLog(transaction.logs)) { + status = new PeginStatus(PEGIN_STATUSES.rejected); + } else { + status = new PeginStatus(PEGIN_STATUSES.notRegistered); + } + return status; + } + + getLockBtcLogIfExists(logs: Array): Log | null { + return this.getThisLogIfFound(LOCK_BTC_SIGNATURE, logs); + } + + hasRejectedPeginLog(logs: Array): boolean { + return this.getThisLogIfFound(REJECTED_PEGIN_SIGNATURE, logs) != null; + } + + parse(transaction: Transaction): PeginStatusDataModel | null { + if (!transaction || !transaction.logs || !transaction.logs.length) { + // This transaction doesn't have the data required to be parsed + return null; + } + let result = new PeginStatusDataModel(); + result.rskTxId = transaction.hash.toString('hex'); + result.rskBlockHeight = transaction.blockHeight; + result.createdOn = transaction.createdOn; + let peginStatus = this.getPeginStatus(transaction); + result.status = peginStatus.status; + if (result.status == PEGIN_STATUSES.locked) { + // rsk recipient address is always the second topic + result.rskRecipient = ensure0x(peginStatus.log.topics[1].substr(27)); + // TODO: extract the transferred value from the data of the log + // result.value = BigInt(0); + } + result.btcTxId = this.getbtcTxId(transaction.data); + + return result; + } + +} diff --git a/src/services/rsk-bridge-data.provider.ts b/src/services/rsk-bridge-data.provider.ts new file mode 100644 index 00000000..0b0a7bd3 --- /dev/null +++ b/src/services/rsk-bridge-data.provider.ts @@ -0,0 +1,7 @@ +import {BridgeDataFilterModel} from '../models/bridge-data-filter.model'; +import {BridgeData} from '../models/rsk/bridge-data.model'; + +export interface RskBridgeDataProvider { + configure(filters: Array): void; + getData(startingBlock: string | number): Promise; +} diff --git a/src/services/rsk-node.service.ts b/src/services/rsk-node.service.ts new file mode 100644 index 00000000..039bb69a --- /dev/null +++ b/src/services/rsk-node.service.ts @@ -0,0 +1,18 @@ +import Web3 from 'web3'; + +export class RskNodeService { + web3: Web3; + constructor() { + this.web3 = new Web3(`${process.env.RSK_NODE_HOST}`); + } + /** getBlock returns the block with its internal transactions */ + getBlock(block: string | number): Promise { + return this.web3.eth.getBlock(block, true); + } + getTransactionReceipt(txHash: string): Promise { + return this.web3.eth.getTransactionReceipt(txHash); + } + getBlockNumber(): Promise { + return this.web3.eth.getBlockNumber(); + } +} diff --git a/src/services/sync-status-mongo.service.ts b/src/services/sync-status-mongo.service.ts new file mode 100644 index 00000000..f84de635 --- /dev/null +++ b/src/services/sync-status-mongo.service.ts @@ -0,0 +1,36 @@ +import mongoose from 'mongoose'; +import {SyncStatusModel} from '../models/rsk/sync-status.model'; +import {MongoDbDataService} from './mongodb-data.service'; + +/* +- THESE MODEL INTERFACES AND CLASSES ARE REQUIRED FOR MONGO BUT WE DON'T WANT THEM EXPOSED OUT OF THIS LAYER +*/ +interface SyncStatusMongoModel extends mongoose.Document, SyncStatusModel { +} + +const SyncStatusSchema = new mongoose.Schema({ + syncId: {type: Number, required: true, unique: true}, + rskBlockHeight: {type: Number, required: true}, + lastSyncedOn: {type: Date, required: true} +}); + +const SyncStatusConnector = mongoose.model("SyncStatus", SyncStatusSchema); + +export class SyncStatusMongoService extends MongoDbDataService { + protected getConnector(): mongoose.Model { + return SyncStatusConnector; + } + + protected getByIdFilter(id: any) { + return {syncId: id}; + } + + protected getManyFilter(filter?: any) { + return {}; + } + + + protected getLoggerName(): string { + return 'syncStatusMongoService'; + } +} diff --git a/src/services/tx-v2-service.service.ts b/src/services/tx-v2-service.service.ts new file mode 100644 index 00000000..88f592df --- /dev/null +++ b/src/services/tx-v2-service.service.ts @@ -0,0 +1,26 @@ +import {inject, Provider} from '@loopback/core'; +import {getService} from '@loopback/service-proxy'; +import {TxV2ProviderDataSource} from '../datasources'; + +export interface Txv2 { + content: string; +} + +export interface TxV2Service { + // this is where you define the Node.js methods that will be + // mapped to REST/SOAP/gRPC operations as stated in the datasource + // json file. + txV2Provider(txId: string): Promise; +} + +export class TxV2ServiceProvider implements Provider { + constructor( + // txV2Provider must match the name property in the datasource json file + @inject('datasources.txV2Provider') + protected dataSource: TxV2ProviderDataSource = new TxV2ProviderDataSource(), + ) { } + + value(): Promise { + return getService(this.dataSource); + } +} diff --git a/src/tsconfig.json b/src/tsconfig.json index 7d6f333c..5cca5f27 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -7,5 +7,7 @@ "outDir": "dist", "rootDir": "src" }, - "include": ["src"] + "include": [ + "src" + ] } diff --git a/src/utils/btc-utils.ts b/src/utils/btc-utils.ts new file mode 100644 index 00000000..6b5e4e42 --- /dev/null +++ b/src/utils/btc-utils.ts @@ -0,0 +1,82 @@ +import base58 from 'bs58'; +import {sha256} from 'js-sha256'; +import {getLogger, Logger} from 'log4js'; +import {remove0x} from './hex-utils'; + +export const calculateBtcTxHash = (transaction: string) => { + let buffer = Buffer.from(remove0x(transaction), 'hex'); + let hash = sha256(buffer); + buffer = Buffer.from(hash, 'hex'); + hash = sha256(buffer); + let bufferedHash = Buffer.from(hash, 'hex'); + bufferedHash.reverse(); + return bufferedHash.toString('hex'); +}; + +export class BtcAddressUtils { + private logger: Logger; + + constructor() { + this.logger = getLogger('BtcAddressUtils'); + } + + public getRefundAddress(addressRefundInfo: string): string { + let addressRefundData; + let addressRefundType; + let address = ""; + try { + addressRefundType = Number(addressRefundInfo.substring(0, 2)); + addressRefundData = addressRefundInfo.substring(2, 42); + + if (addressRefundType == 1) { //P2PKH_ADDRESS_TYPE + address = this.getAddress(addressRefundData, 'P2PKH'); + } else if (addressRefundType == 2) { //P2SH_ADDRESS_TYPE + address = this.getAddress(addressRefundData, 'P2SH'); + } else { + const errorMessage = `Wrong refund address type. Current type: ${addressRefundType}`; + throw new Error(errorMessage); + } + } + catch (error) { + this.logger.warn("Error parsing refund address", error.message); + } + return address; + } + + private getNetPrefix(netName: string, type: string) { + if (netName == 'mainnet') { + if (type == 'P2PKH') { + return '00'; + } else if (type == 'P2SH') { + return '05'; + } + } else if (netName == 'testnet') { + if (type == 'P2PKH') { + return '6F'; + } else if (type == 'P2SH') { + return 'C4'; + } + } + return ''; + } + + private getAddress(data: string, typeAddress: string): string { //TODO: To test with Ed's data + if (data.length != 40) { + this.logger.warn("Wrong size for script getting BTC refund address"); + return ''; + } + + try { + const network = process.env.NETWORK ?? 'tesnet'; + const prefix = this.getNetPrefix(network, typeAddress); + + data = `${prefix}${data}`; + let checksum = sha256(sha256(data)).slice(0, 8); + return base58.encode(Buffer.from(`${data}${checksum}`, 'hex')) + } + catch (error) { + this.logger.warn("Error getting BTC refund address"); + } + return ''; + } +} diff --git a/src/utils/error-messages.ts b/src/utils/error-messages.ts new file mode 100644 index 00000000..fa3c46c0 --- /dev/null +++ b/src/utils/error-messages.ts @@ -0,0 +1,7 @@ +export enum errorCodes { + NOT_A_PEGIN_LESS_THAN_MINIMUN_ERROR, + NOT_A_PEGIN_FED_ADDRESS_ERROR, + OUTPUT_CONTENT_ERROR, + REFUND_WRONG_ADDRESS_ERROR, + GENERAL_ERROR +} diff --git a/src/utils/hex-utils.ts b/src/utils/hex-utils.ts new file mode 100644 index 00000000..05214e2f --- /dev/null +++ b/src/utils/hex-utils.ts @@ -0,0 +1,7 @@ +export const ensure0x = (value: string) => { + return value.startsWith('0x') ? value : '0x' + value; +}; + +export const remove0x = (value: string) => { + return !value.startsWith('0x') ? value : value.substring(2); +} diff --git a/src/utils/metric-logger.ts b/src/utils/metric-logger.ts new file mode 100644 index 00000000..d72d9e9f --- /dev/null +++ b/src/utils/metric-logger.ts @@ -0,0 +1,13 @@ +import {Logger} from 'log4js'; + +const getTime = () => (new Date()).getTime(); + +export const getMetricLogger = (logger: Logger, methodName: any) => { + let start = getTime(); + return () => { + if (process.env.METRICS_ENABLED && + process.env.METRICS_ENABLED.toLowerCase() === 'true') { + logger.trace(`${methodName} took ${getTime() - start}ms`); + } + }; +}; diff --git a/src/utils/rsk-address-utils.ts b/src/utils/rsk-address-utils.ts new file mode 100644 index 00000000..e3e09d2b --- /dev/null +++ b/src/utils/rsk-address-utils.ts @@ -0,0 +1,11 @@ + +export class RskAddressUtils { + + constructor() { + } + + public getRskAddressFromOpReturn(data: string): string { + return Buffer.from(`${data}`, 'hex').toString('hex'); + } + +}