Skip to content

Commit

Permalink
feat: BREAKING implement Interledger Payment Request
Browse files Browse the repository at this point in the history
based on interledger/rfcs#76

closes #22
  • Loading branch information
emschwartz committed Aug 23, 2016
1 parent 9ded9a4 commit 4401a8b
Show file tree
Hide file tree
Showing 10 changed files with 96 additions and 78 deletions.
17 changes: 8 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ This is a low-level interface to ILP, largely intended for building ILP into oth

#### The ILP Client does:

* Generate payment requests on the receiving side, including handling [Crypto Condition](/~https://github.com/interledger/rfcs/tree/master/0002-crypto-conditions) generation and fulfillment (using the [Interactive Transport Protocol (ITP)](/~https://github.com/interledger/rfcs/blob/master/0011-interactive-transport-protocol/0011-interactive-transport-protocol.md) )
* Generate [Interledger Payment Requests](/~https://github.com/interledger/rfcs/blob/master/0011-interledger-payment-request/0011-interledger-payment-request.md) on the receiving side, including handling [Crypto Condition](/~https://github.com/interledger/rfcs/tree/master/0002-crypto-conditions) generation and fulfillment)
* Pay for payment requests on the sending side
* Quote and send payments through multiple ledger types (using [`ilp-core`](/~https://github.com/interledger/js-ilp-core))

Expand All @@ -42,14 +42,12 @@ For a higher-level interface that includes the above features, see the [Wallet C

`npm install --save ilp ilp-plugin-bells`

*Note that [ledger plugins](https://www.npmjs.com/search?q=ilp-plugin) must be installed alongside this module*
*Note that [ledger plugins](https://www.npmjs.com/search?q=ilp-plugin) must be installed alongside this module


## ITP Request / Pay

The client implements the [Interactive Transport Protocol (ITP)](/~https://github.com/interledger/rfcs/blob/master/0011-interactive-transport-protocol/0011-interactive-transport-protocol.md) for generating and fulfilling payment requests.

ITP uses recipient-generated conditions to secure payments. This means that the recipient must first generate a payment request, which the sender then fulfills. This client library handles the generation of such requests, but **not** the communication of the request details from the recipient to the sender.
The client uses recipient-generated [Interledger Payment Requests](/~https://github.com/interledger/rfcs/blob/master/0011-interledger-payment-request/0011-interledger-payment-request.md), which include the condition for the payment. This means that the recipient must first generate a payment request, which the sender then fulfills. This client library handles the generation of such requests, but **not** the communication of the request details from the recipient to the sender.

### Requesting + Handling Incoming Payments

Expand Down Expand Up @@ -152,7 +150,7 @@ co(function * () {
<a name="module_Sender..createSender"></a>

### Sender~createSender(opts) ⇒ <code>Sender</code>
Returns an ITP/ILP Sender to quote and pay for payment requests.
Returns an ILP Sender to quote and pay for payment requests.

**Kind**: inner method of <code>[Sender](#module_Sender)</code>

Expand All @@ -178,7 +176,7 @@ Quote a request from a receiver

| Param | Type | Description |
| --- | --- | --- |
| paymentRequest | <code>Object</code> | Payment request generated by an ITP/ILP Receiver |
| paymentRequest | <code>Object</code> | Payment request generated by an ILP Receiver |

<a name="module_Sender..createSender..payRequest"></a>

Expand All @@ -196,7 +194,7 @@ Pay for a payment request
<a name="module_Receiver..createReceiver"></a>

### Receiver~createReceiver(opts) ⇒ <code>Receiver</code>
Returns an ITP/ILP Receiver to create payment requests,
Returns an ILP Receiver to create payment requests,
listen for incoming transfers, and automatically fulfill conditions
of transfers paying for the payment requests created by the Receiver.

Expand All @@ -205,7 +203,7 @@ of transfers paying for the payment requests created by the Receiver.
| Param | Type | Default | Description |
| --- | --- | --- | --- |
| opts._plugin | <code>LedgerPlugin</code> | | Ledger plugin used to connect to the ledger, passed to [ilp-core](/~https://github.com/interledger/js-ilp-core) |
| opts | <code>Objct</code> | | Plugin parameters, passed to [ilp-core](/~https://github.com/interledger/js-ilp-core) |
| opts | <code>Object</code> | | Plugin parameters, passed to [ilp-core](/~https://github.com/interledger/js-ilp-core) |
| [opts.client] | <code>ilp-core.Client</code> | <code>create a new instance with the plugin and opts</code> | [ilp-core](/~https://github.com/interledger/js-ilp-core) Client, which can optionally be supplied instead of the previous options |
| [opts.hmacKey] | <code>Buffer</code> | <code>crypto.randomBytes(32)</code> | 32-byte secret used for generating request conditions |
| [opts.defaultRequestTimeout] | <code>Number</code> | <code>30</code> | Default time in seconds that requests will be valid for |
Expand All @@ -229,6 +227,7 @@ Create a payment request
| params.amount | <code>String</code> | | Amount to request |
| [params.id] | <code>String</code> | <code>uuid.v4()</code> | Unique ID for the request (used to ensure conditions are unique per request) |
| [params.expiresAt] | <code>String</code> | <code>30 seconds from now</code> | Expiry of request |
| [params.data] | <code>Object</code> | <code></code> | Additional data to include in the request |

<a name="module_Receiver..createReceiver..listen"></a>

Expand Down
6 changes: 2 additions & 4 deletions docs/README.template.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ This is a low-level interface to ILP, largely intended for building ILP into oth

#### The ILP Client does:

* Generate payment requests on the receiving side, including handling [Crypto Condition](/~https://github.com/interledger/rfcs/tree/master/0002-crypto-conditions) generation and fulfillment (using the [Interactive Transport Protocol (ITP)](/~https://github.com/interledger/rfcs/blob/master/0011-interactive-transport-protocol/0011-interactive-transport-protocol.md) )
* Generate [Interledger Payment Requests](/~https://github.com/interledger/rfcs/blob/master/0011-interledger-payment-request/0011-interledger-payment-request.md) on the receiving side, including handling [Crypto Condition](/~https://github.com/interledger/rfcs/tree/master/0002-crypto-conditions) generation and fulfillment)
* Pay for payment requests on the sending side
* Quote and send payments through multiple ledger types (using [`ilp-core`](/~https://github.com/interledger/js-ilp-core))

Expand All @@ -47,9 +47,7 @@ For a higher-level interface that includes the above features, see the [Wallet C

## ITP Request / Pay

The client implements the [Interactive Transport Protocol (ITP)](/~https://github.com/interledger/rfcs/blob/master/0011-interactive-transport-protocol/0011-interactive-transport-protocol.md) for generating and fulfilling payment requests.

ITP uses recipient-generated conditions to secure payments. This means that the recipient must first generate a payment request, which the sender then fulfills. This client library handles the generation of such requests, but **not** the communication of the request details from the recipient to the sender.
The client uses recipient-generated [Interledger Payment Requests](/~https://github.com/interledger/rfcs/blob/master/0011-interledger-payment-request/0011-interledger-payment-request.md), which include the condition for the payment. This means that the recipient must first generate a payment request, which the sender then fulfills. This client library handles the generation of such requests, but **not** the communication of the request details from the recipient to the sender.

### Requesting + Handling Incoming Payments

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
"keywords": [
"interledger",
"ilp",
"interactive",
"itp",
"payment request",
"ipr",
"crypto",
"condition",
"five-bells-condition",
Expand Down
33 changes: 19 additions & 14 deletions src/lib/receiver.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const BigNumber = require('bignumber.js')
*/

/**
* Returns an ITP/ILP Receiver to create payment requests,
* Returns an ILP Receiver to create payment requests,
* listen for incoming transfers, and automatically fulfill conditions
* of transfers paying for the payment requests created by the Receiver.
*
Expand Down Expand Up @@ -47,6 +47,7 @@ function createReceiver (opts) {
* @param {String} params.amount Amount to request
* @param {String} [params.id=uuid.v4()] Unique ID for the request (used to ensure conditions are unique per request)
* @param {String} [params.expiresAt=30 seconds from now] Expiry of request
* @param {Object} [params.data=null] Additional data to include in the request
* @return {Object}
*/
function createRequest (params) {
Expand All @@ -62,21 +63,17 @@ function createReceiver (opts) {
throw new Error('expiresAt must be an ISO 8601 timestamp')
}

const packet = {
const paymentRequest = {
address: account + '.' + (params.id || uuid.v4()),
amount: String(params.amount),
account: account,
data: {
expires_at: params.expiresAt || moment().add(defaultRequestTimeout, 'seconds').toISOString(),
request_id: params.id || uuid.v4()
}
expires_at: params.expiresAt || moment().add(defaultRequestTimeout, 'seconds').toISOString(),
data: params.data
}

const conditionPreimage = generateConditionPreimage(hmacKey, packet)
const conditionPreimage = generateConditionPreimage(hmacKey, paymentRequest)
paymentRequest.condition = toConditionUri(conditionPreimage)

return {
packet: packet,
condition: toConditionUri(conditionPreimage)
}
return paymentRequest
}

/**
Expand All @@ -99,14 +96,22 @@ function createReceiver (opts) {
return 'no-execution'
}

// The request is the ilp_header
// The payment request is extracted from the ilp_header
let packet = transfer.data && transfer.data.ilp_header

if (!packet) {
debug('got notification of transfer with no packet attached')
return 'no-packet'
}

const paymentRequest = {
address: packet.account,
amount: packet.amount,
expires_at: packet.data.expires_at,
data: packet.data.data
}


if ((new BigNumber(transfer.amount)).lessThan(packet.amount)) {
debug('got notification of transfer where amount is less than expected (' + packet.amount + ')', transfer)
return 'insufficient'
Expand All @@ -117,7 +122,7 @@ function createReceiver (opts) {
return 'overpayment-disallowed'
}

if (packet.data.expires_at && moment().isAfter(packet.data.expires_at)) {
if (paymentRequest.expires_at && moment().isAfter(paymentRequest.expires_at)) {
debug('got notification of transfer with expired packet', transfer)
return 'expired'
}
Expand Down
28 changes: 16 additions & 12 deletions src/lib/sender.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const debug = require('debug')('ilp:sender')
*/

/**
* Returns an ITP/ILP Sender to quote and pay for payment requests.
* Returns an ILP Sender to quote and pay for payment requests.
*
* @param {LedgerPlugin} opts._plugin Ledger plugin used to connect to the ledger, passed to [ilp-core](/~https://github.com/interledger/js-ilp-core)
* @param {Objct} opts Plugin parameters, passed to [ilp-core](/~https://github.com/interledger/js-ilp-core)
Expand All @@ -24,24 +24,25 @@ function createSender (opts) {

/**
* Quote a request from a receiver
* @param {Object} paymentRequest Payment request generated by an ITP/ILP Receiver
* @param {Object} paymentRequest Payment request generated by an ILP Receiver
* @return {Promise.<PaymentParams>} Resolves with the parameters that can be passed to payRequest
*/
function quoteRequest (request) {
if (!request.packet) {
return Promise.reject(new Error('Malformed payment request: no packet'))
if (!request.address) {
return Promise.reject(new Error('Malformed payment request: no address'))
}
if (!request.amount) {
return Promise.reject(new Error('Malformed payment request: no amount'))
}
if (!request.condition) {
return Promise.reject(new Error('Malformed payment request: no condition'))
}

// TODO validate request more

return client.connect()
.then(() => client.waitForConnection())
.then(() => client.quote({
destinationAddress: request.packet.account,
destinationAmount: request.packet.amount
destinationAddress: request.address,
destinationAmount: request.amount
}))
.then((quote) => {
debug('got quote response', quote)
Expand All @@ -51,11 +52,14 @@ function createSender (opts) {
return {
sourceAmount: String(quote.sourceAmount),
connectorAccount: quote.connectorAccount,
destinationAmount: String(request.packet.amount),
destinationAccount: request.packet.account,
destinationMemo: request.packet.data,
destinationAmount: String(request.amount),
destinationAccount: request.address,
destinationMemo: {
data: request.data,
expires_at: request.expires_at
},
expiresAt: moment.min([
moment(request.packet.data.expires_at),
moment(request.expires_at),
moment().add(maxHoldDuration, 'seconds')
]).toISOString(),
executionCondition: request.condition
Expand Down
10 changes: 6 additions & 4 deletions test/data/paymentParams.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
{
"connectorAccount": "https://blue.ilpdemo.org/ledger/accounts/connie",
"destinationAccount": "ilpdemo.blue.bob",
"destinationAccount": "ilpdemo.blue.bob.22e315dc-3f99-4f89-9914-1987ceaa906d",
"sourceAmount": "2",
"destinationAmount": "1",
"destinationMemo":{
"request_id": "22e315dc-3f99-4f89-9914-1987ceaa906d",
"expires_at": "1970-01-01T00:00:10Z"
"expires_at": "1970-01-01T00:00:10Z",
"data": {
"for": "that thing"
}
},
"executionCondition": "cc:0:3:nQ7peM4X0r84PFg04aOKX84OR7WBLTVvppN8gFYC1Is:32",
"executionCondition": "cc:0:3:hd5x8kpaDDLQu-KqMyCrlsg5QJ9g9qaFr9ytTwqyCsw:32",
"expiresAt": "1970-01-01T00:00:10.000Z"
}
14 changes: 6 additions & 8 deletions test/data/paymentRequest.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
{
"packet": {
"account": "ilpdemo.blue.bob",
"amount": "1",
"data": {
"request_id": "22e315dc-3f99-4f89-9914-1987ceaa906d",
"expires_at": "1970-01-01T00:00:10Z"
}
"address": "ilpdemo.blue.bob.22e315dc-3f99-4f89-9914-1987ceaa906d",
"amount": "1",
"expires_at": "1970-01-01T00:00:10Z",
"data": {
"for": "that thing"
},
"condition": "cc:0:3:nQ7peM4X0r84PFg04aOKX84OR7WBLTVvppN8gFYC1Is:32"
"condition": "cc:0:3:hd5x8kpaDDLQu-KqMyCrlsg5QJ9g9qaFr9ytTwqyCsw:32"
}
10 changes: 6 additions & 4 deletions test/data/transferIncoming.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@
"amount": "1.0000",
"data":{
"ilp_header": {
"account": "ilpdemo.blue.bob",
"account": "ilpdemo.blue.bob.22e315dc-3f99-4f89-9914-1987ceaa906d",
"amount": "1",
"data": {
"request_id": "22e315dc-3f99-4f89-9914-1987ceaa906d",
"expires_at": "1970-01-01T00:00:10Z"
"expires_at": "1970-01-01T00:00:10Z",
"data": {
"for": "that thing"
}
}
}
},
"executionCondition": "cc:0:3:nQ7peM4X0r84PFg04aOKX84OR7WBLTVvppN8gFYC1Is:32",
"executionCondition": "cc:0:3:hd5x8kpaDDLQu-KqMyCrlsg5QJ9g9qaFr9ytTwqyCsw:32",
"expiresAt": "1970-01-01T00:00:10.000Z"
}
28 changes: 16 additions & 12 deletions test/receiverSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,25 +136,33 @@ describe('Receiver Module', function () {
})).to.be.a('object')
})

it('should generate an id if one is not given', function () {
it('should use the account from the client in the address', function () {
const request = this.receiver.createRequest({
amount: 10
})
expect(request.packet.data.request_id).to.match(/^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$/)
expect(request.address).to.match(/^ilpdemo\.blue\.bob/)
})

it('should use the account from the client', function () {
it('should create a request-specific address using the account and id', function () {
const request = this.receiver.createRequest({
amount: 10,
id: 'test'
})
expect(request.address).to.equal('ilpdemo.blue.bob.test')
})

it('should generate an id if one is not given', function () {
const request = this.receiver.createRequest({
amount: 10
})
expect(request.packet.account).to.equal('ilpdemo.blue.bob')
expect(request.address).to.match(/^ilpdemo\.blue\.bob\.[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$/)
})

it('should set the expiresAt to be 30 seconds if one is not supplied', function () {
const request = this.receiver.createRequest({
amount: 10
})
expect(request.packet.data.expires_at).to.equal('1970-01-01T00:00:30.000Z')
expect(request.expires_at).to.equal('1970-01-01T00:00:30.000Z')
})

it.skip('should generate the condition from the request details', function () {
Expand Down Expand Up @@ -251,14 +259,10 @@ describe('Receiver Module', function () {

it('should fulfill the conditions of transfers corresponding to requests generated by the receiver', function * () {
const request = this.receiver.createRequest({
amount: 1
amount: 1,
id: '22e315dc-3f99-4f89-9914-1987ceaa906d'
})
const results = yield this.client.emitAsync('incoming_prepare', _.assign(this.transfer, {
data: {
ilp_header: request.packet
},
executionCondition: request.condition
}))
const results = yield this.client.emitAsync('incoming_prepare', this.transfer)
expect(results).to.deep.equal(['sent'])
})

Expand Down
24 changes: 15 additions & 9 deletions test/senderSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,10 @@ describe('Sender Module', function () {
beforeEach(function () {
this.paymentRequest = _.cloneDeep(paymentRequest)
this.quoteStub = sinon.stub(this.client, 'quote')
this.quoteStub.withArgs({
destinationAddress: 'ilpdemo.blue.bob',
this.quoteStub.withArgs(sinon.match({
destinationAddress: sinon.match(/^ilpdemo\.blue\.bob\./),
destinationAmount: '1'
}).resolves({
})).resolves({
connectorAccount: 'https://blue.ilpdemo.org/ledger/accounts/connie',
sourceAmount: '2'
})
Expand All @@ -91,16 +91,22 @@ describe('Sender Module', function () {

})

it('should reject if there is no execution condition', function (done) {
it('should reject if there is no address', function (done) {
expect(this.sender.quoteRequest(_.assign(this.paymentRequest, {
condition: null
}))).to.be.rejectedWith('Malformed payment request: no condition').notify(done)
address: null
}))).to.be.rejectedWith('Malformed payment request: no address').notify(done)
})

it('should reject if there is no packet', function (done) {
it('should reject if there is no amount', function (done) {
expect(this.sender.quoteRequest(_.assign(this.paymentRequest, {
packet: null
}))).to.be.rejectedWith('Malformed payment request: no packet').notify(done)
amount: null
}))).to.be.rejectedWith('Malformed payment request: no amount').notify(done)
})

it('should reject if there is no execution condition', function (done) {
expect(this.sender.quoteRequest(_.assign(this.paymentRequest, {
condition: null
}))).to.be.rejectedWith('Malformed payment request: no condition').notify(done)
})

it('should accept a payment request generated by the Receiver', function * () {
Expand Down

0 comments on commit 4401a8b

Please sign in to comment.