Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Support pathfinding by source amount #24

Merged
merged 1 commit into from Feb 10, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -57,6 +57,7 @@
"eslint-plugin-standard": "^1.3.0",
"istanbul": "^0.4.0",
"mocha": "^2.3.2",
"nock": "^7.0.2",
"randomgraph": "^0.1.3",
"spec-xunit-file": "0.0.1-3",
"supertest": "^1.1.0"
Expand Down
23 changes: 15 additions & 8 deletions src/pathfinder.js
Expand Up @@ -67,9 +67,11 @@ class Pathfinder {

log.info('findPath found ' + paths.length + ' paths')

const pathsQuotes = yield this.quotingClient.quotePathsFromDestination({
destinationAccount: params.destinationAccount,
destinationAmount: params.destinationAmount
const pathsQuotes = yield this.quotingClient.quotePaths({
source_account: params.sourceAccount,
source_amount: params.sourceAmount,
destination_account: params.destinationAccount,
destination_amount: params.destinationAmount
// TODO add other params like destination expiry duration
}, paths)

Expand All @@ -82,11 +84,16 @@ class Pathfinder {
return cheapestPath
}

// params -
// sourceLedger
// destinationLedger
// destinationAmount
// destinationAccount (optional)
/**
* @param {Object} params
* @param {String} params.sourceLedger
* @param {String} params.destinationLedger
* @param {String} params.sourceAmount - either this or destinationAmount is required
* @param {String} params.destinationAmount - either this or sourceAmount is required
* @param {String} params.sourceAccount - provided with sourceAmount (optional)
* @param {String} params.destinationAccount - provided with destinationAmount (optional)
* @returns {Promise<[Quote]>}
*/
findPath (params) { return co(this._findPath.bind(this), params) }
}

Expand Down
102 changes: 59 additions & 43 deletions src/quoting-client.js
Expand Up @@ -2,63 +2,77 @@

const request = require('superagent')
const uuid = require('uuid4')
const _ = require('lodash')

class QuotingClient {
constructor (crawler) {
const _this = this

this.crawler = crawler
this.pairs = {}

this.crawler.on('pair', function (pair) {
_this.handleTradingPair(pair)
})
this.crawler.on('pair', this._handleTradingPair.bind(this))
}

handleTradingPair (pair) {
_handleTradingPair (pair) {
this.pairs[pair.source + ';' + pair.destination] = pair.uri
}

* quotePathFromDestination (params, path) {
path = path.slice()

let payments = []
let destination = path.pop()
let destinationAccount = params.destinationAccount
let destinationAmount = params.destinationAmount
let destinationExpiryDuration = params.destinationExpiryDuration ||
path.length + 2

while (path.length) {
let source = path.pop()
let trader = this.pairs[source + ';' + destination]
quotePaths (params, paths) {
if (params.destination_amount) {
return this.quotePathsFromDestination(params, paths)
}
if (params.source_amount) {
return this.quotePathsFromSource(params, paths)
}
throw new Error('requires sourceAmount or destinationAmount')
}

let query = {
source_ledger: source,
destination_ledger: destination,
destination_account: destinationAccount,
destination_amount: destinationAmount,
destination_expiry_duration: destinationExpiryDuration
}
quotePathsFromDestination (params, paths) {
return paths.map((path) => this.quotePathFromDestination(params, path))
}

let payment = yield this.getTraderQuote(trader, query)
let paymentUuid = uuid()
payment.id = trader + '/payments/' + paymentUuid
quotePathsFromSource (params, paths) {
return paths.map((path) => this.quotePathFromSource(params, path))
}

* quotePathFromDestination (params, path) {
params = _.clone(params)
path = path.slice()
const payments = []
params.destination_expiry_duration = params.destination_expiry_duration || path.length + 1
params.destination_ledger = path.pop()
params.source_ledger = path.pop()
while (params.source_ledger) {
const payment = yield this.getTraderQuote(params)
payments.unshift(payment)
destination = source
destinationAmount = payment.source_transfers[0].credits[0].amount
destinationAccount = payment.source_transfers[0].credits[0].account
destinationExpiryDuration = payment.source_transfers[0].expiry_duration
params = {
source_ledger: path.pop(),
destination_ledger: params.source_ledger,
destination_amount: payment.source_transfers[0].credits[0].amount,
destination_account: payment.source_transfers[0].credits[0].account,
destination_expiry_duration: payment.source_transfers[0].expiry_duration
}
}

return payments
}

quotePathsFromDestination (params, paths) {
return paths.map((path) => {
return this.quotePathFromDestination(params, path)
})
* quotePathFromSource (params, path) {
params = _.clone(params)
path = path.slice()
const payments = []
params.source_expiry_duration = params.source_expiry_duration || 2 * path.length
params.source_ledger = path.shift()
params.destination_ledger = path.shift()
while (params.destination_ledger) {
const payment = yield this.getTraderQuote(params)
payments.push(payment)
params = {
source_ledger: params.destination_ledger,
destination_ledger: path.shift(),
source_amount: payment.destination_transfers[0].debits[0].amount,
source_account: payment.destination_transfers[0].debits[0].account,
source_expiry_duration: payment.destination_transfers[0].expiry_duration
}
}
return payments
}

/**
Expand All @@ -71,15 +85,17 @@ class QuotingClient {
* destination_asset
* destination_amount or source_amount
*/
* getTraderQuote (trader, quoteOpts) {
let res = yield request
* getTraderQuote (quoteOpts) {
const trader = this.pairs[quoteOpts.source_ledger + ';' + quoteOpts.destination_ledger]
const res = yield request
.get(trader + '/quote')
.query(quoteOpts)
if (res.status >= 400) {
throw new Error('Server Error: ' + res.status + ' ' + res.body)
}

return res.body
const payment = res.body
payment.id = trader + '/payments/' + uuid()
return payment
}
}

Expand Down
39 changes: 39 additions & 0 deletions test/fixtures/from-destination.json
@@ -0,0 +1,39 @@
{
"quoteArgs23": {
"source_ledger": "http://ledger2.example",
"destination_ledger": "http://ledger3.example",
"destination_account": "http://ledger3.example/accounts/bob",
"destination_amount": "100",
"destination_expiry_duration": 1
},

"quoteArgs12": {
"source_ledger": "http://ledger1.example",
"destination_ledger": "http://ledger2.example",
"destination_account": "http://ledger2.example/accounts/trader23",
"destination_amount": "90",
"destination_expiry_duration": 2
},

"quote23": {
"source_transfers": [{
"debits": [{"amount": "100"}],
"credits": [{
"account": "http://ledger2.example/accounts/trader23",
"amount": "90"
}],
"expiry_duration": 2
}]
},

"quote12": {
"source_transfers": [{
"debits": [{"amount": "90"}],
"credits": [{
"account": "http://ledger1.example/accounts/trader12",
"amount": "80"
}],
"expiry_duration": 3
}]
}
}
39 changes: 39 additions & 0 deletions test/fixtures/from-source.json
@@ -0,0 +1,39 @@
{
"quoteArgs12": {
"source_ledger": "http://ledger1.example",
"destination_ledger": "http://ledger2.example",
"source_account": "http://ledger1.example/accounts/alice",
"source_amount": "80",
"source_expiry_duration": 3
},

"quoteArgs23": {
"source_ledger": "http://ledger2.example",
"destination_ledger": "http://ledger3.example",
"source_account": "http://ledger2.example/accounts/trader12",
"source_amount": "90",
"source_expiry_duration": 2
},

"quote12": {
"destination_transfers": [{
"debits": [{
"account": "http://ledger2.example/accounts/trader12",
"amount": "90"
}],
"credits": [{"amount": "90"}],
"expiry_duration": 2
}]
},

"quote23": {
"destination_transfers": [{
"debits": [{
"account": "http://ledger3.example/accounts/trader23",
"amount": "100"
}],
"credits": [{"amount": "90"}],
"expiry_duration": 1
}]
}
}
110 changes: 110 additions & 0 deletions test/quoting-client.test.js
@@ -0,0 +1,110 @@
/* eslint-env node, mocha */
'use strict'

const assert = require('assert')
const EventEmitter = require('events').EventEmitter
const inherits = require('util').inherits
const nock = require('nock')
const QuotingClient = require('../src/quoting-client').QuotingClient

function FakeCrawler () { }
inherits(FakeCrawler, EventEmitter)

const pairs = {
'http://ledger1.example;http://ledger2.example': 'http://trader12.example',
'http://ledger2.example;http://ledger3.example': 'http://trader23.example'
}

// (alice -> trader12) -> (trader12 -> trader23) -> (trader23 -> bob)
const path = [
'http://ledger1.example',
'http://ledger2.example',
'http://ledger3.example'
]

beforeEach(function () {
this.client = new QuotingClient(new FakeCrawler())
this.client.pairs = pairs
})

describe('QuotingClient#quotePathFromDestination', function () {
const fixtures = require('./fixtures/from-destination')
const quoteArgs12 = fixtures.quoteArgs12
const quoteArgs23 = fixtures.quoteArgs23
const quote12 = fixtures.quote12
const quote23 = fixtures.quote23

it('returns quotes', function * () {
const quote23Nock = nock('http://trader23.example').get('/quote')
.query(quoteArgs23)
.reply(200, quote23)
const quote12Nock = nock('http://trader12.example').get('/quote')
.query(quoteArgs12)
.reply(200, quote12)

const quotes = yield this.client.quotePathFromDestination({
destination_account: 'http://ledger3.example/accounts/bob',
destination_amount: '100',
destination_expiry_duration: 1
}, path)

quote23Nock.done()
quote12Nock.done()

assert.equal(quotes.length, 2)
assert.equal(quotes[0].id.indexOf('http://trader12.example/payments/'), 0)
assert.equal(quotes[1].id.indexOf('http://trader23.example/payments/'), 0)
assert.equal(quotes[0].source_transfers[0].expiry_duration, 3)
assert.equal(quotes[1].source_transfers[0].expiry_duration, 2)
})

it('throws on 400', function * () {
const quote23Nock = nock('http://trader23.example').get('/quote')
.query(quoteArgs23)
.reply(400)

try {
yield this.client.quotePathFromDestination({
destination_account: 'http://ledger3.example/accounts/bob',
destination_amount: '100',
destination_expiry_duration: 1
}, path)
} catch (err) {
quote23Nock.done()
return
}
assert(false)
})
})

describe('QuotingClient#quotePathFromSource', function () {
const fixtures = require('./fixtures/from-source')
const quoteArgs12 = fixtures.quoteArgs12
const quoteArgs23 = fixtures.quoteArgs23
const quote12 = fixtures.quote12
const quote23 = fixtures.quote23

it('returns quotes', function * () {
const quote12Nock = nock('http://trader12.example').get('/quote')
.query(quoteArgs12)
.reply(200, quote12)
const quote23Nock = nock('http://trader23.example').get('/quote')
.query(quoteArgs23)
.reply(200, quote23)

const quotes = yield this.client.quotePathFromSource({
source_account: 'http://ledger1.example/accounts/alice',
source_amount: '80',
source_expiry_duration: 3
}, path)

quote23Nock.done()
quote12Nock.done()

assert.equal(quotes.length, 2)
assert.equal(quotes[0].id.indexOf('http://trader12.example/payments/'), 0)
assert.equal(quotes[1].id.indexOf('http://trader23.example/payments/'), 0)
assert.equal(quotes[0].destination_transfers[0].expiry_duration, 2)
assert.equal(quotes[1].destination_transfers[0].expiry_duration, 1)
})
})