diff --git a/.eslintrc.json b/.eslintrc.json index aa47d6b8..d0095aeb 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -13,6 +13,7 @@ "ecmaVersion": 12 }, "rules": { + "no-labels": "off", "simple-import-sort/imports": "error", "no-underscore-dangle": "off", "camelcase":"off", diff --git a/__tests__/integration/impact-value-transfer.spec.js b/__tests__/integration/impact-value-transfer.spec.js new file mode 100644 index 00000000..1a3537ee --- /dev/null +++ b/__tests__/integration/impact-value-transfer.spec.js @@ -0,0 +1,215 @@ +const request = require('supertest'); +const Zaven = require("../mock-data/Zaven.json"); +const Meisze = require("../mock-data/Meisze.json"); +const TokenA = require("../mock-data/TokenA"); +const testUtils = require("./testUtils"); +const { expect } = require('chai'); +const log = require('loglevel'); +const server = require("../../server/app"); + +describe("Impact Value", () => { + let registeredZaven; + let registeredMeisze; + + beforeEach(async () => { + await testUtils.clear(); + registeredZaven = await testUtils.registerAndLogin(Zaven); + await testUtils.addToken(registeredZaven, TokenA); + registeredMeisze = await testUtils.registerAndLogin(Meisze); + }) + + describe("Zaven request to send 4 impact value to Meisze", () => { + let transferId; + + + beforeEach(async () => { + await request(server) + .post(`/transfers`) + .set('Content-Type', "application/json") + .set('treetracker-api-key', registeredZaven.apiKey) + .set('Authorization', `Bearer ${registeredZaven.token}`) + .send({ + sender_wallet: registeredZaven.name, + receiver_wallet: registeredMeisze.name, + impact: { + value: 4, + accept_deviation: 2, + } + }) + .expect(202) + .then(res => { + expect(res).property("body").property("id").a("string"); + transferId = res.body.id; + }); + }); + + it("Meisze accept the transfer", async () => { + await request(server) + .post(`/transfers/${transferId}/accept`) + .set('Content-Type', "application/json") + .set('treetracker-api-key', registeredMeisze.apiKey) + .set('Authorization', `Bearer ${registeredMeisze.token}`) + .expect(200) + .then(res => { + expect(res).property("body").property("impact_value_transferred").eq(4); + }); + + // Meisze should have one token + const token = await testUtils.getTokenById(TokenA.id); + expect(token).property("id").a("string"); + expect(token).property("wallet_id").eq(registeredMeisze.id); + expect(token).property("claim").eq(false); + + // the transfer's claim is false (by default) + const transfer = await testUtils.getKnex().table("transfer").first().where("id", transferId); + expect(transfer).property("claim").eq(false); + + // the transaction's claim is false (by default) + const transactions = await testUtils.getKnex().table("transaction").where("transfer_id", transferId); + transactions.forEach(t => expect(t).property("claim").eq(false)); + }); + + it("Meisze decline the transfer", async () => { + await request(server) + .post(`/transfers/${transferId}/decline`) + .set('Content-Type', "application/json") + .set('treetracker-api-key', registeredMeisze.apiKey) + .set('Authorization', `Bearer ${registeredMeisze.token}`) + .expect(200); + + // Zeven still have the token + const token = await testUtils.getTokenById(TokenA.id); + expect(token).property("id").a("string"); + expect(token).property("wallet_id").eq(registeredZaven.id); + + }); + + }); + + describe("Meisze request to receive 4 impact value from Zaven", () => { + let transferId; + + + beforeEach(async () => { + await request(server) + .post(`/transfers`) + .set('Content-Type', "application/json") + .set('treetracker-api-key', registeredMeisze.apiKey) + .set('Authorization', `Bearer ${registeredMeisze.token}`) + .send({ + sender_wallet: registeredZaven.name, + receiver_wallet: registeredMeisze.name, + impact: { + value: 4, + accept_deviation: 2, + } + }) + .expect(202) + .then(res => { + expect(res).property("body").property("id").a("string"); + transferId = res.body.id; + }); + }); + + it("Zaven fulfill the transfer", async () => { + await request(server) + .post(`/transfers/${transferId}/fulfill`) + .set('Content-Type', "application/json") + .set('treetracker-api-key', registeredZaven.apiKey) + .set('Authorization', `Bearer ${registeredZaven.token}`) + .send({ + implicit: true, + }) + .expect(200) + .then(res => { + expect(res).property("body").property("impact_value_transferred").eq(4); + }); + + // Meisze should have one token + const token = await testUtils.getTokenById(TokenA.id); + expect(token).property("id").a("string"); + expect(token).property("wallet_id").eq(registeredMeisze.id); + expect(token).property("claim").eq(false); + + // the transfer's claim is false (by default) + const transfer = await testUtils.getKnex().table("transfer").first().where("id", transferId); + expect(transfer).property("claim").eq(false); + + // the transaction's claim is false (by default) + const transactions = await testUtils.getKnex().table("transaction").where("transfer_id", transferId); + transactions.forEach(t => expect(t).property("claim").eq(false)); + }); + + it("Zaven fulfill the transfer with explicit tokens, should throw error", async () => { + await request(server) + .post(`/transfers/${transferId}/fulfill`) + .set('Content-Type', "application/json") + .set('treetracker-api-key', registeredZaven.apiKey) + .set('Authorization', `Bearer ${registeredZaven.token}`) + .send({ + tokens: [TokenA.id], + }) + .expect(403) + .then(res => { + expect(res.body).property("message").match(/implicit/); + }); + + }); + + it("Zaven decline the transfer", async () => { + await request(server) + .post(`/transfers/${transferId}/decline`) + .set('Content-Type', "application/json") + .set('treetracker-api-key', registeredZaven.apiKey) + .set('Authorization', `Bearer ${registeredZaven.token}`) + .expect(200); + + // Zeven still have the token + const token = await testUtils.getTokenById(TokenA.id); + expect(token).property("id").a("string"); + expect(token).property("wallet_id").eq(registeredZaven.id); + + }); + + }); + + describe("Zaven request to send 4 impact value to Meisze, with the trust relationship between Z and M", () => { + let transferId; + + beforeEach(async () => { + log.warn("Add trust Z to M"); + testUtils.trustASendToB(registeredZaven, registeredMeisze); + }); + + beforeEach(async () => { + await request(server) + .post(`/transfers`) + .set('Content-Type', "application/json") + .set('treetracker-api-key', registeredMeisze.apiKey) + .set('Authorization', `Bearer ${registeredZaven.token}`) + .send({ + sender_wallet: registeredZaven.name, + receiver_wallet: registeredMeisze.name, + impact: { + value: 4, + accept_deviation: 2, + } + }) + .expect(201) + .then(res => { + expect(res).property("body").property("id").a("string"); + expect(res).property("body").property("state").eq("completed"); + transferId = res.body.id; + expect(res).property("body").property("impact_value_transferred").eq(4); + }); + }); + + it("Meisze already has the token", async () => { + // Meisze should have one token + const token = await testUtils.getTokenById(TokenA.id); + expect(token).property("id").a("string"); + expect(token).property("wallet_id").eq(registeredMeisze.id); + }); + }); + +}); diff --git a/__tests__/integration/testUtils.js b/__tests__/integration/testUtils.js index 6255fae4..930ffb50 100644 --- a/__tests__/integration/testUtils.js +++ b/__tests__/integration/testUtils.js @@ -5,6 +5,7 @@ const generator = require('generate-password'); const { expect } = require('chai'); const JWTService = require("../../server/services/JWTService"); const Transfer = require("../../server/models/Transfer"); +const TrustRelationship = require("../../server/models/TrustRelationship"); const knex = require("../../server/database/knex"); /* @@ -79,6 +80,7 @@ async function clear() { * Add a token to a wallet */ async function addToken(wallet, token){ + expect(token).property("value").a("number"); const result = await knex("token") .insert({ ...token, @@ -114,10 +116,34 @@ async function sendAndPend( return result[0]; } +async function getTokenById(id){ + const tokens = await knex("token").where("id", id); + return tokens[0]; +} + +async function trustASendToB(walletA, walletB){ + const result = await knex("wallet_trust") + .insert({ + id: uuid.v4(), + actor_wallet_id: walletA.id, + target_wallet_id: walletB.id, + originator_wallet_id: walletA.id, + type: TrustRelationship.ENTITY_TRUST_TYPE.send, + request_type: TrustRelationship.ENTITY_TRUST_REQUEST_TYPE.send, + state: TrustRelationship.ENTITY_TRUST_STATE_TYPE.trusted, + active: true, + }).returning("*"); + expect(result[0]).property("id").a("string"); + return result[0]; +} + module.exports = { register, registerAndLogin, clear, sendAndPend, addToken, + getTokenById, + trustASendToB, + getKnex: () => knex, } diff --git a/__tests__/mock-data/TokenA.json b/__tests__/mock-data/TokenA.json index 31565599..68585924 100644 --- a/__tests__/mock-data/TokenA.json +++ b/__tests__/mock-data/TokenA.json @@ -1,4 +1,5 @@ { "id": "71a20380-d561-43f9-bbb4-54d7a9f2ecbe", - "capture_id": "8f7e7d4a-f5c3-409d-8c5b-880edf73c758" + "capture_id": "8f7e7d4a-f5c3-409d-8c5b-880edf73c758", + "value": 4 } diff --git a/database/migrations/20210401005831-AddClaimBoolean.js b/database/migrations/20210401005831-AddClaimBoolean.js index b764d68f..2d775ce7 100644 --- a/database/migrations/20210401005831-AddClaimBoolean.js +++ b/database/migrations/20210401005831-AddClaimBoolean.js @@ -1,4 +1,3 @@ -// 'use strict'; let dbm; let type; diff --git a/database/migrations/20210602215334-AddImpactValueInToken.js b/database/migrations/20210602215334-AddImpactValueInToken.js new file mode 100644 index 00000000..58a32726 --- /dev/null +++ b/database/migrations/20210602215334-AddImpactValueInToken.js @@ -0,0 +1,27 @@ +// 'use strict'; + +let dbm; +let type; +let seed; + +/** + * We receive the dbmigrate dependency from dbmigrate initially. + * This enables us to not have to rely on NODE_PATH. + */ +exports.setup = function(options, seedLink) { + dbm = options.dbmigrate; + type = dbm.dataType; + seed = seedLink; +}; + +exports.up = function(db) { + return db.addColumn('token', 'value', { type: 'int', notNull: true, defaultValue: 0 }) +}; + +exports.down = function(db) { + return db.removeColumn('token', 'value'); +}; + +exports._meta = { + "version": 1 +}; diff --git a/docs/api/spec/treetracker-token-api.yaml b/docs/api/spec/treetracker-token-api.yaml index 3f919fbd..c46131db 100644 --- a/docs/api/spec/treetracker-token-api.yaml +++ b/docs/api/spec/treetracker-token-api.yaml @@ -208,6 +208,13 @@ paths: - pine - Tanzania claim: false + ImpactValue: + value: + impact: + value: 100 + accept_deviation: true + sender_wallet: zaven4 + receiver_wallet: zaven required: true responses: '201': @@ -216,6 +223,22 @@ paths: application/json: schema: $ref: '#/components/schemas/sendRequestProcessedResponse' + examples: + example-1: + value: + originating_wallet: johnwallet1 + source_wallet: planeter + destination_wallet: just.a.guy + type: send + parameters: + $ref: '#/components/schemas/requestBundleRequestParameters' + state: completed + created_at: '2020-07-09T00:41:49+00:00' + closed_at: '2020-07-09T00:41:49+00:00' + application/xml: + schema: + type: object + properties: {} '202': description: 'The transfer could not be completely processed because a trust relationship does not exist allowing for automated processing. If a the source wallet is controlled by the authenitcated user, a transfer in the pending state has been created. If the source wallet is not controlled but the authenticated user, but the destination wallet is, then a transfer in the requested state has been created' content: @@ -379,6 +402,16 @@ paths: responses: '200': description: '' + content: + application/json: + schema: + type: object + properties: + impace_value_transfered: + type: integer + description: 'The number of impact valued that has been actually transfered, this field is just valid when the transfer request type is setting impact value as the parameter' + '': + type: string '401': $ref: '#/components/responses/UnauthorizedError' '/transfers/{transfer_id}/decline': @@ -426,6 +459,14 @@ paths: responses: '200': description: '' + content: + application/json: + schema: + type: object + properties: + impact_value_transfered: + type: integer + description: 'The number of impact valued that has been actually transfered, this field is just valid when the transfer request type is setting impact value as the parameter' '401': $ref: '#/components/responses/UnauthorizedError' '/transfers/{transfer_id}/tokens': @@ -468,6 +509,7 @@ paths: description: '' '401': $ref: '#/components/responses/UnauthorizedError' + description: '' /events: get: tags: @@ -772,6 +814,12 @@ components: type: string capture: type: string + value: + type: integer + description: impact value + value_matrix_id: + type: integer + description: The id of the impact value matrix accountrequest: title: accountrequest required: @@ -802,17 +850,13 @@ components: type: integer transferrequest: title: transferrequest - required: - - tokens - - sender_wallet - - receiver_wallet type: object properties: tokens: type: array + description: '' items: type: string - description: '' bundle: type: object properties: @@ -832,12 +876,23 @@ components: oneOf: - type: string - type: number - example: - tokens: - - e1b278e8-f025-44b7-9dd8-36ffb2d58f7e - - e533653e-2dbf-48cd-940a-a87e5a393158 - sender_wallet: zaven4 - receiver_wallet: zaven + impact: + type: object + description: Use impact value to express how many tokens should be tranfered + properties: + value: + type: integer + description: 'The totla amount of the impact value ' + minimum: 0 + maximum: 10000 + accept_deviation: + type: boolean + description: 'If this transfer accept deviation when transfer happens, it means, when user choose to accept it, there might be some deviation between the actual transfered impact value and the value given here, the range of the deviation will be defined in the impact value matrix.' + default: true + required: + - tokens + - sender_wallet + - receiver_wallet transferbundlerequest: title: transferbundlerequest required: @@ -884,6 +939,16 @@ components: sendRequestProcessedResponse: title: sendRequestProcessedResponse type: object + example: + originating_wallet: johnwallet1 + source_wallet: planeter + destination_wallet: just.a.guy + type: send + parameters: + $ref: '#/components/schemas/requestBundleRequestParameters' + state: completed + created_at: '2020-07-09T00:41:49+00:00' + closed_at: '2020-07-09T00:41:49+00:00' properties: id: type: string @@ -906,16 +971,24 @@ components: type: string closed_at: type: string - example: - originating_wallet: johnwallet1 - source_wallet: planeter - destination_wallet: just.a.guy - type: send - parameters: - $ref: '#/components/schemas/requestBundleRequestParameters' - state: completed - created_at: '2020-07-09T00:41:49+00:00' - closed_at: '2020-07-09T00:41:49+00:00' + impact_value_transfered: + type: integer + description: 'The number of impact valued that has been actually transfered, this field is just valid when the transfer request type is setting impact value as the parameter' + '': + type: string + x-examples: + example-1: + value: + originating_wallet: johnwallet1 + source_wallet: planeter + destination_wallet: just.a.guy + type: send + parameters: + $ref: '#/components/schemas/requestBundleRequestParameters' + state: completed + created_at: '2020-07-09T00:41:49+00:00' + closed_at: '2020-07-09T00:41:49+00:00' + description: '' sendRequestPendingResponse: title: sendRequestPendingResponse sendBundleRequest: @@ -969,17 +1042,19 @@ components: properties: tokens: type: array + description: optional array of explicit tokens items: type: string - description: optional array of explicit tokens bundle_size: type: integer description: required number of trees to transfer matching_all_tags: type: array + description: optional list of tags that trees must match ALL of items: type: string - description: optional list of tags that trees must match ALL of + impact: + type: object required: - bundle_size requestBundleFulfillBody: @@ -990,7 +1065,7 @@ components: type: array items: type: string - description: optional array of explicit tokens + description: optional array of explicit tokens, NOTE, if the transfer type is impact value, then, can not set explicit 'token' here, must set this fulfillment request to 'implicit' implicit: type: boolean description: automatically fill the request with tokens matching the specified parameters if set diff --git a/server/models/Transfer.js b/server/models/Transfer.js index 24ea5d33..be2a003a 100644 --- a/server/models/Transfer.js +++ b/server/models/Transfer.js @@ -1,3 +1,4 @@ +const _ = require("lodash"); class Transfer{ } @@ -16,4 +17,15 @@ Transfer.STATE = { failed: "failed", } +Transfer.hasCompleted = function(transferJson){ + return transferJson.state === Transfer.STATE.completed; +} + +Transfer.isImpactValue = function(transferJson){ + return ( + _.has(transferJson, "parameters.impact.value") && + _.has(transferJson, "parameters.impact.accept_deviation") + ); +} + module.exports = Transfer; diff --git a/server/models/Wallet.js b/server/models/Wallet.js index 96ac5d52..64bcee83 100644 --- a/server/models/Wallet.js +++ b/server/models/Wallet.js @@ -10,6 +10,7 @@ const TransferRepository = require("../repositories/TransferRepository"); const HttpError = require("../utils/HttpError"); const Transfer = require("./Transfer"); const Token = require("./Token"); +const _ = require("lodash"); class Wallet{ @@ -452,7 +453,7 @@ class Wallet{ /* * Transfer some tokens from the sender to receiver */ - async transfer(sender, receiver, tokens, claimBoolean){ + async transfer(sender, receiver, tokens, claimBoolean = false){ // await this.checkDeduct(sender, receiver); // check tokens belong to sender for(const token of tokens){ @@ -648,6 +649,78 @@ class Wallet{ } + async transferImpact(sender, receiver, value, accept_deviation){ + // check if the impact value can be acceptted + const tokens = await this.tokenService.makeImpactPackage(sender, value, accept_deviation); + + const isDeduct = await this.isDeduct(sender,receiver); + // If has the trust, and is not deduct request (now, if wallet request some token from another wallet, can not pass the transfer directly) + const hasTrust = await this.hasTrust(TrustRelationship.ENTITY_TRUST_REQUEST_TYPE.send, sender, receiver); + const hasControlOverSender = await this.hasControlOver(sender); + const hasControlOverReceiver = await this.hasControlOver(receiver); + if( + (hasControlOverSender && hasControlOverReceiver) || + (!isDeduct && hasTrust) + ){ + const transfer = await this.transferRepository.create({ + originator_wallet_id: this._id, + source_wallet_id: sender.getId(), + destination_wallet_id: receiver.getId(), + state: Transfer.STATE.completed, + parameters: { + impact: { + value, + accept_deviation, + } + }, + // TODO remove hard code + claim: false, + }); + log.debug("now, deal with tokens"); + // need to check if tokens are not claim + await this.tokenService.completeTransfer(tokens, transfer); + + return transfer; + } + if(hasControlOverSender){ + log.debug("OK, no permission, source under control, now pending it"); + const transfer = await this.transferRepository.create({ + originator_wallet_id: this._id, + source_wallet_id: sender.getId(), + destination_wallet_id: receiver.getId(), + state: Transfer.STATE.pending, + parameters: { + impact: { + value, + accept_deviation, + } + }, + // TODO remove hard code + claim: false, + }); + return transfer; + }if(hasControlOverReceiver){ + log.debug("OK, no permission, receiver under control, now request it"); + const transfer = await this.transferRepository.create({ + originator_wallet_id: this._id, + source_wallet_id: sender.getId(), + destination_wallet_id: receiver.getId(), + state: Transfer.STATE.requested, + parameters: { + impact: { + value, + accept_deviation, + } + }, + // TODO remove hard code + claim: false, + }); + return transfer; + } + // TODO + expect.fail(); + } + /* * I have control over given wallet */ @@ -732,6 +805,17 @@ class Wallet{ throw new HttpError(403, "Do not have enough tokens"); } await this.tokenService.completeTransfer(tokens, transfer); + }else if( + transfer.parameters && + transfer.parameters.impact && + transfer.parameters.impact.value && + transfer.parameters.impact.accept_deviation + ){ + log.debug("transfer impact of tokens"); + const {source_wallet_id} = transfer; + const senderWallet = new Wallet(source_wallet_id, this._session); + const tokens = await this.tokenService.makeImpactPackage(senderWallet, transfer.parameters.impact.value, transfer.parameters.impact.accept_deviation); + await this.tokenService.completeTransfer(tokens, transfer); }else{ log.debug("transfer tokens"); const tokens = await this.tokenService.getTokensByPendingTransferId(transferId); @@ -829,6 +913,19 @@ class Wallet{ const senderWallet = new Wallet(source_wallet_id, this._session); const tokens = await this.tokenService.getTokensByBundle(senderWallet, transfer.parameters.bundle.bundleSize); await this.tokenService.completeTransfer(tokens, transfer); + }else if( + _.get(transfer, "parameters.impact.value") && + _.get(transfer, "parameters.impact.accept_deviation") + ){ + log.debug("transfer impact of tokens"); + const {source_wallet_id} = transfer; + const senderWallet = new Wallet(source_wallet_id, this._session); + const tokens = await this.tokenService.makeImpactPackage( + senderWallet, + transfer.parameters.impact.value, + transfer.parameters.impact.accept_deviation + ); + await this.tokenService.completeTransfer(tokens, transfer); }else{ log.debug("transfer tokens"); const tokens = await this.tokenService.getTokensByPendingTransferId(transfer.id); @@ -858,10 +955,12 @@ class Wallet{ // deal with tokens if( - // TODO optimize - transfer.parameters && - transfer.parameters.bundle && - transfer.parameters.bundle.bundleSize){ + _.get(transfer, "parameters.impact.value") + ){ + throw new HttpError(403, "Impact value type of transfer must set 'implicit' to fulfill the transfer", true); + }else if( + _.get(transfer, "parameters.bundle.bundleSize") + ){ log.debug("transfer bundle of tokens"); const {source_wallet_id} = transfer; const senderWallet = new Wallet(source_wallet_id, this._session); diff --git a/server/repositories/TransferRepository.js b/server/repositories/TransferRepository.js index a4a89f30..66411955 100644 --- a/server/repositories/TransferRepository.js +++ b/server/repositories/TransferRepository.js @@ -3,6 +3,7 @@ const knex = require("../database/knex"); const Transfer = require("../models/Transfer"); const BaseRepository = require("./BaseRepository"); const Session = require("../models/Session"); +const log = require("loglevel"); class TransferRepository extends BaseRepository{ @@ -28,6 +29,24 @@ class TransferRepository extends BaseRepository{ state: Transfer.STATE.pending, }); } + + /* + * To calculate the sum of impact value of a transfer + */ + async getImpactValue(transferId){ + const result = await this._session.getDB().raw( + ` + SELECT sum(t.value) as impact_value_transferred FROM "token" t + LEFT JOIN "transaction" tr + ON t.id = tr.token_id + WHERE tr.transfer_id = ? + `, + [transferId] + ); + log.debug("sum impact value:", result); + return parseInt(result.rows[0].impact_value_transferred); + } + } module.exports = TransferRepository; diff --git a/server/routes/transferRouter.js b/server/routes/transferRouter.js index 365eacfe..dfc332f0 100644 --- a/server/routes/transferRouter.js +++ b/server/routes/transferRouter.js @@ -36,15 +36,31 @@ transferRouter.post( // TODO: add boolean for claim, but default to false. claim: Joi.boolean(), }), - otherwise: Joi.object({ - bundle: Joi.object({ - bundle_size: Joi.number().min(1).max(10000).integer(), - }).required(), - sender_wallet: Joi.string() - .required(), - receiver_wallet: Joi.string() - .required(), - claim: Joi.boolean().required(), + otherwise: Joi.alternatives() + // if there is tokens field + .conditional(Joi.object({ + impact: Joi.any().required(), + }).unknown(),{ + then: Joi.object({ + impact: Joi.object({ + value: Joi.number().min(1).max(10000).integer(), + accept_deviation: Joi.number().min(1).max(10).integer(), + }).required(), + sender_wallet: Joi.string() + .required(), + receiver_wallet: Joi.string() + .required(), + }), + otherwise: Joi.object({ + bundle: Joi.object({ + bundle_size: Joi.number().min(1).max(10000).integer(), + }).required(), + sender_wallet: Joi.string() + .required(), + receiver_wallet: Joi.string() + .required(), + claim: Joi.boolean().required(), + }) }), }) ); @@ -71,11 +87,16 @@ transferRouter.post( } // Case 1: with trust, token transfer result = await walletLogin.transfer(walletSender, walletReceiver, tokens, claim); + }else if(req.body.impact){ + // impact case + result = await walletLogin.transferImpact(walletSender, walletReceiver, req.body.impact.value, req.body.impact.accept_deviation); + }else{ // Case 2: with trust, bundle transfer // TODO: get only transferrable tokens result = await walletLogin.transferBundle(walletSender, walletReceiver, req.body.bundle.bundle_size, claim); } + await session.commitTransaction(); const transferService = new TransferService(session); result = await transferService.convertToResponse(result); if (result.state === Transfer.STATE.completed) { @@ -88,7 +109,6 @@ transferRouter.post( } else { throw new Error(`Unexpected state ${result.state}`); } - await session.commitTransaction(); }catch(e){ if(e instanceof HttpError && !e.shouldRollback()){ // if the error type is HttpError, means the exception has been handled @@ -127,8 +147,8 @@ transferRouter.post( const transferJson2 = await transferService.convertToResponse( transferJson, ); - res.status(200).json(transferJson2); await session.commitTransaction(); + res.status(200).json(transferJson2); } catch (e) { if (e instanceof HttpError && !e.shouldRollback()) { // if the error type is HttpError, means the exception has been handled @@ -167,8 +187,8 @@ transferRouter.post( const transferJson2 = await transferService.convertToResponse( transferJson, ); - res.status(200).json(transferJson2); await session.commitTransaction(); + res.status(200).json(transferJson2); } catch (e) { if (e instanceof HttpError && !e.shouldRollback()) { // if the error type is HttpError, means the exception has been handled @@ -207,8 +227,8 @@ transferRouter.delete( const transferJson2 = await transferService.convertToResponse( transferJson, ); - res.status(200).json(transferJson2); await session.commitTransaction(); + res.status(200).json(transferJson2); } catch (e) { if (e instanceof HttpError && !e.shouldRollback()) { // if the error type is HttpError, means the exception has been handled @@ -234,24 +254,24 @@ transferRouter.post( transfer_id: Joi.string().guid().required(), }), ); - Joi.assert( - req.body, - Joi.alternatives() - // if there is tokens field - .conditional( - Joi.object({ - tokens: Joi.any().required(), - }).unknown(), - { - then: Joi.object({ - tokens: Joi.array().items(Joi.string()).required().unique(), - }), - otherwise: Joi.object({ - implicit: Joi.boolean().truthy().required(), - }), - }, - ), - ); + // simplify the way to using joi + if(req.body.tokens){ + // explicit + Joi.assert( + req.body, + Joi.object({ + tokens: Joi.array().items(Joi.string()).required().unique(), + }) + ); + }else{ + // implicit + Joi.assert( + req.body, + Joi.object({ + implicit: Joi.boolean().truthy().required(), + }) + ); + } const session = new Session(); // begin transaction try { @@ -280,8 +300,8 @@ transferRouter.post( const transferJson2 = await transferService.convertToResponse( transferJson, ); - res.status(200).json(transferJson2); await session.commitTransaction(); + res.status(200).json(transferJson2); } catch (e) { if (e instanceof HttpError && !e.shouldRollback()) { // if the error type is HttpError, means the exception has been handled diff --git a/server/services/TokenService.js b/server/services/TokenService.js index 049429d1..a96232af 100644 --- a/server/services/TokenService.js +++ b/server/services/TokenService.js @@ -3,6 +3,8 @@ const Joi = require("joi"); const Token = require("../models/Token"); const TokenRepository = require("../repositories/TokenRepository"); const TransactionRepository = require("../repositories/TransactionRepository"); +const HttpError = require("../utils/HttpError"); +const expect = require("expect-runtime"); class TokenService{ @@ -115,7 +117,7 @@ class TokenService{ /* * To replace token.completeTransfer, as a bulk operaction */ - async completeTransfer(tokens, transfer, claimBoolean){ + async completeTransfer(tokens, transfer, claimBoolean = false){ log.debug("Token complete transfer batch"); await this.tokenRepository.updateByIds({ transfer_pending: false, @@ -164,6 +166,63 @@ class TokenService{ ); } + /* + * To get a token package whose value equal to impactValue, deviation is + * under acceptDeviation + * TODO a better algorithm to make the package, currently, just use a simple + * way to try to calculate the impact value sum. + */ + async makeImpactPackage(wallet, impactValue, acceptDeviation){ +// expect(impactValue).a("number"); + log.debug("make package for impact, value:%d, deviation:%d", impactValue, acceptDeviation); + const limit = 200; + const maximumRecord = 100000; + let offset = 0; + const tokenPackage = []; + let total = 0; + loop: while(true){ + const tokens = await this.tokenRepository.getByFilter({ + wallet_id: wallet.getId(), + claim: false, + },{ + limit, + offset, + }); + if(tokens.length === 0){ + throw new HttpError(403, "Do not have enough tokens for this amount of impact value"); + } + for(const token of tokens){ + log.debug("total:%d", total); + log.warn("token:", token); + // check the token value + Joi.assert(token.value, Joi.number().min(0).required().error(new HttpError(403, `Token has bad value field, token id:${token.id}, value: ${token.value}`))); + if(token.value + total <= impactValue + acceptDeviation){ + tokenPackage.push(token); + total += token.value; + } + if(Math.abs(impactValue - total) <= acceptDeviation){ + log.debug("Meet the condition, total:%d, impact value:%d, deviation:%d, tokens:%d", + total, + impactValue, + acceptDeviation, + tokenPackage.length, + ); + break loop; + } + } + offset += tokens.length; + if(offset > maximumRecord){ + log.warn("Reach to the maximum record to make the package!"); + throw new HttpError(403, "Can not get token package for this amount of impact value"); + } + } + + // build model + const tokenPackage2 = tokenPackage.map(token => new Token(token, this._session)); + return tokenPackage2; + + } + } module.exports = TokenService; diff --git a/server/services/TokenService.spec.js b/server/services/TokenService.spec.js index 3d8b4605..352bb6b8 100644 --- a/server/services/TokenService.spec.js +++ b/server/services/TokenService.spec.js @@ -149,4 +149,50 @@ describe("Token", () => { expect(updateByIds).calledWith(sinon.match({transfer_pending: false, transfer_pending_id:null}), [tokenId1]); }); + describe("makeImpactPackage", () => { + + it("Successfully get the package", async () => { + const walletId1 = uuid.v4(); + const tokenId1 = uuid.v4(); + const wallet = new Wallet(walletId1, session); + sinon.stub(TokenRepository.prototype, "getByFilter").resolves([{ + id: tokenId1, + value: 4, + }]); + const tokens = await tokenService.makeImpactPackage(wallet, 4, 4); + expect(tokens).to.have.lengthOf(1); + }); + + // TODO + // it("Don't have enough token to make the package") + + describe("The money change case: try to find the exact amount of impact", () => { + it("impact: 6, deviation: 0, the token sequence: 4, 4, 2, 4, ...", async () => { + const walletId1 = uuid.v4(); + const tokenId1 = uuid.v4(); + const wallet = new Wallet(walletId1, session); + const getByFilter = sinon.stub(TokenRepository.prototype, "getByFilter") + getByFilter.onCall(0).resolves([{ + id: tokenId1, + value: 4, + }]); + getByFilter.onCall(1).resolves([{ + id: tokenId1, + value: 4, + }]); + getByFilter.onCall(2).resolves([{ + id: tokenId1, + value: 2, + }]); + getByFilter.resolves([{ + id: tokenId1, + value: 4, + }]); + const tokens = await tokenService.makeImpactPackage(wallet, 6, 0); + expect(tokens).to.have.lengthOf(2);// 4,2 + }); + }); + + }); + }); diff --git a/server/services/TransferService.js b/server/services/TransferService.js index 1097f923..56ed8cc0 100644 --- a/server/services/TransferService.js +++ b/server/services/TransferService.js @@ -1,9 +1,13 @@ const WalletService = require('./WalletService'); +const _ = require("lodash"); +const Transfer = require("../models/Transfer"); +const TransferRepository = require("../repositories/TransferRepository"); class TransferService { constructor(session) { this._session = session; this.walletService = new WalletService(session); + this.transferRepository = new TransferRepository(session); } async convertToResponse(transferObject) { @@ -31,6 +35,15 @@ class TransferService { result.destination_wallet = await json.name; delete result.destination_wallet_id; } + + // deal with the impact value + if( + Transfer.isImpactValue(result) && + Transfer.hasCompleted(result) + ){ + const impactValue = await this.transferRepository.getImpactValue(result.id); + result.impact_value_transferred = impactValue; + } return result; } }