Skip to content

Commit

Permalink
Merge pull request #48 from mojaloop/feature/three-stage-transfers
Browse files Browse the repository at this point in the history
Feature/three stage transfers
  • Loading branch information
bushjames committed Sep 12, 2019
2 parents 799061e + 8f31ade commit 56ee09c
Show file tree
Hide file tree
Showing 7 changed files with 608 additions and 35 deletions.
55 changes: 55 additions & 0 deletions src/__mocks__/@mojaloop/sdk-standard-components.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**************************************************************************
* (C) Copyright ModusBox Inc. 2019 - All rights reserved. *
* *
* This file is made available under the terms of the license agreement *
* specified in the corresponding source code repository. *
* *
* ORIGINAL AUTHOR: *
* James Bush - james.bush@modusbox.com *
**************************************************************************/

'use strict';

const util = require('util');
const EventEmitter = require('events');


class MockMojaloopRequests extends EventEmitter {
constructor(config) {
super();
console.log('MockMojaloopRequests constructed');
this.config = config;
}

getParties(...args) {
console.log(`MockMojaloopRequests.getParties called with args: ${util.inspect(args)}`);
setImmediate(() => { this.emit('getParties'); });
return Promise.resolve(null);
}

postQuotes(...args) {
console.log(`MockMojaloopRequests.postQuotes called with args: ${util.inspect(args)}`);
setImmediate(() => { this.emit('postQuotes'); });
return Promise.resolve(null);
}

postTransfers(...args) {
console.log(`MockMojaloopRequests.postTransfers called with args: ${util.inspect(args)}`);
setImmediate(() => { this.emit('postTransfers'); });
return Promise.resolve(null);
}
}


class MockIlp {
constructor(config) {
console.log('MockIlp constructed');
this.config = config;
}
}


module.exports = {
MojaloopRequests: MockMojaloopRequests,
Ilp: MockIlp
};
71 changes: 71 additions & 0 deletions src/__mocks__/cache.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**************************************************************************
* (C) Copyright ModusBox Inc. 2019 - All rights reserved. *
* *
* This file is made available under the terms of the license agreement *
* specified in the corresponding source code repository. *
* *
* ORIGINAL AUTHOR: *
* James Bush - james.bush@modusbox.com *
**************************************************************************/

'use strict';

const EventEmitter = require('events');

class MockClient extends EventEmitter {
constructor() {
super();
console.log('MockClient constructed');
}

subscribe(key) {
console.log(`MockClient got subscription for key: ${key}`);
}

unsubscribe(key, callback) {
console.log(`MockClient got unsubscribe for key: ${key}`);

if(callback) {
return callback();
}
}

emitMessage(data) {
process.nextTick(() => {
console.log(`MockClient emitting event data: ${data}`);
this.emit('message', 'channel', data);
});
}
}


class MockCache {
constructor() {
console.log('MockCache constructed');

this.data = {};
}

async set(key, value) {
this.data[key] = value;
return Promise.resolve('OK');
}

async get(key) {
return Promise.resolve(this.data[key]);
}

emitMessage(data) {
this.client.emitMessage(data);
}

async getClient() {
if(!this.client) {
this.client = new MockClient();
}
return Promise.resolve(this.client);
}
}


module.exports = MockCache;
3 changes: 3 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ let DEFAULTS = {
checkIlp: true,
expirySeconds: 60,
autoAcceptQuotes: true,
autoAcceptParty: true,
tls: {
mutualTLS: {enabled: false},
inboundCreds: {
Expand Down Expand Up @@ -100,7 +101,9 @@ const setConfig = async cfg => {
config.ilpSecret = cfg.ILP_SECRET;
config.checkIlp = cfg.CHECK_ILP.toLowerCase() === 'false' ? false : true;
config.expirySeconds = Number(cfg.EXPIRY_SECONDS);

config.autoAcceptQuotes = cfg.AUTO_ACCEPT_QUOTES.toLowerCase() === 'true' ? true : false;
config.autoAcceptParty = cfg.AUTO_ACCEPT_PARTY ? (cfg.AUTO_ACCEPT_PARTY.toLowerCase() === 'true' ? true : false) : true;

// Getting secrets from files instead of environment variables reduces the likelihood of
// accidental leakage.
Expand Down
100 changes: 65 additions & 35 deletions src/lib/model/outboundModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ const shared = require('@internal/shared');
const ASYNC_TIMEOUT_MILLS = 30000;

const transferStateEnum = {
'WAITING_FOR_PARTY_ACEPTANCE': 'WAITING_FOR_PARTY_ACCEPTANCE',
'WAITING_FOR_QUOTE_ACCEPTANCE': 'WAITING_FOR_QUOTE_ACCEPTANCE',
'ERROR_OCCURED': 'ERROR_OCCURED',
'COMPLETED': 'COMPLETED'
'COMPLETED': 'COMPLETED',
};


Expand All @@ -37,6 +38,7 @@ class OutboundTransfersModel {
this.dfspId = config.dfspId;
this.expirySeconds = config.expirySeconds;
this.autoAcceptQuotes = config.autoAcceptQuotes;
this.autoAcceptParty = config.autoAcceptParty;

this.requests = new MojaloopRequests({
logger: this.logger,
Expand All @@ -61,14 +63,14 @@ class OutboundTransfersModel {
this.stateMachine = new StateMachine({
init: initState,
transitions: [
{ name: 'resolvePayee', from: 'start', to: 'resolvePayee' },
{ name: 'requestQuote', from: 'resolvePayee', to: 'requestQuote' },
{ name: 'executeTransfer', from: 'requestQuote', to: 'executeTransfer' },
{ name: 'succeeded', from: 'executeTransfer', to: 'succeeded' },
{ name: 'resolvePayee', from: 'start', to: 'payeeResolved' },
{ name: 'requestQuote', from: 'payeeResolved', to: 'quoteReceived' },
{ name: 'executeTransfer', from: 'quoteReceived', to: 'succeeded' },
{ name: 'error', from: '*', to: 'errored' },
],
methods: {
onAfterTransition: this._handleTransition.bind(this),
onTransition: this._handleTransition.bind(this),
onAfterTransition: this._afterTransition.bind(this),
onPendingTransition: (transition, from, to) => {
// allow transitions to 'error' state while other transitions are in progress
if(transition !== 'error') {
Expand All @@ -82,6 +84,14 @@ class OutboundTransfersModel {
}


/**
* Updates the internal state representation to reflect that of the state machine itself
*/
_afterTransition() {
this.data.currentState = this.stateMachine.state;
}


/**
* Initializes the transfer model
*
Expand Down Expand Up @@ -470,19 +480,20 @@ class OutboundTransfersModel {
* @returns {object} - Response representing the result of the transfer process
*/
getResponse() {
// make sure the current stateMachine state is up to date
this.data.currentState = this.stateMachine.state;

// we want to project some of our internal state into a more useful
// representation to return to the SDK API consumer
let resp = { ...this.data };

switch(this.data.currentState) {
case 'requestQuote':
case 'payeeResolved':
resp.currentState = transferStateEnum.WAITING_FOR_PARTY_ACEPTANCE;
break;

case 'quoteReceived':
resp.currentState = transferStateEnum.WAITING_FOR_QUOTE_ACCEPTANCE;
break;

case 'executeTransfer':
case 'succeeded':
resp.currentState = transferStateEnum.COMPLETED;
break;

Expand Down Expand Up @@ -552,34 +563,53 @@ class OutboundTransfersModel {
*/
async run() {
try {
if(this.data.currentState === 'start') {
// we are at the start of the transfer process so proceed with reslving payee and requesting a quote
await this.stateMachine.resolvePayee();
this.logger.log(`Payee resolved for transfer ${this.data.transferId}`);

await this.stateMachine.requestQuote();
this.logger.log(`Quote received for transfer ${this.data.transferId}`);

if(!this.autoAcceptQuotes) {
// we are configured to require a quote confirmation so return now.
// we may be resumed later with a quote confirmation.
this.logger.log('Transfer model skipping execution of transfer and will wait for quote confirmation or rejection');
await this._save();
return this.getResponse();
}
}
// run transitions based on incoming state
switch(this.data.currentState) {
case 'start':
// next transition is to resolvePayee
await this.stateMachine.resolvePayee();
this.logger.log(`Payee resolved for transfer ${this.data.transferId}`);
if(this.stateMachine.state === 'payeeResolved' && !this.autoAcceptParty) {
//we break execution here and return the resolved party details to allow asynchronous accept or reject
//of the resolved party
await this._save();
return this.getResponse();
}
break;

case 'payeeResolved':
// next transition is to requestQuote
await this.stateMachine.requestQuote();
this.logger.log(`Quote received for transfer ${this.data.transferId}`);
if(this.stateMachine.state === 'quoteReceived' && !this.autoAcceptQuotes) {
//we break execution here and return the quote response details to allow asynchronous accept or reject
//of the quote
await this._save();
return this.getResponse();
}
break;

//if(this.data.currentState !== 'requestQuote') {
// throw new Error(`Unable to continue with transfer ${this.data.transferId} model. Expected to be in requestQuote state but in ${this.data.currentState}`);
//}
case 'quoteReceived':
// next transition is executeTransfer
await this.stateMachine.executeTransfer();
this.logger.log(`Transfer ${this.data.transferId} has been completed`);
break;

await this.stateMachine.executeTransfer();
this.logger.log('Transfer fulfilled');
case 'succeeded':
// all steps complete so return
this.logger.log('Transfer completed successfully');
await this._save();
return this.getResponse();

this.logger.log(`Transfer model state machine ended in state: ${this.stateMachine.state}`);
case 'error':
// stopped in errored state
this.logger.log('State machine in errored state');
return this.getResponse();
}

await this._unsubscribeAll();
return this.getResponse();
// now call ourslves recursively to deal with the next transition
this.logger.log(`Transfer model state machine transition completed in state: ${this.stateMachine.state}. Recusring to handle next transition.`);
return await this.run();
}
catch(err) {
this.logger.push({ err }).log('Error running transfer model');
Expand Down
5 changes: 5 additions & 0 deletions src/local.env
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ EXPIRY_SECONDS=60
# confirmation call will be required to complete the final transfer stage.
AUTO_ACCEPT_QUOTES=false

# if set to false the SDK will not automatically accept a resolved party
# but will halt the transer after a party lookup response is received. A further
# cnofirmation call will be required to progress the transfer to quotes state.
AUTO_ACCEPT_PARTY=false

# set to true to validate ILP, otherwise false to ignore ILP
CHECK_ILP=true

Expand Down
1 change: 1 addition & 0 deletions src/test/unit/config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe('config', () => {
checkIlp: true,
expirySeconds: 60,
autoAcceptQuotes: true,
autoAcceptParty: true,
tls: {
mutualTLS: { enabled: false },
inboundCreds: {
Expand Down

0 comments on commit 56ee09c

Please sign in to comment.