Skip to content

Commit

Permalink
Merge pull request #24 from interledger/feature/dj-path-from-source
Browse files Browse the repository at this point in the history
[FEATURE] Support pathfinding by source amount
  • Loading branch information
emschwartz committed Feb 10, 2016
2 parents 0e05819 + cb2294b commit 62af709
Show file tree
Hide file tree
Showing 6 changed files with 263 additions and 51 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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)
})
})

0 comments on commit 62af709

Please sign in to comment.