├── .eslintrc ├── .gitignore ├── Gemfile ├── README.md ├── _config.yml ├── _includes ├── docs-sidebar.html ├── footer.html ├── head.html ├── nav.html └── scripts.html ├── _layouts ├── page.html └── redirect.html ├── circle.yml ├── hosted-ledgers ├── index.md ├── plugins.js ├── streaming-client-from-before.js └── streaming-shop-from-before.js ├── http-ilp ├── client1.js ├── client2.js ├── client3.js ├── index.md ├── shop-koa.js ├── shop1.js ├── shop2.js └── shop3.js ├── index.md ├── letter-shop ├── README.md ├── client.js ├── completed │ ├── client.js │ ├── package-lock.json │ ├── package.json │ ├── pay.js │ ├── plugins.js │ └── shop.js ├── index.md ├── package-lock.json ├── package.json ├── pay.js ├── plugins.js └── shop.js ├── package.json ├── streaming-payments ├── index.md ├── shop-from-before.js ├── streaming-client1.js ├── streaming-client2.js └── streaming-shop.js └── test └── .eslintrc /.eslintrc: -------------------------------------------------------------------------------- 1 | extends: standard 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | creds.js 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # nyc test coverage 20 | .nyc_output 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directories 32 | node_modules 33 | jspm_packages 34 | 35 | # Optional npm cache directory 36 | .npm 37 | 38 | # Optional REPL history 39 | .node_repl_history 40 | 41 | # Jekyll 42 | /_site 43 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'github-pages', group: :jekyll_plugins 3 | 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Interledger tutorials 2 | 3 | This repo is available to view at https://interledger.org/tutorials/ 4 | 5 | The following tutorials are available: 6 | 7 | * [The Letter Shop](../../tree/master/letter-shop) 8 | * [Streaming Payments](../../tree/master/streaming-payments) 9 | * [Trustlines](../../tree/master/trustlines) 10 | 11 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | title: The Interledger Project 2 | email: info@interledger.org 3 | description: > # this means to ignore newlines until "baseurl:" 4 | Write an awesome description for your new site here. You can edit this 5 | line in _config.yml. It will appear in your document head meta (for 6 | Google search results) and in your feed.xml site description. 7 | baseurl: "/tutorials" # the subpath of your site, e.g. /blog 8 | url: "https://interledger.org" # the base hostname & protocol for your site, e.g. http://example.com 9 | twitter_username: interledger 10 | github_username: interledger 11 | interledgerjs_baseurl: https://interledgerjs.github.io/ 12 | 13 | plugins: 14 | - jemoji 15 | -------------------------------------------------------------------------------- /_includes/docs-sidebar.html: -------------------------------------------------------------------------------- 1 | 41 | -------------------------------------------------------------------------------- /_includes/footer.html: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /_includes/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /_includes/nav.html: -------------------------------------------------------------------------------- 1 | 2 | 33 | 34 | -------------------------------------------------------------------------------- /_includes/scripts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /_layouts/page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ page.title }} 5 | {% include head.html %} 6 | 7 | 8 | 9 | {% include nav.html %} 10 | 11 |
12 |
13 | {% include docs-sidebar.html %} 14 |
15 | 16 | {{ content }} 17 | 18 |
19 |
20 |
21 | 22 | {% include footer.html %} 23 | {% include scripts.html %} 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /_layouts/redirect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Interledger 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 29 | 30 | 31 | 32 | 33 |

Redirecting...

34 | Click here if you are not redirected. 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 7.7.1 4 | test: 5 | post: 6 | # Upload code coverage data 7 | - bash <(curl -s https://codecov.io/bash) || echo "Codecov did not collect coverage reports" 8 | deployment: 9 | production: 10 | branch: master 11 | commands: 12 | # Push NPM package if not yet published 13 | - mv npmrc-env .npmrc 14 | - if [ "$(npm show btp-packet version || echo)" != "$(npm ls --depth=-1 2>/dev/null | head -1 | cut -f 1 -d " " | cut -f 2 -d @)" ] ; then npm publish ; fi 15 | general: 16 | artifacts: 17 | - "coverage/lcov-report" 18 | -------------------------------------------------------------------------------- /hosted-ledgers/index.md: -------------------------------------------------------------------------------- 1 | # Hosted Ledgers Tutorial 2 | 3 | ## What you need before you start: 4 | 5 | * complete the [Letter Shop](/tutorials/letter-shop), [http-ilp](/tutorials/http-ilp), and [Streaming Payments](/tutorials/streaming-payments) tutorials first 6 | 7 | ## What you'll learn: 8 | 9 | * how to use a hosted ledger to speed things up 10 | * [Bilateral Transfer Protocol (BTP)](https://interledger.org/rfcs/0023-bilateral-transfer-protocol/draft-2.html) and its relation to ILP 11 | 12 | ## Using a hosted ledger 13 | 14 | Getting one letter per second is not very fast. It would be nice if we could stream the money faster, so that 15 | the content arrives faster! For this, we can add a ledger to the shop. The client opens an account on this ledger, 16 | and then pays for letters from its account at the shop's ledger, which will be much faster 17 | than paying via the XRP ledger. We sometimes call such a private ledger (hosted by one of the two parties in a 18 | business relationship, without any trusted third party) a "trustline". 19 | 20 | There are two types of trustline, symmetrical and asymmetrical: 21 | 22 | > An asymmetrical trustline is a ledger with two account holders, and one of them is also the ledger administrator. 23 | 24 | > A symmetrical trustline is a ledger with two account holders, who collaborate on an equal basis to administer the ledger between them. 25 | 26 | The shop's ledger will expose version 1.0 of the Bilateral Transfer Protocol (BTP), which is an optimization of the Ledger Plugin Interface (LPI) 27 | that we already saw in the Letter Shop tutorial, transported over a WebSocket. 28 | These BTP packets are similar to the objects passed to `plugin.sendTransfer` or `plugin.fulfillCondition`, 29 | although they are a bit more concise, and before they go onto the WebSocket, they are serialized into OER buffers. 30 | 31 | Once a BTP connection has been established between two peers, ILP payments can move back and forth over it in both directions. 32 | In our case though, the ILP receiver (the shop) will be a BTP server, and the ILP sender will be a BTP client. 33 | To learn more about the BTP protocol, read [the BTP spec](https://interledger.org/rfcs/0023-bilateral-transfer-protocol/draft-2.html). 34 | 35 | Thanks to the plugin architecture, we have to change surprisingly little to switch from XRP to BTP: we just include the 36 | `'ilp-plugin-payment-channel-framework'` plugin instead of the `'ilp-plugin-xrp-escrow'` one in plugins.js, and give it the config options 37 | it needs: 38 | 39 | ```js 40 | const HostedLedgerPlugin = require('ilp-plugin-payment-channel-framework') 41 | const ObjStore = require('ilp-plugin-payment-channel-framework/src/model/in-memory-store') 42 | 43 | exports.xrp /* sic */ = { 44 | Customer: function () { 45 | return new HostedLedgerPlugin({ 46 | server: 'btp+ws://:@localhost:9000/' 47 | }) 48 | }, 49 | Shop: function () { 50 | return new HostedLedgerPlugin({ 51 | listener: { 52 | port: 9000 53 | }, 54 | incomingSecret: '', 55 | maxBalance: '1000000000', 56 | prefix: 'example.letter-shop.mytrustline.', 57 | info: { 58 | currencyScale: 9, 59 | currencyCode: 'XRP', 60 | prefix: 'example.letter-shop.mytrustline.', 61 | connectors: [] 62 | }, 63 | _store: new ObjStore() 64 | }) 65 | } 66 | } 67 | ``` 68 | 69 | To run the streaming payments shop and client using this hosted ledger, run `node ./streaming-shop-from-before.js` in one terminal screen, and 70 | `node ./streaming-client-from-before.js` in another. You can experiment with tweaking the number of milliseconds on line 50 of `streaming-client-from-before.js` 71 | down from 1000 to e.g. 100, or even just 10. 72 | 73 | ## What you learned 74 | 75 | We added a BTP-enabled ledger to the shop, so that our content consumption client can receive letters faster. 76 | -------------------------------------------------------------------------------- /hosted-ledgers/plugins.js: -------------------------------------------------------------------------------- 1 | const HostedLedgerPlugin = require('ilp-plugin-payment-channel-framework') 2 | const ObjStore = require('ilp-plugin-payment-channel-framework/src/model/in-memory-store') 3 | 4 | exports.xrp /* sic */ = { 5 | Customer: function () { 6 | return new HostedLedgerPlugin({ 7 | server: 'btp+ws://:@localhost:9000/' 8 | }) 9 | }, 10 | Shop: function () { 11 | return new HostedLedgerPlugin({ 12 | listener: { 13 | port: 9000 14 | }, 15 | incomingSecret: '', 16 | maxBalance: '1000000000', 17 | prefix: 'example.letter-shop.mytrustline.', 18 | info: { 19 | currencyScale: 9, 20 | currencyCode: 'XRP', 21 | prefix: 'example.letter-shop.mytrustline.', 22 | connectors: [] 23 | }, 24 | _store: new ObjStore() 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /hosted-ledgers/streaming-client-from-before.js: -------------------------------------------------------------------------------- 1 | const IlpPacket = require('ilp-packet') 2 | const plugin = require('./plugins.js').xrp.Customer() 3 | const uuid = require('uuid/v4') 4 | const fetch = require('node-fetch') 5 | const crypto = require('crypto') 6 | 7 | function base64url (buf) { 8 | return buf.toString('base64') 9 | .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') 10 | } 11 | 12 | function sha256 (preimage) { 13 | return crypto.createHash('sha256').update(preimage).digest() 14 | } 15 | 16 | function hmac (secret, input) { 17 | return crypto.createHmac('sha256', secret).update(input).digest() 18 | } 19 | 20 | plugin.connect().then(function () { 21 | return fetch('http://localhost:8000/') 22 | }).then(function (res) { 23 | const parts = res.headers.get('Pay').split(' ') 24 | if (parts[0] === 'interledger-psk') { 25 | let paymentId = 0 26 | setInterval(function () { 27 | const destinationAmount = parts[1] 28 | const destinationAddress = parts[2] + '.' + paymentId 29 | const sharedSecret = Buffer.from(parts[3], 'base64') 30 | const ilpPacket = IlpPacket.serializeIlpPayment({ 31 | account: destinationAddress, 32 | amount: destinationAmount, 33 | data: '' 34 | }) 35 | process.stdout.write('.') 36 | const fulfillmentGenerator = hmac(sharedSecret, 'ilp_psk_condition') 37 | const fulfillment = hmac(fulfillmentGenerator, ilpPacket) 38 | const condition = sha256(fulfillment) 39 | plugin.sendTransfer({ 40 | id: uuid(), 41 | from: plugin.getAccount(), 42 | to: destinationAddress, 43 | ledger: plugin.getInfo().prefix, 44 | expiresAt: new Date(new Date().getTime() + 1000000).toISOString(), 45 | amount: destinationAmount, 46 | executionCondition: base64url(condition), 47 | ilp: base64url(ilpPacket) 48 | }) 49 | paymentId++ 50 | }, 1) 51 | } 52 | res.body.pipe(process.stdout) 53 | }) 54 | -------------------------------------------------------------------------------- /hosted-ledgers/streaming-shop-from-before.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const url = require('url') 3 | const crypto = require('crypto') 4 | const plugin = require('./plugins.js').xrp.Shop() 5 | const IlpPacket = require('ilp-packet') 6 | 7 | function base64url (buf) { 8 | return buf.toString('base64') 9 | .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') 10 | } 11 | 12 | function sha256 (preimage) { 13 | return crypto.createHash('sha256').update(preimage).digest() 14 | } 15 | 16 | function hmac (secret, input) { 17 | return crypto.createHmac('sha256', secret).update(input).digest() 18 | } 19 | 20 | let sharedSecrets = {} 21 | const cost = 10 22 | 23 | console.log(`== Starting the shop server == `) 24 | console.log(` 1. Connecting to an account to accept payments...`) 25 | 26 | plugin.connect().then(function () { 27 | // Get ledger and account information from the plugin 28 | const ledgerInfo = plugin.getInfo() 29 | const account = plugin.getAccount() 30 | 31 | console.log(` - Connected to ledger: ${ledgerInfo.prefix}`) 32 | console.log(` -- Account: ${account}`) 33 | console.log(` -- Currency: ${ledgerInfo.currencyCode}`) 34 | console.log(` -- CurrencyScale: ${ledgerInfo.currencyScale}`) 35 | 36 | // Convert our cost (10) into the right format given the ledger scale 37 | const normalizedCost = cost / Math.pow(10, parseInt(ledgerInfo.currencyScale)) 38 | 39 | console.log(` 2. Starting web server to accept requests...`) 40 | console.log(` - Charging ${normalizedCost} ${ledgerInfo.currencyCode}`) 41 | 42 | // Handle incoming web requests 43 | http.createServer(function (req, res) { 44 | // Generate a client ID and a shared secret from which this client 45 | // can derive fulfillment/condition pairs. 46 | const clientId = base64url(crypto.randomBytes(8)) 47 | const sharedSecret = crypto.randomBytes(32) 48 | 49 | // Store the shared secret and the http request context to use when we get paid 50 | sharedSecrets[clientId] = { sharedSecret, res } 51 | 52 | console.log(` - Waiting for payments...`) 53 | 54 | res.writeHead(200, { 55 | Pay: `interledger-psk ${cost} ${account}.${clientId} ${base64url(sharedSecret)}` 56 | }) 57 | // Flush the headers in a first TCP packet: 58 | res.socket.write(res._header) 59 | res._headerSent = true 60 | }).listen(8000, function () { 61 | console.log(` - Listening on http://localhost:8000`) 62 | }) 63 | 64 | // Handle incoming payments 65 | plugin.on('incoming_prepare', function (transfer) { 66 | if (parseInt(transfer.amount) < 10) { 67 | // Transfer amount is incorrect 68 | console.log(` - Payment received for the wrong amount ` + 69 | `(${transfer.amount})... Rejected`) 70 | 71 | const normalizedAmount = transfer.amount / 72 | Math.pow(10, parseInt(ledgerInfo.currencyScale)) 73 | 74 | plugin.rejectIncomingTransfer(transfer.id, { 75 | code: 'F04', 76 | name: 'Insufficient Destination Amount', 77 | message: `Please send at least 10 ${ledgerInfo.currencyCode},` + 78 | `you sent ${normalizedAmount}`, 79 | triggered_by: plugin.getAccount(), 80 | triggered_at: new Date().toISOString(), 81 | forwarded_by: [], 82 | additional_info: {} 83 | }) 84 | return 85 | } 86 | // Generate fulfillment from packet and this client's shared secret 87 | const ilpPacket = Buffer.from(transfer.ilp, 'base64') 88 | const payment = IlpPacket.deserializeIlpPayment(ilpPacket) 89 | const clientId = payment.account.substring(plugin.getAccount().length + 1).split('.')[0] 90 | const secret = sharedSecrets[clientId].sharedSecret 91 | const res = sharedSecrets[clientId].res 92 | 93 | if (!clientId || !secret) { 94 | // We don't have a fulfillment for this condition 95 | console.log(` - Payment received with an unknown condition: ` + 96 | `${transfer.executionCondition}`) 97 | 98 | plugin.rejectIncomingTransfer(transfer.id, { 99 | code: 'F05', 100 | name: 'Wrong Condition', 101 | message: `Unable to fulfill the condition: ` + 102 | `${transfer.executionCondition}`, 103 | triggered_by: plugin.getAccount(), 104 | triggered_at: new Date().toISOString(), 105 | forwarded_by: [], 106 | additional_info: {} 107 | }) 108 | return 109 | } 110 | console.log(` - Calculating hmac; for clientId ${clientId}, the shared secret is ${base64url(secret)}.`) 111 | const fulfillmentGenerator = hmac(secret, 'ilp_psk_condition') 112 | const fulfillment = hmac(fulfillmentGenerator, ilpPacket) 113 | 114 | // Get the letter that we are selling 115 | const letter = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ') 116 | .split('')[(Math.floor(Math.random() * 26))] 117 | 118 | console.log(` - Generated letter (${letter}) `) 119 | res.write(letter) 120 | 121 | console.log(` 4. Accepted payment with condition ` + 122 | `${transfer.executionCondition}.`) 123 | console.log(` - Fulfilling transfer on the ledger ` + 124 | `using fulfillment: ${base64url(fulfillment)}`) 125 | 126 | // The ledger will check if the fulfillment is correct and 127 | // if it was submitted before the transfer's rollback timeout 128 | plugin.fulfillCondition(transfer.id, base64url(fulfillment)) 129 | .catch(function () { 130 | console.log(` - Error fulfilling the transfer`) 131 | }) 132 | console.log(` - Payment complete`) 133 | 134 | }) 135 | }) 136 | -------------------------------------------------------------------------------- /http-ilp/client1.js: -------------------------------------------------------------------------------- 1 | const IlpPacket = require('ilp-packet') 2 | const plugin = require('./plugins.js').xrp.Customer() 3 | const uuid = require('uuid/v4') 4 | const fetch = require('node-fetch') 5 | const crypto = require('crypto') 6 | 7 | function base64url (buf) { 8 | return buf.toString('base64') 9 | .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') 10 | } 11 | 12 | function sha256 (preimage) { 13 | return crypto.createHash('sha256').update(preimage).digest() 14 | } 15 | 16 | function hmac (secret, input) { 17 | return crypto.createHmac('sha256', secret).update(input).digest() 18 | } 19 | 20 | plugin.connect().then(function () { 21 | return fetch('http://localhost:8000/') 22 | }).then(function (res) { 23 | const parts = res.headers.get('Pay').split(' ') 24 | if (parts[0] === 'interledger-psk') { 25 | const paymentId = 0 26 | const destinationAmount = parts[1] 27 | const destinationAddress = parts[2] + '.' + paymentId 28 | const sharedSecret = Buffer.from(parts[3], 'base64') 29 | const ilpPacket = IlpPacket.serializeIlpPayment({ 30 | account: destinationAddress, 31 | amount: destinationAmount, 32 | data: '' 33 | }) 34 | console.log('Calculating hmac using shared secret:', base64url(sharedSecret)) 35 | const fulfillmentGenerator = hmac(sharedSecret, 'ilp_psk_condition') 36 | const fulfillment = hmac(fulfillmentGenerator, ilpPacket) 37 | const condition = sha256(fulfillment) 38 | return plugin.sendTransfer({ 39 | id: uuid(), 40 | from: plugin.getAccount(), 41 | to: destinationAddress, 42 | ledger: plugin.getInfo().prefix, 43 | expiresAt: new Date(new Date().getTime() + 1000000).toISOString(), 44 | amount: destinationAmount, 45 | executionCondition: base64url(condition), 46 | ilp: base64url(ilpPacket) 47 | }) 48 | } 49 | }) 50 | 51 | plugin.on('outgoing_fulfill', function (transferId, fulfillmentBase64) { 52 | fetch('http://localhost:8000/' + fulfillmentBase64).then(function (res) { 53 | return res.text() 54 | }).then(function (body) { 55 | console.log(body) 56 | return plugin.disconnect() 57 | }).then(function () { 58 | process.exit() 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /http-ilp/client2.js: -------------------------------------------------------------------------------- 1 | const IlpPacket = require('ilp-packet') 2 | const plugin = require('./plugins.js').xrp.Customer() 3 | const uuid = require('uuid/v4') 4 | const fetch = require('node-fetch') 5 | const crypto = require('crypto') 6 | 7 | function base64url (buf) { 8 | return buf.toString('base64') 9 | .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') 10 | } 11 | 12 | function sha256 (preimage) { 13 | return crypto.createHash('sha256').update(preimage).digest() 14 | } 15 | 16 | function hmac (secret, input) { 17 | return crypto.createHmac('sha256', secret).update(input).digest() 18 | } 19 | 20 | const sharedSecret = crypto.randomBytes(32) 21 | 22 | plugin.connect().then(function () { 23 | return fetch('http://localhost:8000/', { 24 | headers: { 25 | 'Pay-Token': base64url(sharedSecret) 26 | } 27 | }) 28 | }).then(function (res) { 29 | const parts = res.headers.get('Pay').split(' ') 30 | if (parts[0] === 'interledger-psk') { 31 | const paymentId = 0 32 | const destinationAmount = parts[1] 33 | const destinationAddress = parts[2] + '.' + paymentId 34 | const sharedSecret = Buffer.from(parts[3], 'base64') 35 | const ilpPacket = IlpPacket.serializeIlpPayment({ 36 | account: destinationAddress, 37 | amount: destinationAmount, 38 | data: '' 39 | }) 40 | console.log('Calculating hmac using shared secret:', base64url(sharedSecret)) 41 | const fulfillmentGenerator = hmac(sharedSecret, 'ilp_psk_condition') 42 | const fulfillment = hmac(fulfillmentGenerator, ilpPacket) 43 | const condition = sha256(fulfillment) 44 | return plugin.sendTransfer({ 45 | id: uuid(), 46 | from: plugin.getAccount(), 47 | to: destinationAddress, 48 | ledger: plugin.getInfo().prefix, 49 | expiresAt: new Date(new Date().getTime() + 1000000).toISOString(), 50 | amount: destinationAmount, 51 | executionCondition: base64url(condition), 52 | ilp: base64url(ilpPacket) 53 | }) 54 | } 55 | }) 56 | 57 | plugin.on('outgoing_fulfill', function (transferId, fulfillmentBase64) { 58 | fetch('http://localhost:8000/', { 59 | headers: { 60 | 'Pay-Token': base64url(sharedSecret) 61 | } 62 | }).then(function (res) { 63 | return res.text() 64 | }).then(function (body) { 65 | console.log(body) 66 | return plugin.disconnect() 67 | }).then(function () { 68 | process.exit() 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /http-ilp/client3.js: -------------------------------------------------------------------------------- 1 | const IlpPacket = require('ilp-packet') 2 | const plugin = require('./plugins.js').xrp.Customer() 3 | const uuid = require('uuid/v4') 4 | const fetch = require('node-fetch') 5 | const crypto = require('crypto') 6 | 7 | function base64url (buf) { 8 | return buf.toString('base64') 9 | .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') 10 | } 11 | 12 | function sha256 (preimage) { 13 | return crypto.createHash('sha256').update(preimage).digest() 14 | } 15 | 16 | function hmac (secret, input) { 17 | return crypto.createHmac('sha256', secret).update(input).digest() 18 | } 19 | 20 | // work around https://github.com/interledgerjs/ilp-plugin/pull/1 21 | plugin._prefix = 'g.crypto.ripple.escrow.' 22 | 23 | const sharedSecret = crypto.randomBytes(32) 24 | 25 | plugin.connect().then(function () { 26 | return fetch('http://localhost:8000/', { 27 | headers: { 28 | 'Pay-Token': base64url(sharedSecret) 29 | } 30 | }) 31 | }).then(function (res) { 32 | const parts = res.headers.get('Pay').split(' ') 33 | if (parts.length === 3) { 34 | const paymentId = 0 35 | const destinationAmount = parts[0] 36 | const destinationAddress = parts[1] + '.' + paymentId 37 | const sharedSecret = Buffer.from(parts[2], 'base64') 38 | const ilpPacket = IlpPacket.serializeIlpPayment({ 39 | account: destinationAddress, 40 | amount: destinationAmount, 41 | data: '' 42 | }) 43 | console.log('Calculating hmac using shared secret:', base64url(sharedSecret)) 44 | const fulfillmentGenerator = hmac(sharedSecret, 'ilp_psk_condition') 45 | const fulfillment = hmac(fulfillmentGenerator, ilpPacket) 46 | const condition = sha256(fulfillment) 47 | return plugin.sendTransfer({ 48 | id: uuid(), 49 | from: plugin.getAccount(), 50 | to: destinationAddress, 51 | ledger: plugin.getInfo().prefix, 52 | expiresAt: new Date(new Date().getTime() + 1000000).toISOString(), 53 | amount: destinationAmount, 54 | executionCondition: base64url(condition), 55 | ilp: base64url(ilpPacket) 56 | }) 57 | } 58 | }) 59 | 60 | plugin.on('outgoing_fulfill', function (transferId, fulfillmentBase64) { 61 | fetch('http://localhost:8000/', { 62 | headers: { 63 | 'Pay-Token': base64url(sharedSecret) 64 | } 65 | }).then(function (res) { 66 | return res.text() 67 | }).then(function (body) { 68 | console.log(body) 69 | return plugin.disconnect() 70 | }).then(function () { 71 | process.exit() 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /http-ilp/index.md: -------------------------------------------------------------------------------- 1 | # HTTP with ILP Tutorial 2 | 3 | ## What you need before you start: 4 | 5 | * complete the [Letter Shop](/tutorials/letter-shop) tutorial first, including the Bonus Step 6 | 7 | ## What you'll learn: 8 | 9 | * how to use the Pre-Shared Key (PSK) protocol for repeated conditional Interledger payments 10 | * how to use the ILP curl tool to automatically administer your plugin credentials 11 | * how to use the koa-ilp module in a webserver middleware framework 12 | * how to use a `Pay-Token` to pay for multiple http requests with prepaid balance 13 | 14 | ## PSK 15 | 16 | The `Pay` header we used in the Letter Shop tutorial specifies which sha256 condition to 17 | use for the payment. But once this condition has been used, and its fulfillment (the 18 | preimage of the sha256 hash) has been made public, it cannot be reused. 19 | 20 | Therefore, if the Letter Shop wants to give its customers a way to securely pay for 21 | multiple letters, it can use the [Pre-Shared Secet (PSK)](https://interledger.org/rfcs/0016-pre-shared-key/draft-3.html) 22 | protocol, to give each customer 23 | a unique pre-shared secret, from which endless fulfillment/condition pairs can be derived. 24 | 25 | To do this, the shop needs to use a `Pay` header with the 'interledger-psk' payment method, instead 26 | of the 'interledger-condition' method which we used in the previous tutorial. 27 | 28 | In order to make each client uniquely identifiable, a different `clientId` is added to the shop's 29 | ILP address. This means each client will pay the shop at a different ILP address, 30 | but these payments still all arrive at the shop. The client will then add another identifier 31 | to the end of the destination address, to make each of its multiple payments unique. So the ILP 32 | address of the shop is then made up of ` . < accountId > . < clientId > . < paymentId >`. 33 | 34 | Apart from the payment's destination address, the client will also use the payment's amount 35 | (measured at the destination), and optional 36 | extra data to derive a fulfillment/condition pair from. The amount is expressed as an big-endian unsigned 64-bit 37 | integer. The three bits of data (address, amount, data), are OER-encoded in a specific, deterministic way, 38 | to get a binary string over which we can calculate an hmac. In the client, this process looks as follows: 39 | 40 | ```js 41 | const paymentId = 0 42 | const destinationAmount = parts[1] 43 | const destinationAddress = parts[2] + '.' + paymentId 44 | const sharedSecret = Buffer.from(parts[3], 'base64') 45 | const ilpPacket = IlpPacket.serializeIlpPayment({ 46 | account: destinationAddress, 47 | amount: destinationAmount, 48 | data: '' 49 | }) 50 | console.log('Calculating hmac using shared secret:', base64url(sharedSecret)) 51 | const fulfillmentGenerator = hmac(sharedSecret, 'ilp_psk_condition') 52 | const fulfillment = hmac(fulfillmentGenerator, ilpPacket) 53 | const condition = sha256(fulfillment) 54 | ``` 55 | 56 | The `ilpPacket` is the OER-encoded binary string from which the fulfillment/condition pair is derived. 57 | This process is deterministic, in the sense that if the client sends the ILP packet along with the 58 | payment (which it does), that will allow the shop to see the client's `clientId`, and to derive the 59 | same fulfillment/condition pair as the client did. 60 | 61 | The `paymentId` is still always set to 0 in this tutorial, so we're not really making use of PSK's ability 62 | to derive endless fulfillment/condition pairs from a single shared secret; we're mainly using PSK here because 63 | 'interledger-psk' is a more standard payment method than 'interledger-condition', and more tools are available 64 | for it, as we'll see shortly. 65 | 66 | Later, in the [Streaming Payments](../streaming-payments) tutorial, we will also see how to use multiple payments 67 | from a single PSK secret to get multiple letters. 68 | 69 | Now copy your `plugins.js` file from the Letter Shop tutorial, and then run `node ./shop1.js` in one terminal 70 | screen, and `node ./client1.js` in another, to see this in action! 71 | 72 | ## Using the `Pay-Token` and `Pay-Balance` headers 73 | 74 | The use of PSK in the `Pay` header is a great step forward in our Letter Shop design, because it makes doing multiple 75 | payments to the same shop easier. But we can also make an optimization in the opposite direction: what if you could 76 | pay more than the invoice amount, to obtain a prepaid balance at the shop? If the ledger(s) over which your payment 77 | travels are slow or expensive to use (*cough* bitcoin *cough*), and you know you will probably need to buy more letters 78 | in the future, it could make sense to pay extra, and obtain a balance at the shop, represented by a token. 79 | 80 | To implement this, we first need to decouple the retrieval of the letter from the fulfillment of the payment. So 81 | instead of sending the fulfillment as proof of payment, the client will send its shared secret to prove that they are 82 | the client that has a certain prepaid balance at the shop. And as we change this, at the same time, we'll move it 83 | from the URL path (where the client was putting the base64url-encoded fulfillment), to an http request header, which 84 | we'll call `Pay-Token`. The shop will also add a response header, `Pay-Balance`, which will inform the client of its 85 | current balance. 86 | 87 | Note that in the [Hosted Ledgers](../hosted-ledgers) tutorial, we will see another way of implementing the idea of prepaid 88 | balance at the shop; there, the shop will run an Interledger-enabled ledger, at which the client has a balance. Both 89 | have advantages and disadvantages, as will be discussed in more detail in the hosted-ledgers tutorial. 90 | 91 | ## Customer-generated shared secret 92 | 93 | A final optimization we want to make is to allow the client to pick its own shared secret, instead of getting one 94 | assigned by the shop. This can be useful when, for instance, a user has two devices (one for paying, one for consuming), 95 | and these devices are not connected to each other, but they do have a shared secret between them (for instance, the same 96 | ssh key is installed on both devices). The user can then use one device to put prepaid credit "on" that shared secret at 97 | the shop, and use the other devices to consume the letter. Implementing this is quite simple: the client already sends 98 | a `Pay-Token` header on its first request: 99 | 100 | ```js 101 | const sharedSecret = crypto.randomBytes(32) 102 | 103 | plugin.connect().then(function () { 104 | return fetch('http://localhost:8000/', { 105 | headers: { 106 | 'Pay-Token': base64url(sharedSecret) 107 | } 108 | }) 109 | ``` 110 | 111 | And then instead of generating a shared secret for it, the shop will use that secret 112 | which the client picked. 113 | 114 | ```js 115 | let sharedSecret = crypto.randomBytes(32) 116 | 117 | // Use client-generated shared secret, if presented: 118 | if (req.headers['Pay-Token']) { 119 | sharedSecret = Buffer.from(req.headers['Pay-Token'], 'base64') 120 | console.log('Accepted shared secret from client', req.headers['Pay-Token']) 121 | } 122 | 123 | // Store the shared secret to use when we get paid 124 | ``` 125 | 126 | You can see the changes from this and the previous section implemented in `shop2.js` and `client2.js`. 127 | 128 | # ILP Curl 129 | 130 | So far, in this tutorial, we updated the Letter Shop with three extra improvements: 131 | * use the (repeatable) `'interledger-psk'` payment method instead of the more basic `'interledger-condition'` method 132 | * add prepaid balance for each customer of the shop 133 | * let the client pick the shared secret 134 | 135 | As a reader, you may be asking yourself where all of these small improvements are going, and why we thought it's so important 136 | to add them in this tutorial. The answer is we didn't pick them by accident: they are all things you need to change 137 | to become compatible with the new 'http-ilp' standard. There are two final changes which we need to make in order to become 138 | compatible with the current experimental implementation of http-ilp (which is slightly different from the latest version of 139 | the http-ilp specification at IETF discussions): remove the `'interledger-psk '` string from the `Pay` header, 140 | and use a [different string](https://github.com/interledgerjs/ilp-plugin/pull/1) to identify the XRP testnet ledger 141 | 142 | These last tweaks have been made in `shop3.js` and `client3.js`, and again, 143 | you can try running them to check that it works as expected. 144 | 145 | And now, finally, to prove that our shop is now compatible with other publically available http-ilp tools, run `shop3.js` and then 146 | instead of running `client3.js`, use the ilp-curl tool to buy a letter from the shop: 147 | 148 | ``` 149 | $ npm install -g ilp-curl 150 | $ ilp-curl -X GET localhost:8000 151 | Your letter: A 152 | ``` 153 | 154 | Behind the scenes, ilp-curl does the following: 155 | * get an account on the XRP testnet and save the credentials in your `~/.ilprc.json` if you don't have that file yet 156 | * read your plugin credentials from your `~/.ilprc.json` file (this file replaces `./plugins.js`) 157 | * generate a shared secret for http://localhost:8000 158 | * make an OPTIONS request to http://localhost:8000 to send the shared secret to the shop and learn the price of one letter 159 | * use an Interledger payment to deposit money into the prepaid account at the shop 160 | * retrieve one letter 161 | * print the result 162 | 163 | ## Koa ILP 164 | 165 | Another cool module we want to show you, is koa-ilp. It allows you to rewrite the Letter Shop 166 | so that instead of the built-in `http` library, it will use the more powerful `koa`, which 167 | is a webserver middleware framework which is very popular among frontend developers. 168 | 169 | With Koa, we can simply import a module to make our server charge 170 | money for requests. As you can see, `shop-koa.js` is only a few lines: 171 | 172 | ```js 173 | const plugin = require('./plugins.js').xrp.Shop() 174 | const Koa = require('koa') 175 | const app = new Koa() 176 | const router = require('koa-router')() 177 | 178 | // work around https://github.com/interledgerjs/ilp-plugin/pull/1 179 | plugin._prefix = 'g.crypto.ripple.escrow.' 180 | 181 | // We use the plugin to create a new koa middleware. This allows us to add a 182 | // function to any endpoint that we want to ILP enable. 183 | const KoaIlp = require('koa-ilp') 184 | const ilp = new KoaIlp({ plugin }) 185 | 186 | // On the server's root endpoint, we add this ilp.paid() function, which 187 | // requires payment of 1000 XRP drops (0.001 XRP) in order to run the main 188 | // function code 189 | router.get('/', ilp.paid({ price: 1000 }), async ctx => { 190 | const letter = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ').split('')[(Math.floor(Math.random() * 26))] 191 | console.log('Sending letter:', letter) 192 | ctx.body = 'Your letter: ' + letter 193 | }) 194 | 195 | // Add the route we defined to the application and then listen on port 8000. 196 | app 197 | .use(router.routes()) 198 | .use(router.allowedMethods()) 199 | .listen(8000) 200 | ``` 201 | 202 | Start the new server with: 203 | 204 | ```sh 205 | $ DEBUG=* node shop-koa.js 206 | ``` 207 | 208 | ## Testing our Server 209 | 210 | To give you more of a feel of what is happening between client and shop on the http level, 211 | let's just see what happens when we make a request to our server with `curl`. 212 | 213 | ```sh 214 | $ curl -X GET localhost:8000/ -H Pay-Token:BPtQLNWS7owdlvFlNkMKbVjpBlmvuh1A-V47XdYmeW8 215 | Your Payment Token BPtQLNWS7owdlvFlNkMKbVjpBlmvuh1A-V47XdYmeW8 has no funds available. It needs at least 1000 216 | ``` 217 | 218 | Oh, that's right. We haven't sent any money. We can see the human-readable 219 | message that the server gave back to us, but there's also a machine readable 220 | version that the ILP tools can use, specified in [HTTP-ILP](https://github.com/interledger/rfcs/blob/58d8dcb015b160a381313126fa3065c64406db05/0014-http-ilp/0014-http-ilp.md#http-ilp). 221 | 222 | Let's look at all the headers that came back on the request we just sent. 223 | We can do that by adding the verbose (`-v`) flag to curl. 224 | 225 | ```sh 226 | $ curl -v -X GET localhost:8000/ -H Pay-Token:BPtQLNWS7owdlvFlNkMKbVjpBlmvuh1A-V47XdYmeW8 227 | 228 | * Rebuilt URL to: localhost:8000/ 229 | * Trying ::1... 230 | * TCP_NODELAY set 231 | * Connected to localhost (::1) port 8000 (#0) 232 | > GET / HTTP/1.1 233 | > Host: localhost:8000 234 | > User-Agent: curl/7.51.0 235 | > Accept: */* 236 | > Pay-Token:BPtQLNWS7owdlvFlNkMKbVjpBlmvuh1A-V47XdYmeW8 237 | > 238 | < HTTP/1.1 402 Payment Required 239 | < Pay: 1000 g.crypto.ripple.escrow.rrhnXcox5bEmZfJCHzPxajUtwdt772zrCW.JCOtNQAm8OQlKPHR8dMeJixwfDXdpEQJw BEYMjoXSFQSCKlFRZ6itCQ 240 | < Pay-Balance: 0 241 | < Content-Type: text/plain; charset=utf-8 242 | < Content-Length: 109 243 | < Date: Thu, 26 Oct 2017 18:51:13 GMT 244 | < Connection: keep-alive 245 | < 246 | * Curl_http_done: called premature == 0 247 | * Connection #0 to host localhost left intact 248 | Your Payment Token BPtQLNWS7owdlvFlNkMKbVjpBlmvuh1A-V47XdYmeW8 has no funds available. It needs at least 1000% 249 | ``` 250 | 251 | That's a lot of output. The lines we care about are in the response headers. 252 | They're called `Pay` and `Pay-Balance`. 253 | 254 | ``` 255 | Pay: 1000 g.crypto.ripple.escrow.rrhnXcox5bEmZfJCHzPxajUtwdt772zrCW.JCOtNQAm8OQlKPHR8dMeJixwfDXdpEQJw BEYMjoXSFQSCKlFRZ6itCQ 256 | ``` 257 | 258 | As you can see, the `Pay` header is made up of three portions instead of four now (the payment method identifier `'interledger-psk'` at the beginning is omitted). 259 | 260 | The `Pay-Balance` header tells us how much money is on our token right now. 261 | We've not funded it yet, so the amount is `0`. 262 | 263 | ## What's next 264 | 265 | In the next tutorial, we will see how letters can flow from the shop, as money flows from the client in a stream. We call that [Streaming Payments](/tutorials/streaming-payments) 266 | 267 | ## What you learned 268 | 269 | We learned how to make our Letter Shop compatible with different versions of the newly proposed HTTP-ILP standard. 270 | We learned how to buy a letter with ILP Curl. 271 | Finally, we learned how to use the high-level ILP developer tools to rewrite our Letter Shop using the Koa webserver middleware framework. 272 | -------------------------------------------------------------------------------- /http-ilp/shop-koa.js: -------------------------------------------------------------------------------- 1 | const plugin = require('./plugins.js').xrp.Shop() 2 | const Koa = require('koa') 3 | const app = new Koa() 4 | const router = require('koa-router')() 5 | 6 | // work around https://github.com/interledgerjs/ilp-plugin/pull/1 7 | plugin._prefix = 'g.crypto.ripple.escrow.' 8 | 9 | // We use the plugin to create a new koa middleware. This allows us to add a 10 | // function to any endpoint that we want to ILP enable. 11 | const KoaIlp = require('koa-ilp') 12 | const ilp = new KoaIlp({ plugin }) 13 | 14 | // On the server's root endpoint, we add this ilp.paid() function, which 15 | // requires payment of 1000 XRP drops (0.001 XRP) in order to run the main 16 | // function code 17 | router.get('/', ilp.paid({ price: 1000 }), async ctx => { 18 | const letter = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ').split('')[(Math.floor(Math.random() * 26))] 19 | console.log('Sending letter:', letter) 20 | ctx.body = 'Your letter: ' + letter 21 | }) 22 | 23 | // Add the route we defined to the application and then listen on port 8000. 24 | app 25 | .use(router.routes()) 26 | .use(router.allowedMethods()) 27 | .listen(8000) 28 | -------------------------------------------------------------------------------- /http-ilp/shop1.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const url = require('url') 3 | const crypto = require('crypto') 4 | const plugin = require('./plugins.js').xrp.Shop() 5 | const IlpPacket = require('ilp-packet') 6 | 7 | function base64url (buf) { 8 | return buf.toString('base64') 9 | .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') 10 | } 11 | 12 | function sha256 (preimage) { 13 | return crypto.createHash('sha256').update(preimage).digest() 14 | } 15 | 16 | function hmac (secret, input) { 17 | return crypto.createHmac('sha256', secret).update(input).digest() 18 | } 19 | 20 | let sharedSecrets = {} 21 | let letters = {} 22 | const cost = 10 23 | 24 | console.log(`== Starting the shop server == `) 25 | console.log(` 1. Connecting to an account to accept payments...`) 26 | 27 | plugin.connect().then(function () { 28 | // Get ledger and account information from the plugin 29 | const ledgerInfo = plugin.getInfo() 30 | const account = plugin.getAccount() 31 | 32 | console.log(` - Connected to ledger: ${ledgerInfo.prefix}`) 33 | console.log(` -- Account: ${account}`) 34 | console.log(` -- Currency: ${ledgerInfo.currencyCode}`) 35 | console.log(` -- CurrencyScale: ${ledgerInfo.currencyScale}`) 36 | 37 | // Convert our cost (10) into the right format given the ledger scale 38 | const normalizedCost = cost / Math.pow(10, parseInt(ledgerInfo.currencyScale)) 39 | 40 | console.log(` 2. Starting web server to accept requests...`) 41 | console.log(` - Charging ${normalizedCost} ${ledgerInfo.currencyCode}`) 42 | 43 | // Handle incoming web requests 44 | http.createServer(function (req, res) { 45 | // Browsers are irritiating and often probe for a favicon, just ignore 46 | if (req.url.startsWith(`/favicon.ico`)) { 47 | res.statusCode = 404 48 | res.end() 49 | return 50 | } 51 | 52 | console.log(` - Incoming request to: ${req.url}`) 53 | const requestUrl = url.parse(req.url) 54 | 55 | if (requestUrl.path === `/`) { 56 | // Request for a letter with no attached fulfillment 57 | 58 | // Respond with a 402 HTTP Status Code (Payment Required) 59 | res.statusCode = 402 60 | 61 | // Generate a client ID and a shared secret from which this client 62 | // can derive fulfillment/condition pairs. 63 | const clientId = base64url(crypto.randomBytes(8)) 64 | const sharedSecret = crypto.randomBytes(32) 65 | 66 | // Store the shared secret to use when we get paid 67 | sharedSecrets[clientId] = sharedSecret 68 | 69 | console.log(` - Waiting for payment...`) 70 | 71 | res.setHeader(`Pay`, `interledger-psk ${cost} ${account}.${clientId} ${base64url(sharedSecret)}`) 72 | 73 | res.end(`Please send an Interledger-PSK payment of` + 74 | ` ${normalizedCost} ${ledgerInfo.currencyCode} to ${account}.${clientId}` + 75 | ` using the shared secret ${base64url(sharedSecret)}\n`) 76 | } else { 77 | // Request for a letter with the fulfillment in the path 78 | 79 | // Get fulfillment from the path 80 | const fulfillmentBase64 = requestUrl.path.substring(1) 81 | 82 | // Lookup the letter we stored previously for this fulfillment 83 | const letter = letters[fulfillmentBase64] 84 | 85 | if (!letter) { 86 | // We have no record of a letter that was issued for this fulfillment 87 | 88 | // Respond with a 404 HTTP Status Code (Not Found) 89 | res.statusCode = 404 90 | 91 | console.log(' - No letter found for fulfillment: ' + 92 | fulfillmentBase64) 93 | 94 | res.end(`Unrecognized fulfillment.`) 95 | } else { 96 | // Provide the customer with their letter 97 | res.end(`Your letter: ${letter}`) 98 | 99 | console.log(` 5. Providing paid letter to customer ` + 100 | `for fulfillment ${fulfillmentBase64}`) 101 | } 102 | } 103 | }).listen(8000, function () { 104 | console.log(` - Listening on http://localhost:8000`) 105 | console.log(` 3. Visit http://localhost:8000 in your browser ` + 106 | `to buy a letter`) 107 | }) 108 | 109 | // Handle incoming payments 110 | plugin.on('incoming_prepare', function (transfer) { 111 | if (parseInt(transfer.amount) < 10) { 112 | // Transfer amount is incorrect 113 | console.log(` - Payment received for the wrong amount ` + 114 | `(${transfer.amount})... Rejected`) 115 | 116 | const normalizedAmount = transfer.amount / 117 | Math.pow(10, parseInt(ledgerInfo.currencyScale)) 118 | 119 | plugin.rejectIncomingTransfer(transfer.id, { 120 | code: 'F04', 121 | name: 'Insufficient Destination Amount', 122 | message: `Please send at least 10 ${ledgerInfo.currencyCode},` + 123 | `you sent ${normalizedAmount}`, 124 | triggered_by: plugin.getAccount(), 125 | triggered_at: new Date().toISOString(), 126 | forwarded_by: [], 127 | additional_info: {} 128 | }) 129 | return 130 | } 131 | // Generate fulfillment from packet and this client's shared secret 132 | const ilpPacket = Buffer.from(transfer.ilp, 'base64') 133 | const payment = IlpPacket.deserializeIlpPayment(ilpPacket) 134 | const clientId = payment.account.substring(plugin.getAccount().length + 1).split('.')[0] 135 | const secret = sharedSecrets[clientId] 136 | 137 | if (!clientId || !secret) { 138 | // We don't have a fulfillment for this condition 139 | console.log(` - Payment received with an unknown condition: ` + 140 | `${transfer.executionCondition}`) 141 | 142 | plugin.rejectIncomingTransfer(transfer.id, { 143 | code: 'F05', 144 | name: 'Wrong Condition', 145 | message: `Unable to fulfill the condition: ` + 146 | `${transfer.executionCondition}`, 147 | triggered_by: plugin.getAccount(), 148 | triggered_at: new Date().toISOString(), 149 | forwarded_by: [], 150 | additional_info: {} 151 | }) 152 | return 153 | } 154 | console.log(` - Calculating hmac; for clientId ${clientId}, the shared secret is ${base64url(secret)}.`) 155 | const fulfillmentGenerator = hmac(secret, 'ilp_psk_condition') 156 | const fulfillment = hmac(fulfillmentGenerator, ilpPacket) 157 | 158 | // Get the letter that we are selling 159 | const letter = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ') 160 | .split('')[(Math.floor(Math.random() * 26))] 161 | 162 | console.log(` - Generated letter (${letter}) `) 163 | 164 | // Store the letter (indexed by the fulfillment) to use when the customer 165 | // requests it 166 | letters[base64url(fulfillment)] = letter 167 | 168 | console.log(` 4. Accepted payment with condition ` + 169 | `${transfer.executionCondition}.`) 170 | console.log(` - Fulfilling transfer on the ledger ` + 171 | `using fulfillment: ${base64url(fulfillment)}`) 172 | 173 | // The ledger will check if the fulfillment is correct and 174 | // if it was submitted before the transfer's rollback timeout 175 | plugin.fulfillCondition(transfer.id, base64url(fulfillment)) 176 | .catch(function () { 177 | console.log(` - Error fulfilling the transfer`) 178 | }) 179 | console.log(` - Payment complete`) 180 | 181 | }) 182 | }) 183 | -------------------------------------------------------------------------------- /http-ilp/shop2.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const url = require('url') 3 | const crypto = require('crypto') 4 | const plugin = require('./plugins.js').xrp.Shop() 5 | const IlpPacket = require('ilp-packet') 6 | 7 | function base64url (buf) { 8 | return buf.toString('base64') 9 | .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') 10 | } 11 | 12 | function sha256 (preimage) { 13 | return crypto.createHash('sha256').update(preimage).digest() 14 | } 15 | 16 | function hmac (secret, input) { 17 | return crypto.createHmac('sha256', secret).update(input).digest() 18 | } 19 | 20 | let sharedSecrets = {} 21 | let letters = {} 22 | let balances = {} 23 | 24 | const cost = 10 25 | 26 | console.log(`== Starting the shop server == `) 27 | console.log(` 1. Connecting to an account to accept payments...`) 28 | 29 | plugin.connect().then(function () { 30 | // Get ledger and account information from the plugin 31 | const ledgerInfo = plugin.getInfo() 32 | const account = plugin.getAccount() 33 | 34 | console.log(` - Connected to ledger: ${ledgerInfo.prefix}`) 35 | console.log(` -- Account: ${account}`) 36 | console.log(` -- Currency: ${ledgerInfo.currencyCode}`) 37 | console.log(` -- CurrencyScale: ${ledgerInfo.currencyScale}`) 38 | 39 | // Convert our cost (10) into the right format given the ledger scale 40 | const normalizedCost = cost / Math.pow(10, parseInt(ledgerInfo.currencyScale)) 41 | 42 | console.log(` 2. Starting web server to accept requests...`) 43 | console.log(` - Charging ${normalizedCost} ${ledgerInfo.currencyCode}`) 44 | 45 | // Handle incoming web requests 46 | http.createServer(function (req, res) { 47 | // Browsers are irritiating and often probe for a favicon, just ignore 48 | if (req.url.startsWith(`/favicon.ico`)) { 49 | res.statusCode = 404 50 | res.end() 51 | return 52 | } 53 | 54 | console.log(` - Incoming request to: ${req.url}`) 55 | const requestUrl = url.parse(req.url) 56 | 57 | if (requestUrl.path === `/`) { 58 | // Request for a letter with no attached fulfillment 59 | 60 | // Generate a client ID and a shared secret from which this client 61 | // can derive fulfillment/condition pairs. 62 | const clientId = base64url(crypto.randomBytes(8)) 63 | let sharedSecret = crypto.randomBytes(32) 64 | console.log('request headers', req.headers) 65 | // Use client-generated shared secret, if presented: 66 | if (req.headers['pay-token']) { 67 | sharedSecret = Buffer.from(req.headers['pay-token'], 'base64') 68 | console.log('Accepted shared secret from client', req.headers['pay-token'], balances) 69 | if (balances[base64url(sharedSecret)]) { 70 | // This code path is now also used to deliver the letter after the client paid: 71 | if (balances[base64url(sharedSecret)] >= cost) { 72 | // Get the letter that we are selling 73 | const letter = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ') 74 | .split('')[(Math.floor(Math.random() * 26))] 75 | balances[base64url(sharedSecret)] -= cost 76 | res.end('Your letter: ' + letter) 77 | return 78 | } 79 | } 80 | } 81 | 82 | // Store the shared secret to use when we get paid 83 | sharedSecrets[clientId] = sharedSecret 84 | if (!balances[base64url(sharedSecret)]) { 85 | // The client is just establishing its prepaid account, but hasn't paid yet 86 | balances[base64url(sharedSecret)] = 0 87 | } 88 | 89 | console.log(` - Waiting for payment...`) 90 | 91 | // Respond with a 402 HTTP Status Code (Payment Required) 92 | res.statusCode = 402 93 | res.setHeader(`Pay`, `interledger-psk ${cost} ${account}.${clientId} ${base64url(sharedSecret)}`) 94 | res.setHeader(`Pay-Balance`, balances[base64url(sharedSecret)].toString()) 95 | 96 | res.end(`Please send an Interledger-PSK payment of` + 97 | ` ${normalizedCost} ${ledgerInfo.currencyCode} to ${account}.${clientId}` + 98 | ` using the shared secret ${base64url(sharedSecret)}\n`) 99 | } 100 | }).listen(8000, function () { 101 | console.log(` - Listening on http://localhost:8000`) 102 | console.log(` 3. Visit http://localhost:8000 in your browser ` + 103 | `to buy a letter`) 104 | }) 105 | 106 | // Handle incoming payments 107 | plugin.on('incoming_prepare', function (transfer) { 108 | // Generate fulfillment from packet and this client's shared secret 109 | const ilpPacket = Buffer.from(transfer.ilp, 'base64') 110 | const payment = IlpPacket.deserializeIlpPayment(ilpPacket) 111 | const clientId = payment.account.substring(plugin.getAccount().length + 1).split('.')[0] 112 | const secret = sharedSecrets[clientId] 113 | 114 | if (!clientId || !secret) { 115 | // We don't have a fulfillment for this condition 116 | console.log(` - Payment received with an unknown condition: ` + 117 | `${transfer.executionCondition}`) 118 | 119 | plugin.rejectIncomingTransfer(transfer.id, { 120 | code: 'F05', 121 | name: 'Wrong Condition', 122 | message: `Unable to fulfill the condition: ` + 123 | `${transfer.executionCondition}`, 124 | triggered_by: plugin.getAccount(), 125 | triggered_at: new Date().toISOString(), 126 | forwarded_by: [], 127 | additional_info: {} 128 | }) 129 | return 130 | } 131 | console.log(` - Calculating hmac; for clientId ${clientId}, the shared secret is ${base64url(secret)}.`) 132 | const fulfillmentGenerator = hmac(secret, 'ilp_psk_condition') 133 | const fulfillment = hmac(fulfillmentGenerator, ilpPacket) 134 | 135 | // Increase this client's balance 136 | balances[base64url(secret)] += parseInt(transfer.amount) 137 | 138 | console.log(` - Increase balance for shared secret ${base64url(secret)} with ${transfer.amount} to ${balances[base64url(secret)]}. `) 139 | 140 | console.log(` 4. Accepted payment with condition ` + 141 | `${transfer.executionCondition}.`) 142 | console.log(` - Fulfilling transfer on the ledger ` + 143 | `using fulfillment: ${base64url(fulfillment)}`) 144 | 145 | // The ledger will check if the fulfillment is correct and 146 | // if it was submitted before the transfer's rollback timeout 147 | plugin.fulfillCondition(transfer.id, base64url(fulfillment)) 148 | .catch(function () { 149 | console.log(` - Error fulfilling the transfer`) 150 | }) 151 | console.log(` - Payment complete`) 152 | 153 | }) 154 | }) 155 | -------------------------------------------------------------------------------- /http-ilp/shop3.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const url = require('url') 3 | const crypto = require('crypto') 4 | const plugin = require('./plugins.js').xrp.Shop() 5 | const IlpPacket = require('ilp-packet') 6 | 7 | function base64url (buf) { 8 | return buf.toString('base64') 9 | .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') 10 | } 11 | 12 | function sha256 (preimage) { 13 | return crypto.createHash('sha256').update(preimage).digest() 14 | } 15 | 16 | function hmac (secret, input) { 17 | return crypto.createHmac('sha256', secret).update(input).digest() 18 | } 19 | 20 | let sharedSecrets = {} 21 | let letters = {} 22 | let balances = {} 23 | 24 | const cost = 10 25 | 26 | // work around https://github.com/interledgerjs/ilp-plugin/pull/1 27 | plugin._prefix = 'g.crypto.ripple.escrow.' 28 | 29 | console.log(`== Starting the shop server == `) 30 | console.log(` 1. Connecting to an account to accept payments...`) 31 | 32 | plugin.connect().then(function () { 33 | // Get ledger and account information from the plugin 34 | const ledgerInfo = plugin.getInfo() 35 | const account = plugin.getAccount() 36 | 37 | console.log(` - Connected to ledger: ${ledgerInfo.prefix}`) 38 | console.log(` -- Account: ${account}`) 39 | console.log(` -- Currency: ${ledgerInfo.currencyCode}`) 40 | console.log(` -- CurrencyScale: ${ledgerInfo.currencyScale}`) 41 | 42 | // Convert our cost (10) into the right format given the ledger scale 43 | const normalizedCost = cost / Math.pow(10, parseInt(ledgerInfo.currencyScale)) 44 | 45 | console.log(` 2. Starting web server to accept requests...`) 46 | console.log(` - Charging ${normalizedCost} ${ledgerInfo.currencyCode}`) 47 | 48 | // Handle incoming web requests 49 | http.createServer(function (req, res) { 50 | // Browsers are irritiating and often probe for a favicon, just ignore 51 | if (req.url.startsWith(`/favicon.ico`)) { 52 | res.statusCode = 404 53 | res.end() 54 | return 55 | } 56 | 57 | console.log(` - Incoming request to: ${req.url}`) 58 | const requestUrl = url.parse(req.url) 59 | 60 | if (requestUrl.path === `/`) { 61 | // Request for a letter with no attached fulfillment 62 | 63 | // Generate a client ID and a shared secret from which this client 64 | // can derive fulfillment/condition pairs. 65 | const clientId = base64url(crypto.randomBytes(8)) 66 | let sharedSecret = crypto.randomBytes(32) 67 | console.log('request headers', req.headers) 68 | // Use client-generated shared secret, if presented: 69 | if (req.headers['pay-token']) { 70 | sharedSecret = Buffer.from(req.headers['pay-token'], 'base64') 71 | console.log('Accepted shared secret from client', req.headers['pay-token'], balances) 72 | if (balances[base64url(sharedSecret)]) { 73 | // This code path is now also used to deliver the letter after the client paid: 74 | if (balances[base64url(sharedSecret)] >= cost) { 75 | // Get the letter that we are selling 76 | const letter = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ') 77 | .split('')[(Math.floor(Math.random() * 26))] 78 | balances[base64url(sharedSecret)] -= cost 79 | res.end('Your letter: ' + letter) 80 | return 81 | } 82 | } 83 | } 84 | 85 | // Store the shared secret to use when we get paid 86 | sharedSecrets[clientId] = sharedSecret 87 | if (!balances[base64url(sharedSecret)]) { 88 | // The client is just establishing its prepaid account, but hasn't paid yet 89 | balances[base64url(sharedSecret)] = 0 90 | } 91 | 92 | console.log(` - Waiting for payment...`) 93 | 94 | // Respond with a 402 HTTP Status Code (Payment Required) 95 | res.statusCode = 402 96 | // res.setHeader(`Pay`, `interledger-psk ${cost} ${account}.${clientId} ${base64url(sharedSecret)}`) 97 | res.setHeader(`Pay`, `${cost} ${account}.${clientId} ${base64url(sharedSecret)}`) 98 | res.setHeader(`Pay-Balance`, balances[base64url(sharedSecret)].toString()) 99 | 100 | res.end(`Please send an Interledger-PSK payment of` + 101 | ` ${normalizedCost} ${ledgerInfo.currencyCode} to ${account}.${clientId}` + 102 | ` using the shared secret ${base64url(sharedSecret)}\n`) 103 | } 104 | }).listen(8000, function () { 105 | console.log(` - Listening on http://localhost:8000`) 106 | console.log(` 3. Visit http://localhost:8000 in your browser ` + 107 | `to buy a letter`) 108 | }) 109 | 110 | // Handle incoming payments 111 | plugin.on('incoming_prepare', function (transfer) { 112 | // Generate fulfillment from packet and this client's shared secret 113 | const ilpPacket = Buffer.from(transfer.ilp, 'base64') 114 | const payment = IlpPacket.deserializeIlpPayment(ilpPacket) 115 | const clientId = payment.account.substring(plugin.getAccount().length + 1).split('.')[0] 116 | const secret = sharedSecrets[clientId] 117 | 118 | if (!clientId || !secret) { 119 | // We don't have a fulfillment for this condition 120 | console.log(` - Payment received with an unknown condition: ` + 121 | `${transfer.executionCondition}`) 122 | 123 | plugin.rejectIncomingTransfer(transfer.id, { 124 | code: 'F05', 125 | name: 'Wrong Condition', 126 | message: `Unable to fulfill the condition: ` + 127 | `${transfer.executionCondition}`, 128 | triggered_by: plugin.getAccount(), 129 | triggered_at: new Date().toISOString(), 130 | forwarded_by: [], 131 | additional_info: {} 132 | }) 133 | return 134 | } 135 | console.log(` - Calculating hmac; for clientId ${clientId}, the shared secret is ${base64url(secret)}.`) 136 | const fulfillmentGenerator = hmac(secret, 'ilp_psk_condition') 137 | const fulfillment = hmac(fulfillmentGenerator, ilpPacket) 138 | 139 | // Increase this client's balance 140 | balances[base64url(secret)] += parseInt(transfer.amount) 141 | 142 | console.log(` - Increase balance for shared secret ${base64url(secret)} with ${transfer.amount} to ${balances[base64url(secret)]}. `) 143 | 144 | console.log(` 4. Accepted payment with condition ` + 145 | `${transfer.executionCondition}.`) 146 | console.log(` - Fulfilling transfer on the ledger ` + 147 | `using fulfillment: ${base64url(fulfillment)}`) 148 | 149 | // The ledger will check if the fulfillment is correct and 150 | // if it was submitted before the transfer's rollback timeout 151 | plugin.fulfillCondition(transfer.id, base64url(fulfillment)) 152 | .catch(function () { 153 | console.log(` - Error fulfilling the transfer`) 154 | }) 155 | console.log(` - Payment complete`) 156 | 157 | }) 158 | }) 159 | -------------------------------------------------------------------------------- /index.md: -------------------------------------------------------------------------------- 1 | # Welcome to the Interledger Tutorials Collection! 2 | 3 | The goal of this collection of tutorials is to help developers 4 | on their way when implementing Interledger-compatible software for the 5 | first time. The main programming language used is JavaScript. 6 | 7 | ## Tutorials 8 | 9 | * [The Letter Shop](../tutorials/letter-shop) 10 | * [HTTP ILP](../tutorials/http-ilp) 11 | * [Streaming Payments](../tutorials/streaming-payments) 12 | * [Hosted Ledgers](../tutorials/hosted-ledgers) 13 | 14 | ## Versioning 15 | 16 | During 2017, the Interledger protocol stack is finally starting to settle 17 | down and consolidate. These tutorials were written in September of that year, 18 | so you will learn the Interledger protocol stack as described in the following 19 | Interledger Requests For Comments (IL-RFCs): 20 | 21 | * [IL-RFC-1, draft 1](https://interledger.org/rfcs/0001-interledger-architecture/draft-1.html): Interledger Architecture 22 | * [IL-RFC-3, draft 3](https://interledger.org/rfcs/0003-interledger-protocol/draft-3.html): Interledger Protocol 23 | * [IL-RFC-15, draft 1](https://interledger.org/rfcs/0015-ilp-addresses/draft-1.html): Interledger Addresses 24 | * [IL-RFC-22, draft 1](https://interledger.org/rfcs/0022-hashed-timelock-agreements/draft-1.html): Hashed Time Lock Agreements 25 | * [IL-RFC-19, draft 1](https://interledger.org/rfcs/0019-glossary/draft-1.html): Glossary 26 | * [IL-RFC-16, draft 3](https://interledger.org/rfcs/0016-pre-shared-key/draft-3.html): Pre-Shared Key (PSK) 27 | * [IL-RFC-23, draft 2](https://interledger.org/rfcs/0023-bilateral-transfer-protocol/draft-2.html): Bilateral Transfer Protocol (BTP) 28 | * [rfcs PR #319](https://github.com/interledger/rfcs/pull/319): Simplified http-ilp flow 29 | 30 | The software you will build during these tutorials will be compatible with software 31 | written by other developers, on several levels: 32 | 33 | * Ledger Plugins will expose the Ledger Plugin Interface (LPI) as described in [IL-RFC-4, draft 8](https://interledger.org/rfcs/0004-ledger-plugin-interface/draft-8.html). 34 | * Websites that (like in the Streaming Payments tutorial) include request for payment in their HTTP responses will expose the `Pay` header as proposed in https://github.com/interledger/rfcs/issues/307 35 | * Publically accessible websites that (like in the Letter Shop tutorial) print a human-readable payment request, will include an Interledger address in there that is reachable from [Amundsen](https://amundsen.michielbdejong.com/), the bootstrap node for the Interledger testnet-of-testnets. The amount in there will be a stringified positive Integer, and the condition will be encoded using [URL-safe base64](https://github.com/interledger/tutorials/blob/dcde0af71854fc15c38a209a53f43263967287db/shop.js#L4). 36 | * On-ledger transfers for XRP will use [Interledger-over-XRP version 17q3](https://github.com/interledger/interledger/wiki/Interledger-over-XRP/16d6ad581ea29b510aeb937277bc691e497cf288) 37 | * On-ledger transfers for ETH will use [Interledger-over-ETH version 17q3](https://github.com/interledger/interledger/wiki/Interledger-over-ETH/c85abcda1c8ad39f7830584ace6098dab0c90baf) 38 | * Unless agreed otherwise, nodes will peer with each other using [Interledger-over-BTP version 17q4](https://github.com/interledger/interledger/wiki/Interledger-over-BTP/58b4197521b39aa69cc922000ad4daca823fcc48), filling in the `vouch`, `ccp` and `paychan` protocols from version 17q3. 39 | -------------------------------------------------------------------------------- /letter-shop/README.md: -------------------------------------------------------------------------------- 1 | # Letter Shop Tutorial 2 | 3 | This is the letter shop tutorial code. You can view the tutorial on the web at https://interledger.org/tutorials/letter-shop or on GitHub [here](./index.md). -------------------------------------------------------------------------------- /letter-shop/client.js: -------------------------------------------------------------------------------- 1 | const IlpPacket = require('ilp-packet') 2 | const plugin = require('./plugins.js').xrp.Customer() 3 | const uuid = require('uuid/v4') 4 | const fetch = require('node-fetch') 5 | 6 | function base64url (buf) { 7 | return buf.toString('base64') 8 | .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') 9 | } 10 | 11 | plugin.connect().then(function () { 12 | return fetch('http://localhost:8000/') 13 | }).then(function (res) { 14 | return res.text() 15 | }).then(function (body) { 16 | const parts = body.split(' ') 17 | if (parts[0] === 'Please') { 18 | const destinationAddress = parts[16] 19 | const destinationAmount = parts[17] 20 | const condition = parts[18] 21 | return plugin.sendTransfer({ 22 | id: uuid(), 23 | from: plugin.getAccount(), 24 | to: destinationAddress, 25 | ledger: plugin.getInfo().prefix, 26 | expiresAt: new Date(new Date().getTime() + 1000000).toISOString(), 27 | amount: destinationAmount, 28 | executionCondition: condition, 29 | ilp: base64url(IlpPacket.serializeIlpPayment({ 30 | account: destinationAddress, 31 | amount: destinationAmount, 32 | data: '' 33 | })) 34 | }) 35 | } 36 | }) 37 | 38 | plugin.on('outgoing_fulfill', function (transferId, fulfillmentBase64) { 39 | fetch('http://localhost:8000/' + fulfillmentBase64).then(function (res) { 40 | return res.text() 41 | }).then(function (body) { 42 | console.log(body) 43 | return plugin.disconnect() 44 | }).then(function () { 45 | process.exit() 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /letter-shop/completed/client.js: -------------------------------------------------------------------------------- 1 | const IlpPacket = require('ilp-packet') 2 | const plugin = require('./plugins.js').xrp.Customer() 3 | const uuid = require('uuid/v4') 4 | const fetch = require('node-fetch') 5 | 6 | function base64url (buf) { 7 | return buf.toString('base64') 8 | .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') 9 | } 10 | 11 | plugin.connect().then(function () { 12 | return fetch('http://localhost:8000/') 13 | }).then(function (res) { 14 | const parts = res.headers.get('Pay').split(' ') 15 | if (parts[0] === 'interledger-condition') { 16 | const destinationAmount = parts[1] 17 | const destinationAddress = parts[2] 18 | const condition = parts[3] 19 | return plugin.sendTransfer({ 20 | id: uuid(), 21 | from: plugin.getAccount(), 22 | to: destinationAddress, 23 | ledger: plugin.getInfo().prefix, 24 | expiresAt: new Date(new Date().getTime() + 1000000).toISOString(), 25 | amount: destinationAmount, 26 | executionCondition: condition, 27 | ilp: base64url(IlpPacket.serializeIlpPayment({ 28 | account: destinationAddress, 29 | amount: destinationAmount, 30 | data: '' 31 | })) 32 | }) 33 | } 34 | }) 35 | 36 | plugin.on('outgoing_fulfill', function (transferId, fulfillmentBase64) { 37 | fetch('http://localhost:8000/' + fulfillmentBase64).then(function (res) { 38 | return res.text() 39 | }).then(function (body) { 40 | console.log(body) 41 | return plugin.disconnect() 42 | }).then(function () { 43 | process.exit() 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /letter-shop/completed/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "letter-shop-tutorial", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/node": { 8 | "version": "6.0.90", 9 | "resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.90.tgz", 10 | "integrity": "sha512-tXoGRVdi7wZX7P1VWoV9Wfk0uYDOAHdEYXAttuWgSrN76Q32wQlSrMX0Rgyv3RTEaQY2ZLQrzYHVM2e8rfo8sA==" 11 | }, 12 | "acorn": { 13 | "version": "5.1.2", 14 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.1.2.tgz", 15 | "integrity": "sha512-o96FZLJBPY1lvTuJylGA9Bk3t/GKPPJG8H0ydQQl01crzwJgspa4AEIq/pVTXigmK0PHVQhiAtn8WMBLL9D2WA==", 16 | "dev": true 17 | }, 18 | "acorn-jsx": { 19 | "version": "3.0.1", 20 | "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", 21 | "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", 22 | "dev": true, 23 | "requires": { 24 | "acorn": "3.3.0" 25 | }, 26 | "dependencies": { 27 | "acorn": { 28 | "version": "3.3.0", 29 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", 30 | "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", 31 | "dev": true 32 | } 33 | } 34 | }, 35 | "ajv": { 36 | "version": "5.3.0", 37 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.3.0.tgz", 38 | "integrity": "sha1-RBT/dKUIecII7l/cgm4ywwNUnto=", 39 | "dev": true, 40 | "requires": { 41 | "co": "4.6.0", 42 | "fast-deep-equal": "1.0.0", 43 | "fast-json-stable-stringify": "2.0.0", 44 | "json-schema-traverse": "0.3.1" 45 | } 46 | }, 47 | "ajv-keywords": { 48 | "version": "2.1.0", 49 | "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.0.tgz", 50 | "integrity": "sha1-opbhf3v658HOT34N5T0pyzIWLfA=", 51 | "dev": true 52 | }, 53 | "ansi-escapes": { 54 | "version": "3.0.0", 55 | "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.0.0.tgz", 56 | "integrity": "sha512-O/klc27mWNUigtv0F8NJWbLF00OcegQalkqKURWdosW08YZKi4m6CnSUSvIZG1otNJbTWhN01Hhz389DW7mvDQ==", 57 | "dev": true 58 | }, 59 | "ansi-regex": { 60 | "version": "2.1.1", 61 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 62 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", 63 | "dev": true 64 | }, 65 | "ansi-styles": { 66 | "version": "2.2.1", 67 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", 68 | "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", 69 | "dev": true 70 | }, 71 | "argparse": { 72 | "version": "1.0.9", 73 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", 74 | "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", 75 | "dev": true, 76 | "requires": { 77 | "sprintf-js": "1.0.3" 78 | } 79 | }, 80 | "array-union": { 81 | "version": "1.0.2", 82 | "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", 83 | "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", 84 | "dev": true, 85 | "requires": { 86 | "array-uniq": "1.0.3" 87 | } 88 | }, 89 | "array-uniq": { 90 | "version": "1.0.3", 91 | "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", 92 | "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", 93 | "dev": true 94 | }, 95 | "arrify": { 96 | "version": "1.0.1", 97 | "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", 98 | "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", 99 | "dev": true 100 | }, 101 | "assertion-error": { 102 | "version": "1.0.2", 103 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", 104 | "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw=", 105 | "dev": true 106 | }, 107 | "babel-code-frame": { 108 | "version": "6.26.0", 109 | "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz", 110 | "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", 111 | "dev": true, 112 | "requires": { 113 | "chalk": "1.1.3", 114 | "esutils": "2.0.2", 115 | "js-tokens": "3.0.2" 116 | }, 117 | "dependencies": { 118 | "chalk": { 119 | "version": "1.1.3", 120 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", 121 | "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", 122 | "dev": true, 123 | "requires": { 124 | "ansi-styles": "2.2.1", 125 | "escape-string-regexp": "1.0.5", 126 | "has-ansi": "2.0.0", 127 | "strip-ansi": "3.0.1", 128 | "supports-color": "2.0.0" 129 | } 130 | }, 131 | "strip-ansi": { 132 | "version": "3.0.1", 133 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", 134 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 135 | "dev": true, 136 | "requires": { 137 | "ansi-regex": "2.1.1" 138 | } 139 | } 140 | } 141 | }, 142 | "balanced-match": { 143 | "version": "1.0.0", 144 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 145 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 146 | "dev": true 147 | }, 148 | "base64url": { 149 | "version": "2.0.0", 150 | "resolved": "https://registry.npmjs.org/base64url/-/base64url-2.0.0.tgz", 151 | "integrity": "sha1-6sFuA+oUOO/5Qj1puqNiYu0fcLs=" 152 | }, 153 | "base64url-adhoc": { 154 | "version": "2.0.1", 155 | "resolved": "https://registry.npmjs.org/base64url-adhoc/-/base64url-adhoc-2.0.1.tgz", 156 | "integrity": "sha1-j5bFIlgHi6yEWziVCF20vUuth+I=", 157 | "requires": { 158 | "@types/node": "6.0.90" 159 | } 160 | }, 161 | "bignumber.js": { 162 | "version": "4.1.0", 163 | "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-4.1.0.tgz", 164 | "integrity": "sha512-eJzYkFYy9L4JzXsbymsFn3p54D+llV27oTQ+ziJG7WFRheJcNZilgVXMG0LoZtlQSKBsJdWtLFqOD0u+U0jZKA==" 165 | }, 166 | "brace-expansion": { 167 | "version": "1.1.8", 168 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", 169 | "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", 170 | "dev": true, 171 | "requires": { 172 | "balanced-match": "1.0.0", 173 | "concat-map": "0.0.1" 174 | } 175 | }, 176 | "browser-stdout": { 177 | "version": "1.3.0", 178 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", 179 | "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", 180 | "dev": true 181 | }, 182 | "builtin-modules": { 183 | "version": "1.1.1", 184 | "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", 185 | "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", 186 | "dev": true 187 | }, 188 | "caller-path": { 189 | "version": "0.1.0", 190 | "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", 191 | "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", 192 | "dev": true, 193 | "requires": { 194 | "callsites": "0.2.0" 195 | } 196 | }, 197 | "callsites": { 198 | "version": "0.2.0", 199 | "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", 200 | "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", 201 | "dev": true 202 | }, 203 | "chai": { 204 | "version": "4.1.2", 205 | "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", 206 | "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", 207 | "dev": true, 208 | "requires": { 209 | "assertion-error": "1.0.2", 210 | "check-error": "1.0.2", 211 | "deep-eql": "3.0.1", 212 | "get-func-name": "2.0.0", 213 | "pathval": "1.1.0", 214 | "type-detect": "4.0.3" 215 | } 216 | }, 217 | "chalk": { 218 | "version": "2.3.0", 219 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", 220 | "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", 221 | "dev": true, 222 | "requires": { 223 | "ansi-styles": "3.2.0", 224 | "escape-string-regexp": "1.0.5", 225 | "supports-color": "4.5.0" 226 | }, 227 | "dependencies": { 228 | "ansi-styles": { 229 | "version": "3.2.0", 230 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", 231 | "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", 232 | "dev": true, 233 | "requires": { 234 | "color-convert": "1.9.0" 235 | } 236 | }, 237 | "supports-color": { 238 | "version": "4.5.0", 239 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", 240 | "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", 241 | "dev": true, 242 | "requires": { 243 | "has-flag": "2.0.0" 244 | } 245 | } 246 | } 247 | }, 248 | "check-error": { 249 | "version": "1.0.2", 250 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", 251 | "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", 252 | "dev": true 253 | }, 254 | "circular-json": { 255 | "version": "0.3.3", 256 | "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", 257 | "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", 258 | "dev": true 259 | }, 260 | "cli-cursor": { 261 | "version": "2.1.0", 262 | "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", 263 | "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", 264 | "dev": true, 265 | "requires": { 266 | "restore-cursor": "2.0.0" 267 | } 268 | }, 269 | "cli-width": { 270 | "version": "2.2.0", 271 | "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", 272 | "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", 273 | "dev": true 274 | }, 275 | "co": { 276 | "version": "4.6.0", 277 | "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", 278 | "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", 279 | "dev": true 280 | }, 281 | "color-convert": { 282 | "version": "1.9.0", 283 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.0.tgz", 284 | "integrity": "sha1-Gsz5fdc5uYO/mU1W/sj5WFNkG3o=", 285 | "dev": true, 286 | "requires": { 287 | "color-name": "1.1.3" 288 | } 289 | }, 290 | "color-name": { 291 | "version": "1.1.3", 292 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 293 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 294 | "dev": true 295 | }, 296 | "commander": { 297 | "version": "2.9.0", 298 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", 299 | "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", 300 | "dev": true, 301 | "requires": { 302 | "graceful-readlink": "1.0.1" 303 | } 304 | }, 305 | "concat-map": { 306 | "version": "0.0.1", 307 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 308 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 309 | "dev": true 310 | }, 311 | "concat-stream": { 312 | "version": "1.6.0", 313 | "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", 314 | "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", 315 | "dev": true, 316 | "requires": { 317 | "inherits": "2.0.3", 318 | "readable-stream": "2.3.3", 319 | "typedarray": "0.0.6" 320 | } 321 | }, 322 | "contains-path": { 323 | "version": "0.1.0", 324 | "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", 325 | "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", 326 | "dev": true 327 | }, 328 | "core-util-is": { 329 | "version": "1.0.2", 330 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 331 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", 332 | "dev": true 333 | }, 334 | "cross-spawn": { 335 | "version": "5.1.0", 336 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", 337 | "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", 338 | "dev": true, 339 | "requires": { 340 | "lru-cache": "4.1.1", 341 | "shebang-command": "1.2.0", 342 | "which": "1.3.0" 343 | } 344 | }, 345 | "debug": { 346 | "version": "2.6.9", 347 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 348 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 349 | "dev": true, 350 | "requires": { 351 | "ms": "2.0.0" 352 | } 353 | }, 354 | "deep-eql": { 355 | "version": "3.0.1", 356 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", 357 | "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", 358 | "dev": true, 359 | "requires": { 360 | "type-detect": "4.0.3" 361 | } 362 | }, 363 | "deep-is": { 364 | "version": "0.1.3", 365 | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", 366 | "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", 367 | "dev": true 368 | }, 369 | "del": { 370 | "version": "2.2.2", 371 | "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", 372 | "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", 373 | "dev": true, 374 | "requires": { 375 | "globby": "5.0.0", 376 | "is-path-cwd": "1.0.0", 377 | "is-path-in-cwd": "1.0.0", 378 | "object-assign": "4.1.1", 379 | "pify": "2.3.0", 380 | "pinkie-promise": "2.0.1", 381 | "rimraf": "2.6.2" 382 | } 383 | }, 384 | "diff": { 385 | "version": "3.2.0", 386 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz", 387 | "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=", 388 | "dev": true 389 | }, 390 | "doctrine": { 391 | "version": "2.0.0", 392 | "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.0.tgz", 393 | "integrity": "sha1-xz2NKQnSIpHhoAejlYBNqLZl/mM=", 394 | "dev": true, 395 | "requires": { 396 | "esutils": "2.0.2", 397 | "isarray": "1.0.0" 398 | } 399 | }, 400 | "encoding": { 401 | "version": "0.1.12", 402 | "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", 403 | "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", 404 | "requires": { 405 | "iconv-lite": "0.4.19" 406 | } 407 | }, 408 | "error-ex": { 409 | "version": "1.3.1", 410 | "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", 411 | "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", 412 | "dev": true, 413 | "requires": { 414 | "is-arrayish": "0.2.1" 415 | } 416 | }, 417 | "escape-string-regexp": { 418 | "version": "1.0.5", 419 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 420 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 421 | "dev": true 422 | }, 423 | "eslint": { 424 | "version": "4.9.0", 425 | "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.9.0.tgz", 426 | "integrity": "sha1-doedJ0BoJhsZH+Dy9Wx0wvQgjos=", 427 | "dev": true, 428 | "requires": { 429 | "ajv": "5.3.0", 430 | "babel-code-frame": "6.26.0", 431 | "chalk": "2.3.0", 432 | "concat-stream": "1.6.0", 433 | "cross-spawn": "5.1.0", 434 | "debug": "3.1.0", 435 | "doctrine": "2.0.0", 436 | "eslint-scope": "3.7.1", 437 | "espree": "3.5.1", 438 | "esquery": "1.0.0", 439 | "estraverse": "4.2.0", 440 | "esutils": "2.0.2", 441 | "file-entry-cache": "2.0.0", 442 | "functional-red-black-tree": "1.0.1", 443 | "glob": "7.1.2", 444 | "globals": "9.18.0", 445 | "ignore": "3.3.6", 446 | "imurmurhash": "0.1.4", 447 | "inquirer": "3.3.0", 448 | "is-resolvable": "1.0.0", 449 | "js-yaml": "3.10.0", 450 | "json-stable-stringify": "1.0.1", 451 | "levn": "0.3.0", 452 | "lodash": "4.17.4", 453 | "minimatch": "3.0.4", 454 | "mkdirp": "0.5.1", 455 | "natural-compare": "1.4.0", 456 | "optionator": "0.8.2", 457 | "path-is-inside": "1.0.2", 458 | "pluralize": "7.0.0", 459 | "progress": "2.0.0", 460 | "require-uncached": "1.0.3", 461 | "semver": "5.4.1", 462 | "strip-ansi": "4.0.0", 463 | "strip-json-comments": "2.0.1", 464 | "table": "4.0.2", 465 | "text-table": "0.2.0" 466 | }, 467 | "dependencies": { 468 | "debug": { 469 | "version": "3.1.0", 470 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 471 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 472 | "dev": true, 473 | "requires": { 474 | "ms": "2.0.0" 475 | } 476 | }, 477 | "semver": { 478 | "version": "5.4.1", 479 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", 480 | "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", 481 | "dev": true 482 | } 483 | } 484 | }, 485 | "eslint-config-standard": { 486 | "version": "10.2.1", 487 | "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-10.2.1.tgz", 488 | "integrity": "sha1-wGHk0GbzedwXzVYsZOgZtN1FRZE=", 489 | "dev": true 490 | }, 491 | "eslint-import-resolver-node": { 492 | "version": "0.3.1", 493 | "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.1.tgz", 494 | "integrity": "sha512-yUtXS15gIcij68NmXmP9Ni77AQuCN0itXbCc/jWd8C6/yKZaSNXicpC8cgvjnxVdmfsosIXrjpzFq7GcDryb6A==", 495 | "dev": true, 496 | "requires": { 497 | "debug": "2.6.9", 498 | "resolve": "1.5.0" 499 | } 500 | }, 501 | "eslint-module-utils": { 502 | "version": "2.1.1", 503 | "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.1.1.tgz", 504 | "integrity": "sha512-jDI/X5l/6D1rRD/3T43q8Qgbls2nq5km5KSqiwlyUbGo5+04fXhMKdCPhjwbqAa6HXWaMxj8Q4hQDIh7IadJQw==", 505 | "dev": true, 506 | "requires": { 507 | "debug": "2.6.9", 508 | "pkg-dir": "1.0.0" 509 | } 510 | }, 511 | "eslint-plugin-import": { 512 | "version": "2.8.0", 513 | "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.8.0.tgz", 514 | "integrity": "sha512-Rf7dfKJxZ16QuTgVv1OYNxkZcsu/hULFnC+e+w0Gzi6jMC3guQoWQgxYxc54IDRinlb6/0v5z/PxxIKmVctN+g==", 515 | "dev": true, 516 | "requires": { 517 | "builtin-modules": "1.1.1", 518 | "contains-path": "0.1.0", 519 | "debug": "2.6.9", 520 | "doctrine": "1.5.0", 521 | "eslint-import-resolver-node": "0.3.1", 522 | "eslint-module-utils": "2.1.1", 523 | "has": "1.0.1", 524 | "lodash.cond": "4.5.2", 525 | "minimatch": "3.0.4", 526 | "read-pkg-up": "2.0.0" 527 | }, 528 | "dependencies": { 529 | "doctrine": { 530 | "version": "1.5.0", 531 | "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", 532 | "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", 533 | "dev": true, 534 | "requires": { 535 | "esutils": "2.0.2", 536 | "isarray": "1.0.0" 537 | } 538 | } 539 | } 540 | }, 541 | "eslint-plugin-node": { 542 | "version": "5.2.1", 543 | "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-5.2.1.tgz", 544 | "integrity": "sha512-xhPXrh0Vl/b7870uEbaumb2Q+LxaEcOQ3kS1jtIXanBAwpMre1l5q/l2l/hESYJGEFKuI78bp6Uw50hlpr7B+g==", 545 | "dev": true, 546 | "requires": { 547 | "ignore": "3.3.6", 548 | "minimatch": "3.0.4", 549 | "resolve": "1.5.0", 550 | "semver": "5.3.0" 551 | }, 552 | "dependencies": { 553 | "semver": { 554 | "version": "5.3.0", 555 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", 556 | "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", 557 | "dev": true 558 | } 559 | } 560 | }, 561 | "eslint-plugin-promise": { 562 | "version": "3.6.0", 563 | "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-3.6.0.tgz", 564 | "integrity": "sha512-YQzM6TLTlApAr7Li8vWKR+K3WghjwKcYzY0d2roWap4SLK+kzuagJX/leTetIDWsFcTFnKNJXWupDCD6aZkP2Q==", 565 | "dev": true 566 | }, 567 | "eslint-plugin-standard": { 568 | "version": "3.0.1", 569 | "resolved": "https://registry.npmjs.org/eslint-plugin-standard/-/eslint-plugin-standard-3.0.1.tgz", 570 | "integrity": "sha1-NNDJFbRe3G8BA5PH7vOCOwhWXPI=", 571 | "dev": true 572 | }, 573 | "eslint-scope": { 574 | "version": "3.7.1", 575 | "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz", 576 | "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=", 577 | "dev": true, 578 | "requires": { 579 | "esrecurse": "4.2.0", 580 | "estraverse": "4.2.0" 581 | } 582 | }, 583 | "espree": { 584 | "version": "3.5.1", 585 | "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.1.tgz", 586 | "integrity": "sha1-DJiLirRttTEAoZVK5LqZXd0n2H4=", 587 | "dev": true, 588 | "requires": { 589 | "acorn": "5.1.2", 590 | "acorn-jsx": "3.0.1" 591 | } 592 | }, 593 | "esprima": { 594 | "version": "4.0.0", 595 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", 596 | "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", 597 | "dev": true 598 | }, 599 | "esquery": { 600 | "version": "1.0.0", 601 | "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.0.tgz", 602 | "integrity": "sha1-z7qLV9f7qT8XKYqKAGoEzaE9gPo=", 603 | "dev": true, 604 | "requires": { 605 | "estraverse": "4.2.0" 606 | } 607 | }, 608 | "esrecurse": { 609 | "version": "4.2.0", 610 | "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.0.tgz", 611 | "integrity": "sha1-+pVo2Y04I/mkHZHpAtyrnqblsWM=", 612 | "dev": true, 613 | "requires": { 614 | "estraverse": "4.2.0", 615 | "object-assign": "4.1.1" 616 | } 617 | }, 618 | "estraverse": { 619 | "version": "4.2.0", 620 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", 621 | "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", 622 | "dev": true 623 | }, 624 | "esutils": { 625 | "version": "2.0.2", 626 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", 627 | "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", 628 | "dev": true 629 | }, 630 | "external-editor": { 631 | "version": "2.0.5", 632 | "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.0.5.tgz", 633 | "integrity": "sha512-Msjo64WT5W+NhOpQXh0nOHm+n0RfU1QUwDnKYvJ8dEJ8zlwLrqXNTv5mSUTJpepf41PDJGyhueTw2vNZW+Fr/w==", 634 | "dev": true, 635 | "requires": { 636 | "iconv-lite": "0.4.19", 637 | "jschardet": "1.5.1", 638 | "tmp": "0.0.33" 639 | } 640 | }, 641 | "fast-deep-equal": { 642 | "version": "1.0.0", 643 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", 644 | "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=", 645 | "dev": true 646 | }, 647 | "fast-json-stable-stringify": { 648 | "version": "2.0.0", 649 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", 650 | "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", 651 | "dev": true 652 | }, 653 | "fast-levenshtein": { 654 | "version": "2.0.6", 655 | "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 656 | "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", 657 | "dev": true 658 | }, 659 | "figures": { 660 | "version": "2.0.0", 661 | "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", 662 | "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", 663 | "dev": true, 664 | "requires": { 665 | "escape-string-regexp": "1.0.5" 666 | } 667 | }, 668 | "file-entry-cache": { 669 | "version": "2.0.0", 670 | "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", 671 | "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", 672 | "dev": true, 673 | "requires": { 674 | "flat-cache": "1.3.0", 675 | "object-assign": "4.1.1" 676 | } 677 | }, 678 | "find-up": { 679 | "version": "1.1.2", 680 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", 681 | "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", 682 | "dev": true, 683 | "requires": { 684 | "path-exists": "2.1.0", 685 | "pinkie-promise": "2.0.1" 686 | } 687 | }, 688 | "flat-cache": { 689 | "version": "1.3.0", 690 | "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz", 691 | "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=", 692 | "dev": true, 693 | "requires": { 694 | "circular-json": "0.3.3", 695 | "del": "2.2.2", 696 | "graceful-fs": "4.1.11", 697 | "write": "0.2.1" 698 | } 699 | }, 700 | "fs.realpath": { 701 | "version": "1.0.0", 702 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 703 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 704 | "dev": true 705 | }, 706 | "function-bind": { 707 | "version": "1.1.1", 708 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 709 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", 710 | "dev": true 711 | }, 712 | "functional-red-black-tree": { 713 | "version": "1.0.1", 714 | "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", 715 | "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", 716 | "dev": true 717 | }, 718 | "get-func-name": { 719 | "version": "2.0.0", 720 | "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", 721 | "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", 722 | "dev": true 723 | }, 724 | "glob": { 725 | "version": "7.1.2", 726 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 727 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 728 | "dev": true, 729 | "requires": { 730 | "fs.realpath": "1.0.0", 731 | "inflight": "1.0.6", 732 | "inherits": "2.0.3", 733 | "minimatch": "3.0.4", 734 | "once": "1.4.0", 735 | "path-is-absolute": "1.0.1" 736 | } 737 | }, 738 | "globals": { 739 | "version": "9.18.0", 740 | "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", 741 | "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", 742 | "dev": true 743 | }, 744 | "globby": { 745 | "version": "5.0.0", 746 | "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", 747 | "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", 748 | "dev": true, 749 | "requires": { 750 | "array-union": "1.0.2", 751 | "arrify": "1.0.1", 752 | "glob": "7.1.2", 753 | "object-assign": "4.1.1", 754 | "pify": "2.3.0", 755 | "pinkie-promise": "2.0.1" 756 | } 757 | }, 758 | "graceful-fs": { 759 | "version": "4.1.11", 760 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", 761 | "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", 762 | "dev": true 763 | }, 764 | "graceful-readlink": { 765 | "version": "1.0.1", 766 | "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", 767 | "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", 768 | "dev": true 769 | }, 770 | "growl": { 771 | "version": "1.9.2", 772 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", 773 | "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", 774 | "dev": true 775 | }, 776 | "has": { 777 | "version": "1.0.1", 778 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", 779 | "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", 780 | "dev": true, 781 | "requires": { 782 | "function-bind": "1.1.1" 783 | } 784 | }, 785 | "has-ansi": { 786 | "version": "2.0.0", 787 | "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", 788 | "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", 789 | "dev": true, 790 | "requires": { 791 | "ansi-regex": "2.1.1" 792 | } 793 | }, 794 | "has-flag": { 795 | "version": "2.0.0", 796 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", 797 | "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", 798 | "dev": true 799 | }, 800 | "he": { 801 | "version": "1.1.1", 802 | "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", 803 | "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", 804 | "dev": true 805 | }, 806 | "hosted-git-info": { 807 | "version": "2.5.0", 808 | "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", 809 | "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==", 810 | "dev": true 811 | }, 812 | "iconv-lite": { 813 | "version": "0.4.19", 814 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz", 815 | "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ==" 816 | }, 817 | "ignore": { 818 | "version": "3.3.6", 819 | "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.6.tgz", 820 | "integrity": "sha512-HrxmNxKTGZ9a3uAl/FNG66Sdt0G9L4TtMbbUQjP1WhGmSj0FOyHvSgx7623aGJvXfPOur8MwmarlHT+37jmzlw==", 821 | "dev": true 822 | }, 823 | "ilp-packet": { 824 | "version": "1.4.0", 825 | "resolved": "https://registry.npmjs.org/ilp-packet/-/ilp-packet-1.4.0.tgz", 826 | "integrity": "sha1-6qP1588VHGHAkY+wmz6QjAsVrE8=", 827 | "requires": { 828 | "base64url": "2.0.0", 829 | "base64url-adhoc": "2.0.1", 830 | "bignumber.js": "4.1.0", 831 | "lodash": "4.17.4", 832 | "long": "3.2.0", 833 | "oer-utils": "1.3.4" 834 | } 835 | }, 836 | "imurmurhash": { 837 | "version": "0.1.4", 838 | "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 839 | "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", 840 | "dev": true 841 | }, 842 | "inflight": { 843 | "version": "1.0.6", 844 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 845 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 846 | "dev": true, 847 | "requires": { 848 | "once": "1.4.0", 849 | "wrappy": "1.0.2" 850 | } 851 | }, 852 | "inherits": { 853 | "version": "2.0.3", 854 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 855 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 856 | "dev": true 857 | }, 858 | "inquirer": { 859 | "version": "3.3.0", 860 | "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", 861 | "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", 862 | "dev": true, 863 | "requires": { 864 | "ansi-escapes": "3.0.0", 865 | "chalk": "2.3.0", 866 | "cli-cursor": "2.1.0", 867 | "cli-width": "2.2.0", 868 | "external-editor": "2.0.5", 869 | "figures": "2.0.0", 870 | "lodash": "4.17.4", 871 | "mute-stream": "0.0.7", 872 | "run-async": "2.3.0", 873 | "rx-lite": "4.0.8", 874 | "rx-lite-aggregates": "4.0.8", 875 | "string-width": "2.1.1", 876 | "strip-ansi": "4.0.0", 877 | "through": "2.3.8" 878 | } 879 | }, 880 | "is-arrayish": { 881 | "version": "0.2.1", 882 | "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", 883 | "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", 884 | "dev": true 885 | }, 886 | "is-builtin-module": { 887 | "version": "1.0.0", 888 | "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", 889 | "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", 890 | "dev": true, 891 | "requires": { 892 | "builtin-modules": "1.1.1" 893 | } 894 | }, 895 | "is-fullwidth-code-point": { 896 | "version": "2.0.0", 897 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", 898 | "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", 899 | "dev": true 900 | }, 901 | "is-path-cwd": { 902 | "version": "1.0.0", 903 | "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", 904 | "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", 905 | "dev": true 906 | }, 907 | "is-path-in-cwd": { 908 | "version": "1.0.0", 909 | "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz", 910 | "integrity": "sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=", 911 | "dev": true, 912 | "requires": { 913 | "is-path-inside": "1.0.0" 914 | } 915 | }, 916 | "is-path-inside": { 917 | "version": "1.0.0", 918 | "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.0.tgz", 919 | "integrity": "sha1-/AbloWg/vaE95mev9xe7wQpI838=", 920 | "dev": true, 921 | "requires": { 922 | "path-is-inside": "1.0.2" 923 | } 924 | }, 925 | "is-promise": { 926 | "version": "2.1.0", 927 | "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", 928 | "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", 929 | "dev": true 930 | }, 931 | "is-resolvable": { 932 | "version": "1.0.0", 933 | "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.0.tgz", 934 | "integrity": "sha1-jfV8YeouPFAUCNEA+wE8+NbgzGI=", 935 | "dev": true, 936 | "requires": { 937 | "tryit": "1.0.3" 938 | } 939 | }, 940 | "is-stream": { 941 | "version": "1.1.0", 942 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", 943 | "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" 944 | }, 945 | "isarray": { 946 | "version": "1.0.0", 947 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 948 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", 949 | "dev": true 950 | }, 951 | "isexe": { 952 | "version": "2.0.0", 953 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 954 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", 955 | "dev": true 956 | }, 957 | "js-tokens": { 958 | "version": "3.0.2", 959 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", 960 | "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", 961 | "dev": true 962 | }, 963 | "js-yaml": { 964 | "version": "3.10.0", 965 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.10.0.tgz", 966 | "integrity": "sha512-O2v52ffjLa9VeM43J4XocZE//WT9N0IiwDa3KSHH7Tu8CtH+1qM8SIZvnsTh6v+4yFy5KUY3BHUVwjpfAWsjIA==", 967 | "dev": true, 968 | "requires": { 969 | "argparse": "1.0.9", 970 | "esprima": "4.0.0" 971 | } 972 | }, 973 | "jschardet": { 974 | "version": "1.5.1", 975 | "resolved": "https://registry.npmjs.org/jschardet/-/jschardet-1.5.1.tgz", 976 | "integrity": "sha512-vE2hT1D0HLZCLLclfBSfkfTTedhVj0fubHpJBHKwwUWX0nSbhPAfk+SG9rTX95BYNmau8rGFfCeaT6T5OW1C2A==", 977 | "dev": true 978 | }, 979 | "json-schema-traverse": { 980 | "version": "0.3.1", 981 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", 982 | "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", 983 | "dev": true 984 | }, 985 | "json-stable-stringify": { 986 | "version": "1.0.1", 987 | "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", 988 | "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", 989 | "dev": true, 990 | "requires": { 991 | "jsonify": "0.0.0" 992 | } 993 | }, 994 | "json3": { 995 | "version": "3.3.2", 996 | "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", 997 | "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", 998 | "dev": true 999 | }, 1000 | "jsonify": { 1001 | "version": "0.0.0", 1002 | "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", 1003 | "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", 1004 | "dev": true 1005 | }, 1006 | "levn": { 1007 | "version": "0.3.0", 1008 | "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", 1009 | "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", 1010 | "dev": true, 1011 | "requires": { 1012 | "prelude-ls": "1.1.2", 1013 | "type-check": "0.3.2" 1014 | } 1015 | }, 1016 | "load-json-file": { 1017 | "version": "2.0.0", 1018 | "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", 1019 | "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", 1020 | "dev": true, 1021 | "requires": { 1022 | "graceful-fs": "4.1.11", 1023 | "parse-json": "2.2.0", 1024 | "pify": "2.3.0", 1025 | "strip-bom": "3.0.0" 1026 | } 1027 | }, 1028 | "locate-path": { 1029 | "version": "2.0.0", 1030 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", 1031 | "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", 1032 | "dev": true, 1033 | "requires": { 1034 | "p-locate": "2.0.0", 1035 | "path-exists": "3.0.0" 1036 | }, 1037 | "dependencies": { 1038 | "path-exists": { 1039 | "version": "3.0.0", 1040 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", 1041 | "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", 1042 | "dev": true 1043 | } 1044 | } 1045 | }, 1046 | "lodash": { 1047 | "version": "4.17.4", 1048 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", 1049 | "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" 1050 | }, 1051 | "lodash._baseassign": { 1052 | "version": "3.2.0", 1053 | "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", 1054 | "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", 1055 | "dev": true, 1056 | "requires": { 1057 | "lodash._basecopy": "3.0.1", 1058 | "lodash.keys": "3.1.2" 1059 | } 1060 | }, 1061 | "lodash._basecopy": { 1062 | "version": "3.0.1", 1063 | "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", 1064 | "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", 1065 | "dev": true 1066 | }, 1067 | "lodash._basecreate": { 1068 | "version": "3.0.3", 1069 | "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz", 1070 | "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=", 1071 | "dev": true 1072 | }, 1073 | "lodash._getnative": { 1074 | "version": "3.9.1", 1075 | "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", 1076 | "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", 1077 | "dev": true 1078 | }, 1079 | "lodash._isiterateecall": { 1080 | "version": "3.0.9", 1081 | "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", 1082 | "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", 1083 | "dev": true 1084 | }, 1085 | "lodash.cond": { 1086 | "version": "4.5.2", 1087 | "resolved": "https://registry.npmjs.org/lodash.cond/-/lodash.cond-4.5.2.tgz", 1088 | "integrity": "sha1-9HGh2khr5g9quVXRcRVSPdHSVdU=", 1089 | "dev": true 1090 | }, 1091 | "lodash.create": { 1092 | "version": "3.1.1", 1093 | "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", 1094 | "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", 1095 | "dev": true, 1096 | "requires": { 1097 | "lodash._baseassign": "3.2.0", 1098 | "lodash._basecreate": "3.0.3", 1099 | "lodash._isiterateecall": "3.0.9" 1100 | } 1101 | }, 1102 | "lodash.isarguments": { 1103 | "version": "3.1.0", 1104 | "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", 1105 | "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", 1106 | "dev": true 1107 | }, 1108 | "lodash.isarray": { 1109 | "version": "3.0.4", 1110 | "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", 1111 | "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", 1112 | "dev": true 1113 | }, 1114 | "lodash.keys": { 1115 | "version": "3.1.2", 1116 | "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", 1117 | "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", 1118 | "dev": true, 1119 | "requires": { 1120 | "lodash._getnative": "3.9.1", 1121 | "lodash.isarguments": "3.1.0", 1122 | "lodash.isarray": "3.0.4" 1123 | } 1124 | }, 1125 | "long": { 1126 | "version": "3.2.0", 1127 | "resolved": "https://registry.npmjs.org/long/-/long-3.2.0.tgz", 1128 | "integrity": "sha1-2CG3E4yhy1gcFymQ7xTbIAtcR0s=" 1129 | }, 1130 | "lru-cache": { 1131 | "version": "4.1.1", 1132 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", 1133 | "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", 1134 | "dev": true, 1135 | "requires": { 1136 | "pseudomap": "1.0.2", 1137 | "yallist": "2.1.2" 1138 | } 1139 | }, 1140 | "mimic-fn": { 1141 | "version": "1.1.0", 1142 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.1.0.tgz", 1143 | "integrity": "sha1-5md4PZLonb00KBi1IwudYqZyrRg=", 1144 | "dev": true 1145 | }, 1146 | "minimatch": { 1147 | "version": "3.0.4", 1148 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 1149 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 1150 | "dev": true, 1151 | "requires": { 1152 | "brace-expansion": "1.1.8" 1153 | } 1154 | }, 1155 | "minimist": { 1156 | "version": "0.0.8", 1157 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 1158 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 1159 | "dev": true 1160 | }, 1161 | "mkdirp": { 1162 | "version": "0.5.1", 1163 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 1164 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 1165 | "dev": true, 1166 | "requires": { 1167 | "minimist": "0.0.8" 1168 | } 1169 | }, 1170 | "mocha": { 1171 | "version": "3.5.3", 1172 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.5.3.tgz", 1173 | "integrity": "sha512-/6na001MJWEtYxHOV1WLfsmR4YIynkUEhBwzsb+fk2qmQ3iqsi258l/Q2MWHJMImAcNpZ8DEdYAK72NHoIQ9Eg==", 1174 | "dev": true, 1175 | "requires": { 1176 | "browser-stdout": "1.3.0", 1177 | "commander": "2.9.0", 1178 | "debug": "2.6.8", 1179 | "diff": "3.2.0", 1180 | "escape-string-regexp": "1.0.5", 1181 | "glob": "7.1.1", 1182 | "growl": "1.9.2", 1183 | "he": "1.1.1", 1184 | "json3": "3.3.2", 1185 | "lodash.create": "3.1.1", 1186 | "mkdirp": "0.5.1", 1187 | "supports-color": "3.1.2" 1188 | }, 1189 | "dependencies": { 1190 | "debug": { 1191 | "version": "2.6.8", 1192 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", 1193 | "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", 1194 | "dev": true, 1195 | "requires": { 1196 | "ms": "2.0.0" 1197 | } 1198 | }, 1199 | "glob": { 1200 | "version": "7.1.1", 1201 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", 1202 | "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=", 1203 | "dev": true, 1204 | "requires": { 1205 | "fs.realpath": "1.0.0", 1206 | "inflight": "1.0.6", 1207 | "inherits": "2.0.3", 1208 | "minimatch": "3.0.4", 1209 | "once": "1.4.0", 1210 | "path-is-absolute": "1.0.1" 1211 | } 1212 | }, 1213 | "has-flag": { 1214 | "version": "1.0.0", 1215 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", 1216 | "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", 1217 | "dev": true 1218 | }, 1219 | "supports-color": { 1220 | "version": "3.1.2", 1221 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz", 1222 | "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", 1223 | "dev": true, 1224 | "requires": { 1225 | "has-flag": "1.0.0" 1226 | } 1227 | } 1228 | } 1229 | }, 1230 | "ms": { 1231 | "version": "2.0.0", 1232 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 1233 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 1234 | "dev": true 1235 | }, 1236 | "mute-stream": { 1237 | "version": "0.0.7", 1238 | "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", 1239 | "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", 1240 | "dev": true 1241 | }, 1242 | "natural-compare": { 1243 | "version": "1.4.0", 1244 | "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", 1245 | "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", 1246 | "dev": true 1247 | }, 1248 | "node-fetch": { 1249 | "version": "1.7.3", 1250 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", 1251 | "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", 1252 | "requires": { 1253 | "encoding": "0.1.12", 1254 | "is-stream": "1.1.0" 1255 | } 1256 | }, 1257 | "normalize-package-data": { 1258 | "version": "2.4.0", 1259 | "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", 1260 | "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", 1261 | "dev": true, 1262 | "requires": { 1263 | "hosted-git-info": "2.5.0", 1264 | "is-builtin-module": "1.0.0", 1265 | "semver": "5.0.3", 1266 | "validate-npm-package-license": "3.0.1" 1267 | } 1268 | }, 1269 | "object-assign": { 1270 | "version": "4.1.1", 1271 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 1272 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", 1273 | "dev": true 1274 | }, 1275 | "oer-utils": { 1276 | "version": "1.3.4", 1277 | "resolved": "https://registry.npmjs.org/oer-utils/-/oer-utils-1.3.4.tgz", 1278 | "integrity": "sha1-sqmtvJK8GRVaKgDwRWg9Hm1KyCM=" 1279 | }, 1280 | "once": { 1281 | "version": "1.4.0", 1282 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 1283 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 1284 | "dev": true, 1285 | "requires": { 1286 | "wrappy": "1.0.2" 1287 | } 1288 | }, 1289 | "onetime": { 1290 | "version": "2.0.1", 1291 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", 1292 | "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", 1293 | "dev": true, 1294 | "requires": { 1295 | "mimic-fn": "1.1.0" 1296 | } 1297 | }, 1298 | "optionator": { 1299 | "version": "0.8.2", 1300 | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", 1301 | "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", 1302 | "dev": true, 1303 | "requires": { 1304 | "deep-is": "0.1.3", 1305 | "fast-levenshtein": "2.0.6", 1306 | "levn": "0.3.0", 1307 | "prelude-ls": "1.1.2", 1308 | "type-check": "0.3.2", 1309 | "wordwrap": "1.0.0" 1310 | } 1311 | }, 1312 | "os-tmpdir": { 1313 | "version": "1.0.2", 1314 | "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", 1315 | "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", 1316 | "dev": true 1317 | }, 1318 | "p-limit": { 1319 | "version": "1.1.0", 1320 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.1.0.tgz", 1321 | "integrity": "sha1-sH/y2aXYi+yAYDWJWiurZqJ5iLw=", 1322 | "dev": true 1323 | }, 1324 | "p-locate": { 1325 | "version": "2.0.0", 1326 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", 1327 | "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", 1328 | "dev": true, 1329 | "requires": { 1330 | "p-limit": "1.1.0" 1331 | } 1332 | }, 1333 | "parse-json": { 1334 | "version": "2.2.0", 1335 | "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", 1336 | "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", 1337 | "dev": true, 1338 | "requires": { 1339 | "error-ex": "1.3.1" 1340 | } 1341 | }, 1342 | "path-exists": { 1343 | "version": "2.1.0", 1344 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", 1345 | "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", 1346 | "dev": true, 1347 | "requires": { 1348 | "pinkie-promise": "2.0.1" 1349 | } 1350 | }, 1351 | "path-is-absolute": { 1352 | "version": "1.0.1", 1353 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 1354 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 1355 | "dev": true 1356 | }, 1357 | "path-is-inside": { 1358 | "version": "1.0.2", 1359 | "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", 1360 | "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", 1361 | "dev": true 1362 | }, 1363 | "path-parse": { 1364 | "version": "1.0.5", 1365 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", 1366 | "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", 1367 | "dev": true 1368 | }, 1369 | "path-type": { 1370 | "version": "2.0.0", 1371 | "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", 1372 | "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", 1373 | "dev": true, 1374 | "requires": { 1375 | "pify": "2.3.0" 1376 | } 1377 | }, 1378 | "pathval": { 1379 | "version": "1.1.0", 1380 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", 1381 | "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", 1382 | "dev": true 1383 | }, 1384 | "pify": { 1385 | "version": "2.3.0", 1386 | "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", 1387 | "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", 1388 | "dev": true 1389 | }, 1390 | "pinkie": { 1391 | "version": "2.0.4", 1392 | "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", 1393 | "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", 1394 | "dev": true 1395 | }, 1396 | "pinkie-promise": { 1397 | "version": "2.0.1", 1398 | "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", 1399 | "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", 1400 | "dev": true, 1401 | "requires": { 1402 | "pinkie": "2.0.4" 1403 | } 1404 | }, 1405 | "pkg-dir": { 1406 | "version": "1.0.0", 1407 | "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", 1408 | "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", 1409 | "dev": true, 1410 | "requires": { 1411 | "find-up": "1.1.2" 1412 | } 1413 | }, 1414 | "pluralize": { 1415 | "version": "7.0.0", 1416 | "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", 1417 | "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==", 1418 | "dev": true 1419 | }, 1420 | "prelude-ls": { 1421 | "version": "1.1.2", 1422 | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", 1423 | "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", 1424 | "dev": true 1425 | }, 1426 | "process-nextick-args": { 1427 | "version": "1.0.7", 1428 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", 1429 | "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", 1430 | "dev": true 1431 | }, 1432 | "progress": { 1433 | "version": "2.0.0", 1434 | "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", 1435 | "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", 1436 | "dev": true 1437 | }, 1438 | "pseudomap": { 1439 | "version": "1.0.2", 1440 | "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", 1441 | "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", 1442 | "dev": true 1443 | }, 1444 | "read-pkg": { 1445 | "version": "2.0.0", 1446 | "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", 1447 | "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", 1448 | "dev": true, 1449 | "requires": { 1450 | "load-json-file": "2.0.0", 1451 | "normalize-package-data": "2.4.0", 1452 | "path-type": "2.0.0" 1453 | } 1454 | }, 1455 | "read-pkg-up": { 1456 | "version": "2.0.0", 1457 | "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", 1458 | "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", 1459 | "dev": true, 1460 | "requires": { 1461 | "find-up": "2.1.0", 1462 | "read-pkg": "2.0.0" 1463 | }, 1464 | "dependencies": { 1465 | "find-up": { 1466 | "version": "2.1.0", 1467 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", 1468 | "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", 1469 | "dev": true, 1470 | "requires": { 1471 | "locate-path": "2.0.0" 1472 | } 1473 | } 1474 | } 1475 | }, 1476 | "readable-stream": { 1477 | "version": "2.3.3", 1478 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", 1479 | "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", 1480 | "dev": true, 1481 | "requires": { 1482 | "core-util-is": "1.0.2", 1483 | "inherits": "2.0.3", 1484 | "isarray": "1.0.0", 1485 | "process-nextick-args": "1.0.7", 1486 | "safe-buffer": "5.1.1", 1487 | "string_decoder": "1.0.3", 1488 | "util-deprecate": "1.0.2" 1489 | } 1490 | }, 1491 | "require-uncached": { 1492 | "version": "1.0.3", 1493 | "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", 1494 | "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", 1495 | "dev": true, 1496 | "requires": { 1497 | "caller-path": "0.1.0", 1498 | "resolve-from": "1.0.1" 1499 | } 1500 | }, 1501 | "resolve": { 1502 | "version": "1.5.0", 1503 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz", 1504 | "integrity": "sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw==", 1505 | "dev": true, 1506 | "requires": { 1507 | "path-parse": "1.0.5" 1508 | } 1509 | }, 1510 | "resolve-from": { 1511 | "version": "1.0.1", 1512 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", 1513 | "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", 1514 | "dev": true 1515 | }, 1516 | "restore-cursor": { 1517 | "version": "2.0.0", 1518 | "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", 1519 | "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", 1520 | "dev": true, 1521 | "requires": { 1522 | "onetime": "2.0.1", 1523 | "signal-exit": "3.0.2" 1524 | } 1525 | }, 1526 | "rimraf": { 1527 | "version": "2.6.2", 1528 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", 1529 | "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", 1530 | "dev": true, 1531 | "requires": { 1532 | "glob": "7.1.2" 1533 | } 1534 | }, 1535 | "run-async": { 1536 | "version": "2.3.0", 1537 | "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", 1538 | "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", 1539 | "dev": true, 1540 | "requires": { 1541 | "is-promise": "2.1.0" 1542 | } 1543 | }, 1544 | "rx-lite": { 1545 | "version": "4.0.8", 1546 | "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", 1547 | "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=", 1548 | "dev": true 1549 | }, 1550 | "rx-lite-aggregates": { 1551 | "version": "4.0.8", 1552 | "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz", 1553 | "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=", 1554 | "dev": true, 1555 | "requires": { 1556 | "rx-lite": "4.0.8" 1557 | } 1558 | }, 1559 | "safe-buffer": { 1560 | "version": "5.1.1", 1561 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 1562 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", 1563 | "dev": true 1564 | }, 1565 | "semver": { 1566 | "version": "5.0.3", 1567 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.0.3.tgz", 1568 | "integrity": "sha1-d0Zt5YnNXTyV8TiqeLxWmjy10no=", 1569 | "dev": true 1570 | }, 1571 | "shebang-command": { 1572 | "version": "1.2.0", 1573 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", 1574 | "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", 1575 | "dev": true, 1576 | "requires": { 1577 | "shebang-regex": "1.0.0" 1578 | } 1579 | }, 1580 | "shebang-regex": { 1581 | "version": "1.0.0", 1582 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", 1583 | "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", 1584 | "dev": true 1585 | }, 1586 | "signal-exit": { 1587 | "version": "3.0.2", 1588 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", 1589 | "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", 1590 | "dev": true 1591 | }, 1592 | "slice-ansi": { 1593 | "version": "1.0.0", 1594 | "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz", 1595 | "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==", 1596 | "dev": true, 1597 | "requires": { 1598 | "is-fullwidth-code-point": "2.0.0" 1599 | } 1600 | }, 1601 | "spdx-correct": { 1602 | "version": "1.0.2", 1603 | "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", 1604 | "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", 1605 | "dev": true, 1606 | "requires": { 1607 | "spdx-license-ids": "1.2.2" 1608 | } 1609 | }, 1610 | "spdx-expression-parse": { 1611 | "version": "1.0.4", 1612 | "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz", 1613 | "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=", 1614 | "dev": true 1615 | }, 1616 | "spdx-license-ids": { 1617 | "version": "1.2.2", 1618 | "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz", 1619 | "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=", 1620 | "dev": true 1621 | }, 1622 | "sprintf-js": { 1623 | "version": "1.0.3", 1624 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 1625 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", 1626 | "dev": true 1627 | }, 1628 | "string-width": { 1629 | "version": "2.1.1", 1630 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", 1631 | "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", 1632 | "dev": true, 1633 | "requires": { 1634 | "is-fullwidth-code-point": "2.0.0", 1635 | "strip-ansi": "4.0.0" 1636 | } 1637 | }, 1638 | "string_decoder": { 1639 | "version": "1.0.3", 1640 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", 1641 | "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", 1642 | "dev": true, 1643 | "requires": { 1644 | "safe-buffer": "5.1.1" 1645 | } 1646 | }, 1647 | "strip-ansi": { 1648 | "version": "4.0.0", 1649 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", 1650 | "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", 1651 | "dev": true, 1652 | "requires": { 1653 | "ansi-regex": "3.0.0" 1654 | }, 1655 | "dependencies": { 1656 | "ansi-regex": { 1657 | "version": "3.0.0", 1658 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", 1659 | "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", 1660 | "dev": true 1661 | } 1662 | } 1663 | }, 1664 | "strip-bom": { 1665 | "version": "3.0.0", 1666 | "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", 1667 | "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", 1668 | "dev": true 1669 | }, 1670 | "strip-json-comments": { 1671 | "version": "2.0.1", 1672 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 1673 | "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", 1674 | "dev": true 1675 | }, 1676 | "supports-color": { 1677 | "version": "2.0.0", 1678 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", 1679 | "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", 1680 | "dev": true 1681 | }, 1682 | "table": { 1683 | "version": "4.0.2", 1684 | "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz", 1685 | "integrity": "sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==", 1686 | "dev": true, 1687 | "requires": { 1688 | "ajv": "5.3.0", 1689 | "ajv-keywords": "2.1.0", 1690 | "chalk": "2.3.0", 1691 | "lodash": "4.17.4", 1692 | "slice-ansi": "1.0.0", 1693 | "string-width": "2.1.1" 1694 | } 1695 | }, 1696 | "text-table": { 1697 | "version": "0.2.0", 1698 | "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", 1699 | "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", 1700 | "dev": true 1701 | }, 1702 | "through": { 1703 | "version": "2.3.8", 1704 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 1705 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", 1706 | "dev": true 1707 | }, 1708 | "tmp": { 1709 | "version": "0.0.33", 1710 | "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", 1711 | "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", 1712 | "dev": true, 1713 | "requires": { 1714 | "os-tmpdir": "1.0.2" 1715 | } 1716 | }, 1717 | "tryit": { 1718 | "version": "1.0.3", 1719 | "resolved": "https://registry.npmjs.org/tryit/-/tryit-1.0.3.tgz", 1720 | "integrity": "sha1-OTvnMKlEb9Hq1tpZoBQwjzbCics=", 1721 | "dev": true 1722 | }, 1723 | "type-check": { 1724 | "version": "0.3.2", 1725 | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", 1726 | "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", 1727 | "dev": true, 1728 | "requires": { 1729 | "prelude-ls": "1.1.2" 1730 | } 1731 | }, 1732 | "type-detect": { 1733 | "version": "4.0.3", 1734 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.3.tgz", 1735 | "integrity": "sha1-Dj8mcLRAmbC0bChNE2p+9Jx0wuo=", 1736 | "dev": true 1737 | }, 1738 | "typedarray": { 1739 | "version": "0.0.6", 1740 | "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", 1741 | "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", 1742 | "dev": true 1743 | }, 1744 | "util-deprecate": { 1745 | "version": "1.0.2", 1746 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1747 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", 1748 | "dev": true 1749 | }, 1750 | "uuid": { 1751 | "version": "3.1.0", 1752 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", 1753 | "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" 1754 | }, 1755 | "validate-npm-package-license": { 1756 | "version": "3.0.1", 1757 | "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz", 1758 | "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", 1759 | "dev": true, 1760 | "requires": { 1761 | "spdx-correct": "1.0.2", 1762 | "spdx-expression-parse": "1.0.4" 1763 | } 1764 | }, 1765 | "which": { 1766 | "version": "1.3.0", 1767 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", 1768 | "integrity": "sha512-xcJpopdamTuY5duC/KnTTNBraPK54YwpenP4lzxU8H91GudWpFv38u0CKjclE1Wi2EH2EDz5LRcHcKbCIzqGyg==", 1769 | "dev": true, 1770 | "requires": { 1771 | "isexe": "2.0.0" 1772 | } 1773 | }, 1774 | "wordwrap": { 1775 | "version": "1.0.0", 1776 | "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", 1777 | "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", 1778 | "dev": true 1779 | }, 1780 | "wrappy": { 1781 | "version": "1.0.2", 1782 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1783 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 1784 | "dev": true 1785 | }, 1786 | "write": { 1787 | "version": "0.2.1", 1788 | "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", 1789 | "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", 1790 | "dev": true, 1791 | "requires": { 1792 | "mkdirp": "0.5.1" 1793 | } 1794 | }, 1795 | "yallist": { 1796 | "version": "2.1.2", 1797 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", 1798 | "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", 1799 | "dev": true 1800 | } 1801 | } 1802 | } 1803 | -------------------------------------------------------------------------------- /letter-shop/completed/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "letter-shop-tutorial", 3 | "version": "1.0.0", 4 | "description": "Interledger Letter Shop Tutorial", 5 | "main": "pay.js", 6 | "dependencies": { 7 | "ilp-packet": "^1.3.0", 8 | "ilp-plugin-payment-channel-framework": "github:interledgerjs/ilp-plugin-payment-channel-framework#bs-clp", 9 | "ilp-plugin-xrp-escrow": "github:michielbdejong/ilp-plugin-xrp-escrow#72e4178", 10 | "node-fetch": "^1.7.3", 11 | "uuid": "^3.1.0" 12 | }, 13 | "devDependencies": { 14 | "chai": "^4.1.1", 15 | "eslint": "^4.5.0", 16 | "eslint-config-standard": "^10.2.1", 17 | "eslint-plugin-import": "^2.7.0", 18 | "eslint-plugin-node": "^5.1.0", 19 | "eslint-plugin-promise": "^3.5.0", 20 | "eslint-plugin-standard": "^3.0.1", 21 | "mocha": "^3.4.1" 22 | }, 23 | "scripts": { 24 | "test": "eslint ." 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/interledger/tutorials.git" 29 | }, 30 | "author": "", 31 | "license": "ISC", 32 | "bugs": { 33 | "url": "https://github.com/interledger/tutorials/issues" 34 | }, 35 | "homepage": "https://interledger.org/tutorials", 36 | "eslintConfig": { 37 | "rules": { 38 | "max-len": ["error", 80] 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /letter-shop/completed/pay.js: -------------------------------------------------------------------------------- 1 | const IlpPacket = require('ilp-packet') 2 | const plugin = require('./plugins.js').xrp.Customer() 3 | const uuid = require('uuid/v4') 4 | 5 | function base64url (buf) { 6 | return buf.toString('base64') 7 | .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') 8 | } 9 | 10 | const destinationAddress = process.argv[2] 11 | const destinationAmount = process.argv[3] 12 | const condition = process.argv[4] 13 | 14 | console.log(`== Starting the payment client == `) 15 | console.log(` 1. Connecting to an account to send payments...`) 16 | 17 | plugin.connect().then(function () { 18 | const ledgerInfo = plugin.getInfo() 19 | const account = plugin.getAccount() 20 | console.log(` - Connected to ledger: ${ledgerInfo.prefix}`) 21 | console.log(` -- Account: ${account}`) 22 | console.log(` -- Currency: ${ledgerInfo.currencyCode}`) 23 | console.log(` -- CurrencyScale: ${ledgerInfo.currencyScale}`) 24 | 25 | console.log(` 2. Making payment to ${destinationAddress} ` + 26 | `using condition: ${condition}`) 27 | 28 | // Send the transfer 29 | plugin.sendTransfer({ 30 | to: destinationAddress, 31 | amount: destinationAmount, 32 | executionCondition: condition, 33 | id: uuid(), 34 | from: plugin.getAccount(), 35 | ledger: plugin.getInfo().prefix, 36 | ilp: base64url(IlpPacket.serializeIlpPayment({ 37 | amount: destinationAmount, 38 | account: destinationAddress 39 | })), 40 | expiresAt: new Date(new Date().getTime() + 1000000).toISOString() 41 | }).then(function () { 42 | console.log(' - Transfer prepared, waiting for fulfillment...') 43 | }, function (err) { 44 | console.error(err.message) 45 | }) 46 | 47 | // Handle fulfillments 48 | plugin.on('outgoing_fulfill', function (transferId, fulfillmentBase64) { 49 | console.log(' - Transfer executed. Got fulfillment: ' + 50 | fulfillmentBase64) 51 | console.log(` 3. Collect your letter at ` + 52 | `http://localhost:8000/${fulfillmentBase64}`) 53 | plugin.disconnect() 54 | process.exit() 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /letter-shop/completed/plugins.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file provides a convenient place to get access to all of the 3 | * plugins we want to use. You can add any plugins here and use them 4 | * in from other scripts. 5 | * 6 | * If you're looking for other plugins you can start with the Interledger 7 | * GitHub repo. All ILP plugin repositories on start with 'ilp-plugin-'. 8 | * 9 | * See: https://github.com/search?utf8=%E2%9C%93&q=ilp-plugin- 10 | * 11 | * We will start by using the XRP testnet, and the 'ilp-plugin-xrp-escrow' 12 | * plugin. 13 | */ 14 | 15 | /* eslint-disable no-unused-vars */ 16 | const XrpEscrowPlugin = require('ilp-plugin-xrp-escrow') 17 | /* eslint-enable no-unused-vars */ 18 | 19 | // TODO Comment this out and uncomment the block below 20 | // after adding valid plugin configurations 21 | exports.xrp = { 22 | Customer: function () { 23 | console.error(`No account configured yet for the Customer.` + 24 | `See 'plugins.js'.`) 25 | process.exit() 26 | }, 27 | Shop: function () { 28 | console.error(`No account configured yet for the Shop.` + 29 | `See 'plugins.js'.`) 30 | process.exit() 31 | } 32 | } 33 | /** 34 | * To get an account and secret to use for the tutorials: 35 | * 36 | * 1. Go to https://ripple.com/build/xrp-test-net/ 37 | * 2. Generate Credentials 38 | * 3. Copy the account and secret into one of the plugin configurations below 39 | * 4. Repeat steps 2 and 3 for the second account 40 | * 5. Copy WEBSOCKETS address from the Test Net Servers info on the same page 41 | */ 42 | 43 | // exports.xrp = { 44 | // Customer: function () { 45 | // return new XrpEscrowPlugin({ 46 | // secret: '', 47 | // account: '', 48 | // server: 'wss://s.altnet.rippletest.net:51233', 49 | // prefix: 'test.crypto.xrp.' 50 | // }) 51 | // }, 52 | // Shop: function () { 53 | // return new XrpEscrowPlugin({ 54 | // secret: '', 55 | // account: '', 56 | // server: 'wss://s.altnet.rippletest.net:51233', 57 | // prefix: 'test.crypto.xrp.' 58 | // }) 59 | // } 60 | // } 61 | -------------------------------------------------------------------------------- /letter-shop/completed/shop.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const url = require('url') 3 | const crypto = require('crypto') 4 | const plugin = require('./plugins.js').xrp.Shop() 5 | 6 | function base64url (buf) { 7 | return buf.toString('base64') 8 | .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') 9 | } 10 | 11 | function sha256 (preimage) { 12 | return crypto.createHash('sha256').update(preimage).digest() 13 | } 14 | 15 | let fulfillments = {} 16 | let letters = {} 17 | const cost = 10 18 | 19 | console.log(`== Starting the shop server == `) 20 | console.log(` 1. Connecting to an account to accept payments...`) 21 | 22 | plugin.connect().then(function () { 23 | // Get ledger and account information from the plugin 24 | const ledgerInfo = plugin.getInfo() 25 | const account = plugin.getAccount() 26 | 27 | console.log(` - Connected to ledger: ${ledgerInfo.prefix}`) 28 | console.log(` -- Account: ${account}`) 29 | console.log(` -- Currency: ${ledgerInfo.currencyCode}`) 30 | console.log(` -- CurrencyScale: ${ledgerInfo.currencyScale}`) 31 | 32 | // Convert our cost (10) into the right format given the ledger scale 33 | const normalizedCost = cost / Math.pow(10, parseInt(ledgerInfo.currencyScale)) 34 | 35 | console.log(` 2. Starting web server to accept requests...`) 36 | console.log(` - Charging ${normalizedCost} ${ledgerInfo.currencyCode}`) 37 | 38 | // Handle incoming web requests 39 | http.createServer(function (req, res) { 40 | // Browsers are irritiating and often probe for a favicon, just ignore 41 | if (req.url.startsWith(`/favicon.ico`)) { 42 | res.statusCode = 404 43 | res.end() 44 | return 45 | } 46 | 47 | console.log(` - Incoming request to: ${req.url}`) 48 | const requestUrl = url.parse(req.url) 49 | 50 | if (requestUrl.path === `/`) { 51 | // Request for a letter with no attached fulfillment 52 | 53 | // Respond with a 402 HTTP Status Code (Payment Required) 54 | res.statusCode = 402 55 | 56 | // Generate a preimage and its SHA256 hash, 57 | // which we'll use as the fulfillment and condition, respectively, of the 58 | // conditional transfer. 59 | const fulfillment = crypto.randomBytes(32) 60 | const condition = sha256(fulfillment) 61 | 62 | // Get the letter that we are selling 63 | const letter = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ') 64 | .split('')[(Math.floor(Math.random() * 26))] 65 | 66 | console.log(` - Generated letter (${letter}) ` + 67 | `at http://localhost:8000${req.url}${base64url(fulfillment)}`) 68 | 69 | // Store the fulfillment (indexed by condition) to use when we get paid 70 | fulfillments[base64url(condition)] = fulfillment 71 | 72 | // Store the letter (indexed by the fulfillment) to use when the customer 73 | // requests it 74 | letters[base64url(fulfillment)] = letter 75 | 76 | console.log(` - Waiting for payment...`) 77 | 78 | res.setHeader(`Pay`, `interledger-condition ${cost} ${account} ${condition}`) 79 | 80 | res.end(`Please send an Interledger payment of` + 81 | ` ${normalizedCost} ${ledgerInfo.currencyCode} to ${account}` + 82 | ` using the condition ${base64url(condition)}\n` + 83 | `> node ./pay.js ${account} ${cost} ${base64url(condition)}`) 84 | } else { 85 | // Request for a letter with the fulfillment in the path 86 | 87 | // Get fulfillment from the path 88 | const fulfillmentBase64 = requestUrl.path.substring(1) 89 | 90 | // Lookup the letter we stored previously for this fulfillment 91 | const letter = letters[fulfillmentBase64] 92 | 93 | if (!letter) { 94 | // We have no record of a letter that was issued for this fulfillment 95 | 96 | // Respond with a 404 HTTP Status Code (Not Found) 97 | res.statusCode = 404 98 | 99 | console.log(' - No letter found for fulfillment: ' + 100 | fulfillmentBase64) 101 | 102 | res.end(`Unrecognized fulfillment.`) 103 | } else { 104 | // Provide the customer with their letter 105 | res.end(`Your letter: ${letter}`) 106 | 107 | console.log(` 5. Providing paid letter to customer ` + 108 | `for fulfillment ${fulfillmentBase64}`) 109 | } 110 | } 111 | }).listen(8000, function () { 112 | console.log(` - Listening on http://localhost:8000`) 113 | console.log(` 3. Visit http://localhost:8000 in your browser ` + 114 | `to buy a letter`) 115 | }) 116 | 117 | // Handle incoming payments 118 | plugin.on('incoming_prepare', function (transfer) { 119 | if (parseInt(transfer.amount) < 10) { 120 | // Transfer amount is incorrect 121 | console.log(` - Payment received for the wrong amount ` + 122 | `(${transfer.amount})... Rejected`) 123 | 124 | const normalizedAmount = transfer.amount / 125 | Math.pow(10, parseInt(ledgerInfo.currencyScale)) 126 | 127 | plugin.rejectIncomingTransfer(transfer.id, { 128 | code: 'F04', 129 | name: 'Insufficient Destination Amount', 130 | message: `Please send at least 10 ${ledgerInfo.currencyCode},` + 131 | `you sent ${normalizedAmount}`, 132 | triggered_by: plugin.getAccount(), 133 | triggered_at: new Date().toISOString(), 134 | forwarded_by: [], 135 | additional_info: {} 136 | }) 137 | } else { 138 | // Lookup fulfillment from condition attached to incoming transfer 139 | const fulfillment = fulfillments[transfer.executionCondition] 140 | 141 | if (!fulfillment) { 142 | // We don't have a fulfillment for this condition 143 | console.log(` - Payment received with an unknown condition: ` + 144 | `${transfer.executionCondition}`) 145 | 146 | plugin.rejectIncomingTransfer(transfer.id, { 147 | code: 'F05', 148 | name: 'Wrong Condition', 149 | message: `Unable to fulfill the condition: ` + 150 | `${transfer.executionCondition}`, 151 | triggered_by: plugin.getAccount(), 152 | triggered_at: new Date().toISOString(), 153 | forwarded_by: [], 154 | additional_info: {} 155 | }) 156 | } 157 | 158 | console.log(` 4. Accepted payment with condition ` + 159 | `${transfer.executionCondition}.`) 160 | console.log(` - Fulfilling transfer on the ledger ` + 161 | `using fulfillment: ${base64url(fulfillment)}`) 162 | 163 | // The ledger will check if the fulfillment is correct and 164 | // if it was submitted before the transfer's rollback timeout 165 | plugin.fulfillCondition(transfer.id, base64url(fulfillment)) 166 | .catch(function () { 167 | console.log(` - Error fulfilling the transfer`) 168 | }) 169 | console.log(` - Payment complete`) 170 | } 171 | }) 172 | }) 173 | -------------------------------------------------------------------------------- /letter-shop/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: "Letter Shop Tutorial | Interledger" 4 | --- 5 | # Letter Shop Tutorial 6 | 7 | In this tutorial you'll learn some of the basic concepts of Interledger and some of the standards that make ILP a protocol that can be applied to any payment network. 8 | 9 | In the tutorial we will run a very basic online service which sells... letters, and write a client to pay for them. 10 | 11 | Every time you visit the site you will be given a random letter from A - Z. But, if you haven't paid the fee you'll be prompted to pay first and will only be able to collect your letter when you have done so. 12 | 13 | In order to make the payment required to get our letter we will also implement a simple client to make the payment. 14 | 15 | > This tutorial uses the XRP Testnet as the underlying payment network but any payment network can be used if there is a working ledger plugin for it. Check out the existing plugins that are in development on [GitHub](https://github.com/search?utf8=%E2%9C%93&q=ilp-plugin-). 16 | 17 | ## What you need before you start: 18 | 19 | * a computer with an internet connection 20 | * [NodeJS](https://nodejs.org/en/download/current/) version 7 or higher installed 21 | * basic knowledge of the command line terminal 22 | * basic knowledge of JavaScript 23 | * (optional) have git installed 24 | 25 | ## What you'll learn: 26 | 27 | * about conditional payments and hashtimelock agreements 28 | * about ILP Addresses 29 | * about the Ledger Plugin Interface (LPI) and how to configure and use plugins 30 | * how to build a service that accepts Interledger payments 31 | * how to build a basic payment client for sending Interledger payments 32 | * (bonus) how to build a client proxy that automates paying for requests as required 33 | 34 | > **NOTE:** There will be times, while doing this tutorial, when following the instructions results in an error. **That may be intentional.** Read on to understand why the error exists and how to fix it. 35 | 36 | ## Background 37 | 38 | To illustrate the basic ILP concepts we're going to build a simple online service that sells letters of the alphabet and accepts payments via ILP. *We haven't patented the idea so please be generous if you deploy this and become an instant millionaire.* 39 | 40 | To do this we need a **server**, run by the shop, and also a **client**, run by the customer. 41 | 42 | ### Server 43 | 44 | The server will perform two functions: 45 | 1. It will host an HTTP service where clients can GET a new random letter with each request. 46 | 1. It will listen for incoming ILP payments related to pending letter purchases, fulfill these to accept the payment, and provide the payer with a token they can use to claim their letter. 47 | 48 | Once we have completed the tutorial the server will perform the following steps: 49 | 50 | 1. Connect to an account and monitor it for incoming payments 51 | 1. Start a web server to accept letter requests 52 | 1. Process a request, store the result, and provide the customer with details of how to pay for the request and get the result 53 | 1. Process an incoming payment related to a previous request, and provide payer with the data required to perform request again using a proof-of-payment token 54 | 1. Process a request containing a proof-of-payment token, and return the originally generated result 55 | 56 | ### Client 57 | 58 | The client will perform an ILP payment and output the returned fulfillment. This can be redeemed to get the letter that was requested in the original request. 59 | 60 | Once we have completed the tutorial the client will perform the following steps: 61 | 62 | 1. Connect to an account from which to send payments 63 | 1. Initiate a payment using the address, amount, and condition provided in the response to the original letter request 64 | 1. Complete the payment and return the proof-of-payment token and instructions on how to use it to complete the original request 65 | 66 | ## Step 1: Get the code 67 | 68 | The code for this tutorial is available in two forms: 69 | 70 | 1. If you follow the tutorial you will start with some very basic code and will flesh it out as we go. 71 | 2. If you'd prefer to start with the finished code and simply walk through the steps then you can do that too. 72 | 73 | First let's get all the code, either using Git or by simply downloading and unzipping it. 74 | 75 | #### Git 76 | 77 | ```shell 78 | git clone https://github.com/interledger/tutorials 79 | ``` 80 | 81 | #### Download 82 | 83 | [ZIP Archive](https://github.com/interledger/tutorials/archive/master.zip) 84 | 85 | Assuming you have either cloned the repository, or unzipped your download, open a terminal window at the root of the project and change into the `letter-shop` directory. 86 | 87 | You should see the following files: 88 | 89 | ```shell 90 | README.md 91 | completed/ 92 | index.md 93 | package-lock.json 94 | package.json 95 | pay.js 96 | plugins.js 97 | proxy.js 98 | shop.js 99 | ``` 100 | 101 | Most of these files are just scaffolding, waiting for you to complete as we go. If you want the complete files so you can just follow along, switch into the `complete` sub-directory. 102 | 103 | ## Step 2: Run the server 104 | 105 | The first thing we need to do is install our dependencies and start our shop's server. 106 | 107 | We install using npm (this may take a few minutes while it downloads all the dependencies): 108 | 109 | ```shell 110 | npm install 111 | ``` 112 | 113 | > If you get an error here make sure you have `node` installed. 114 | 115 | Then we try and start our shop server: 116 | 117 | ```shell 118 | node shop.js 119 | ``` 120 | 121 | But, we've hit a snag... Read on. 122 | 123 | ### Interledger plugins 124 | 125 | Our shop, and many other components we'll build in this tutorial, use ledger plugins. A plugin is a piece of code that talks to a specific account on a specific ledger. 126 | 127 | Since Interledger connects potentially very different ledgers together, we need an abstraction layer that hides the specifics of the ledger, but exposes the interface needed to send and receive money over that ledger. That is what Interledger plugins are for. 128 | 129 | Ledger plugins expose a common interface (the Ledger Plugin Interface) so that irrespective of which ledger your application is connected to, the way it sends and receives payments is identical. 130 | 131 | > **NOTE:** As we go through this tutorial we'll be using different functions of the Ledger Plugin Interface. You can find the reference documentation in [IL-RFC 0004, Draft 8](https://interledger.org/rfcs/0004-ledger-plugin-interface/draft-8.html). 132 | 133 | We've put all of the plugin config into a single file called `plugins.js`. The default plugin we use is the *XRP Escrow* plugin which allows us to connect to the XRP Testnet Ledger. 134 | 135 | The *XRP Escrow* plugin is a wrapper around [RippleLib](https://github.com/ripple/ripple-lib), and exposes the Ledger Plugin Interface (LPI). In the code, you see that an 'ilp-plugin-xrp-escrow' Plugin is being configured with secret, account, server, and prefix. The first three values come from the [XRP Testnet Faucet](https://ripple.com/build/xrp-test-net/). 136 | 137 | Edit this file in your favourite text editor and follow the instructions in the code comments to: 138 | 139 | 1. Get different XRP Testnet credentials for both the shop and the customer. 140 | 1. Configure the XRP Ledger plugins with those new credentials 141 | 142 | > **IMPORTANT:** Even if you are using the completed tutorial code you'll need to set this up so you are using your own test accounts. 143 | 144 | Now try running your server again. 145 | 146 | ```shell 147 | $ node shop.js 148 | ``` 149 | 150 | You should see something like: 151 | 152 | ``` 153 | == Starting the shop server == 154 | ``` 155 | 156 | Inside `shop.js` you'll see we got an instance of the ledger plugin you configured in `plugins.js`: 157 | 158 | ```js 159 | const plugin = require('./plugins.js').xrp.Shop() 160 | ``` 161 | 162 | Now we need to **do something** with that plugin. Replace the text `// Do something...` with the following: 163 | 164 | ```js 165 | console.log(` 1. Connecting to an account to accept payments...`) 166 | 167 | plugin.connect().then(function () { 168 | // Get ledger and account information from the plugin 169 | const ledgerInfo = plugin.getInfo() 170 | const account = plugin.getAccount() 171 | 172 | console.log(` - Connected to ledger: ${ledgerInfo.prefix}`) 173 | console.log(` -- Account: ${account}`) 174 | console.log(` -- Currency: ${ledgerInfo.currencyCode}`) 175 | console.log(` -- CurrencyScale: ${ledgerInfo.currencyScale}`) 176 | 177 | // Convert our cost (10) into the right format given the ledger scale 178 | const normalizedCost = cost / Math.pow(10, parseInt(ledgerInfo.currencyScale)) 179 | 180 | console.log(` 2. Starting web server to accept requests...`) 181 | console.log(` - Charging ${normalizedCost} ${ledgerInfo.currencyCode}`) 182 | 183 | // Handle incoming web requests... 184 | 185 | // Handle incoming transfers... 186 | 187 | }) 188 | ``` 189 | 190 | Stop the server and restart it with the new code. Now it will use the ledger plugin to connect to the XRP Testnet Ledger. When it is connected (usually a few seconds) you'll see some logging in the console providing info about the ledger. 191 | 192 | We can get a lot of info from the plugin about the ledger and the account it is connected to using the `getInfo()` and `getAccount()` methods. 193 | 194 | > **NOTE:** The currency of the ledger is XRP and the currency scale is 6. That means that to send 1 XRP to this ledger we must send an Interledger payment with an amount of 1000000 (1 million Drops). 195 | 196 | > **Interledger standardizes on 64-bit unsigned integers for amounts.** 197 | 198 | > Note how we normalize the cost of our service (10 Drops) to be able to express it in XRP (0.00001 XRP) for display purposes. 199 | 200 | > **IMPORTANT:** Scale (and precision) can be a confusing aspect of any financial protocol and you'd do well to understand the ledger you are working with and be sure that when you are sending payments and displaying amounts to users you are getting your scale and precision right. We made the decision to avoid some complexity by using only scale and requiring that ledgers always express their currency with a precision of zero. This allowed us to only use integers (not decimals) in the protocol so all numbers are normalised to that form. There are a lot of other technical reasons behind the decision which you'll find discussed at length in the project issue list if you are interested. 201 | 202 | Now we have the plugin connected and we have the info we need about the account where we will be accepting payments. 203 | 204 | :tada: Congratulations, our shop is performing step 1 of our 5 step plan! 205 | 206 | Our next job is to start up the web server that will host our Letter Shop service. 207 | 208 | Whenever a customer visits the website we'll try to find a proof-of-payment token in the URL. If the token is there we'll try to redeem it and give the customer their letter. If not we'll generate the letter and the proof-of-payment token and store these for later. 209 | 210 | Lastly we derive the condition for the payment (explained in more detail later) which we can give to the customer to send with their payment. This has an important function in Interledger but for our shop it's also useful to reconcile the payment and this original request. 211 | 212 | Replace the text `// Handle incoming web requests...` with the following: 213 | 214 | ```js 215 | // Handle incoming web requests 216 | http.createServer(function (req, res) { 217 | // Browsers are irritiating and often probe for a favicon, just ignore 218 | if (req.url.startsWith(`/favicon.ico`)) { 219 | res.statusCode = 404 220 | res.end() 221 | return 222 | } 223 | 224 | console.log(` - Incoming request to: ${req.url}`) 225 | const requestUrl = url.parse(req.url) 226 | 227 | if (requestUrl.path === `/`) { 228 | // Request for a letter with no attached fulfillment 229 | 230 | // Respond with a 402 HTTP Status Code (Payment Required) 231 | res.statusCode = 402 232 | 233 | // Generate a preimage and its SHA256 hash, 234 | // which we'll use as the fulfillment and condition, respectively, of the 235 | // conditional transfer. 236 | const fulfillment = crypto.randomBytes(32) 237 | const condition = sha256(fulfillment) 238 | 239 | // Get the letter that we are selling 240 | const letter = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ') 241 | .split('')[(Math.floor(Math.random() * 26))] 242 | 243 | console.log(` - Generated letter (${letter}) ` + 244 | `at http://localhost:8000${req.url}${base64url(fulfillment)}`) 245 | 246 | // Store the fulfillment (indexed by condition) to use when we get paid 247 | fulfillments[base64url(condition)] = fulfillment 248 | 249 | // Store the letter (indexed by the fulfillment) to use when the customer 250 | // requests it 251 | letters[base64url(fulfillment)] = letter 252 | 253 | console.log(` - Waiting for payment...`) 254 | 255 | res.setHeader(`Pay`, `${cost} ${account} ${base64url(condition)}`) 256 | 257 | res.end(`Please send an Interledger payment of` + 258 | ` ${normalizedCost} ${ledgerInfo.currencyCode} to ${account}` + 259 | ` using the condition ${base64url(condition)}\n` + 260 | `> node ./pay.js ${account} ${cost} ${base64url(condition)}`) 261 | } else { 262 | // Request for a letter with the fulfillment in the path 263 | 264 | // Get fulfillment from the path 265 | const fulfillmentBase64 = requestUrl.path.substring(1) 266 | 267 | // Lookup the letter we stored previously for this fulfillment 268 | const letter = letters[fulfillmentBase64] 269 | 270 | if (!letter) { 271 | // We have no record of a letter that was issued for this fulfillment 272 | 273 | // Respond with a 404 HTTP Status Code (Not Found) 274 | res.statusCode = 404 275 | 276 | console.log(' - No letter found for fulfillment: ' + 277 | fulfillmentBase64) 278 | 279 | res.end(`Unrecognized fulfillment.`) 280 | } else { 281 | // Provide the customer with their letter 282 | res.end(`Your letter: ${letter}`) 283 | 284 | console.log(` 5. Providing paid letter to customer ` + 285 | `for fulfillment ${fulfillmentBase64}`) 286 | } 287 | } 288 | }).listen(8000, function () { 289 | console.log(` - Listening on http://localhost:8000`) 290 | console.log(` 3. Visit http://localhost:8000 in your browser ` + 291 | `to buy a letter`) 292 | }) 293 | ``` 294 | 295 | This snippet defines two code paths for incoming HTTP requests (well three but the first is just a hack to ignore favicon requests). 296 | 297 | The first path handles requests to http://localhost:8000/ (i.e. no token in the URL) and defines the logic to generate a new letter, but to store this in memory until the customer pays for it. 298 | 299 | First we set the HTTP Response code to 402 (Payment Required). This is not necessary but it's a useful convention. We'll see later how this becomes more valuable. 300 | 301 | Then we generate the data required to request an Interledger payment from the customer. 302 | 303 | ```js 304 | // Generate a preimage and its SHA256 hash, 305 | // which we'll use as the fulfillment and condition, respectively, of the 306 | // conditional transfer. 307 | const fulfillment = crypto.randomBytes(32) 308 | const condition = sha256(fulfillment) 309 | ``` 310 | 311 | The first piece of data is the fulfillment that we will release to the payer when the payment is delivered. The second is the condition we give to the payer to attach to the payment. 312 | 313 | The condition is simply a SHA-256 hash of the fulfillment, meaning nobody can derive the fulfillment from the condition but it's possible to quickly verify that the condition is a hash of the fulfillment. 314 | 315 | > **NOTE:** Conditions and fulfillments are a critical aspect of Interledger which we'll explain further when we attempt to make the payment. For now it's fine to simply understand that the fulfillment is a secret held by the payee and the condition is sent by the payer with their payment. 316 | 317 | In our shop we use the fulfillment as a proof-of-payment token so we store the letter we have generated for the customer using the fulfillment as the index. 318 | 319 | The rest of that branch of the code simply returns the amount, ILP Address, and condition back to the customer so they can pay for their request. 320 | 321 | ### Interledger Addresses 322 | 323 | Note that a key piece of data we provide the customer is the ILP Address they must make the payment to. 324 | 325 | ILP Addresses are universal addresses for any account on any network or ledger. They consist of a prefix that identifies the ledger/network, an identifier for the account, and may also have a suffix that is specific to the transaction (not used in this tutorial). 326 | 327 | The prefix is an Interledger prefix, which is like an [IP subnet](https://en.wikipedia.org/wiki/Subnetwork). In this case, `test.` indicates that we are connecting to the Interledger testnet-of-testnets. 328 | 329 | The next part, `crypto.` indicates that we will be referring to a crypto currency's ledger. And finally, `xrp.` indicates that this ledger is the XRP testnet ledger. If you know the ledger prefix and the account, you can put them together to get the Interledger Address. 330 | 331 | In this case, the Interledger address of our shop's account is `test.crypto.xrp.` and was output to the console when you started the server. 332 | 333 | > **NOTE:** Having a universal addressing scheme is critical if we want a ledger/network agnostic payment protocol. All payments need a destination, and while many traditional accounts have their own addressing scheme (IBANs, PANs, PayPal addresses), there is not a single universal scheme that can be used to address any payment. 334 | 335 | > To read more about ILP Addresses and how these can be derived for accounts on both traditional and new payment networks see [IL-RFC-15, draft 1](https://interledger.org/rfcs/0015-ilp-addresses/draft-1.html). 336 | 337 | If you run the server now you'll see that it completes both step 1 and 2 of our 5 step program. 338 | 339 | :tada: Congratulations, you're making great progress to becoming a letter baron. 340 | 341 | Now let's put ourselves in the shoes of the customer and attempt to buy our first letter! 342 | 343 | ## Step 3: Paying for a letter 344 | 345 | As instructed on the console, open a browser window and go to http://localhost:8000. 346 | 347 | You should get a message along the lines of: 348 | 349 | ``` 350 | Please send an Interledger payment of 0.00001 XRP to XXXXXXXXXXXXXXXXX using the condition YYYYYYYYYYYYYYYY 351 | > node ./pay.js XXXXXXXXXXXXXXXXX 10 YYYYYYYYYYYYYYYY 352 | ``` 353 | 354 | As they say, *"There is no such thing as a free letter!"*. So now we turn our attention to the **client**, and paying for this elusive character. 355 | 356 | Open a **new console window** and make sure your current working directory is the same one you're using to run the shop. Copy the command that the shop helpfully provided and paste it into the console window. 357 | 358 | ```shell 359 | node ./pay.js XXXXXXXXXXXXXXXXX 10 YYYYYYYYYYYYYYYY 360 | ``` 361 | 362 | If you already configured the customer plugin in `plugins.js` then you'll likely see the following and then the script completes: 363 | 364 | ```shell 365 | == Starting the payment client == 366 | ``` 367 | 368 | Just like we did with the server, we need to do something with our configured plugin. So let's edit `pay.js` and replace the text `// Do something...` with the following and try again: 369 | 370 | ```js 371 | console.log(` 1. Connecting to an account to send payments...`) 372 | 373 | plugin.connect().then(function () { 374 | const ledgerInfo = plugin.getInfo() 375 | const account = plugin.getAccount() 376 | console.log(` - Connected to ledger: ${ledgerInfo.prefix}`) 377 | console.log(` -- Account: ${account}`) 378 | console.log(` -- Currency: ${ledgerInfo.currencyCode}`) 379 | console.log(` -- CurrencyScale: ${ledgerInfo.currencyScale}`) 380 | 381 | // Make payment... 382 | 383 | // Listen for fulfillments... 384 | 385 | }) 386 | ``` 387 | 388 | You'll see that the script connects to the ledger and returns similar information to that returned when we connected the shop to the same ledger using its own account credentials. 389 | 390 | But after that it simply hangs... 391 | 392 | The comments in the code probably gave it away, but we now need to make the payment. Replace the text `// Make payment...` with: 393 | 394 | ```js 395 | console.log(` 2. Making payment to ${destinationAddress} ` + 396 | `using condition: ${condition}`) 397 | 398 | // Send the transfer 399 | plugin.sendTransfer({ 400 | to: destinationAddress, 401 | amount: destinationAmount, 402 | executionCondition: condition, 403 | id: uuid(), 404 | from: plugin.getAccount(), 405 | ledger: plugin.getInfo().prefix, 406 | ilp: base64url(IlpPacket.serializeIlpPayment({ 407 | amount: destinationAmount, 408 | account: destinationAddress 409 | })), 410 | expiresAt: new Date(new Date().getTime() + 1000000).toISOString() 411 | }).then(function () { 412 | console.log(' - Transfer prepared, waiting for fulfillment...') 413 | }, function (err) { 414 | console.error(err.message) 415 | }) 416 | ``` 417 | 418 | We have parsed the `destinationAddress`, `destinationAmount`, and `condition` from the command line and now we are instructing our ledger plugin to prepare a transfer on the ledger. 419 | 420 | > Note how the amount is expressed in the scale of the ledger. We know this to be 6 therefor our payment of 0.00001 XRP is expressed as 10 Drops. 421 | 422 | > Also note how we are setting an expiry on the transfer we are preparing so that if the transfer is not fulfilled before that time it will roll-back. 423 | 424 | ### Conditional Payments and Hash Timelocked Agreements 425 | 426 | At this point we should dig a little deeper into how transfers work in Interledger. They are a little different to a regular transfer on most traditional payment networks/ledgers in that they happen in two phases: 427 | 1. The transfer is prepared pending either the fulfillment of a condition, or expiry of a timeout 428 | 1. The transfer is either executed because the condition was fulfilled, or it rolls-back because it expired 429 | 430 | In ILP we define a standard for the condition and the fulfillment. The condition is a 32-byte [SHA-256 hash](https://en.wikipedia.org/wiki/SHA-2) and the fulfillment is the 32-byte preimage of that hash. When a transfer is prepared by the sender the funds are locked, waiting for delivery to the receiver. The lock is the condition (a hash) and the key is the fulfillment. But, to avoid funds being locked up indefinitely, if the key is never produced the prepared transfer also has an expiry. 431 | 432 | > **INFO:** A hash is a one-way function. Therefor for `condition = sha256hash(fulfillment)` there is no function `f` for which `fulfillment = f(condition)`. The only way to figure out the fulfillment is to guess. Because of the size of a SHA-256 hash (32 bytes) it is estimated that all the computing power in the world would take millions of years to guess its preimage. A pretty safe lock then for a transfer that shouldn't take more than a few seconds to complete. 433 | 434 | We call this arrangement, between the parties to the transfer, a Hash Timelock Agreement (HTLA). You may have come across HTLCs or Hash Timelock Contracts as used in many crypto-currency based system like Lightning. HTLAs are a generalization of this that don't prescribe how they are enforced (i.e. It may not be a contract written in the code of the ledger like in a crypto-currency ledger, it may be an agreement enforced by law or simply based on trust). 435 | 436 | How two parties make transfers to one-another on a ledger will depend on the relationship between them and also the features of the underlying ledger. For our tutorial we are using a plugin specific to XRP Ledger, which uses [the on-ledger escrow](https://ripple.com/build/rippleapi/#transaction-types) features of that ledger. This means the parties are able to make transfers with no pre-existing relationship. 437 | 438 | For more info on the different types of HTLA have a look at [IL-RFC 0022](https://interledger.org/rfcs/0022-hashed-timelock-agreements/). 439 | 440 | 441 | > **NOTE:** ILP is a protocol for making payments that can traverse multiple networks. Therefor it's necessary to distinguish between the **payment** (from a sender to a receiver) and the one or more **transfers** on different networks/ledgers (between sender, intermediaries and the receiver) that make up the payment. 442 | 443 | > In this simple example the payment is made using only a single transfer so the destination, and amount of the transfer, is the same as the destination and amount of the payment. You can see this from the fact that the same values are passed to the `sendTransfer()` function and used to build the ILP packet (`IlpPacket.serializeIlpPayment()`) 444 | 445 | > In a later tutorial we'll expand this example to pay across multiple networks and demonstrate how these values will often be different. 446 | 447 | Now that we've added the code to make our transfer let's run the script again and pay for our letter. This time you should see a message to say that we are making our payment, and waiting for the fulfillment. 448 | 449 | :tada: Congratulations, that's step 2 of 3 completed for our client! Now switch back to the terminal where you're running the shop and let's see if the payment has arrived... 450 | 451 | It hasn't, has it? So let's figure out why. 452 | 453 | ### Finding your transfer on the ledger 454 | 455 | > The following short section is very specific to XRP Ledger but gives you an idea of what to do if you are debugging a failed payment. If you would rather skip ahead to the next section you can. 456 | 457 | Let's dissect what should have happened under the hood. You used the `pay.js` script to load the XRP Escrow plugin and prepare an escrow payment for 10 Drops (0.00001 XRP) to the address provided by the shop. 458 | 459 | Our payment hasn't arrived at the shop so let's start by inspecting the XRP Testnet Ledger and see if that payment was prepared. We're going to use the REST API exposed by the same testnet server we connect our plugin to. 460 | 461 | First you need to find the sending account address you used. Look in `plugins.js` and find the account you configured your customer plugin with, or look at the output from running `pay.js` (you only need the Ripple address so trim off the ILP ledger prefix). 462 | 463 | The following will install a small tool that formats the JSON output to make it easier to read: 464 | 465 | ```shell 466 | npm install -g jslint 467 | ``` 468 | 469 | Next use `curl` to POST an API call to the XRP Testnet server (replace the address with your sending address): 470 | 471 | ```shell 472 | curl -X POST -d '{ "method": "account_objects", "params": [{"ledger_index":"validated", "account": "YOUR-SENDING-ADDRESS", "type": "escrow"}]}' https://client.altnet.rippletest.net:51234 | jslint 473 | ``` 474 | 475 | *Installing `curl` is out of scope for this tutorial so if you don't already have it you can skip ahead or figure that bit out yourself.* 476 | 477 | You should get back a JSON object that shows all the transactions sent from your account. In among them should be the transaction you just created. 478 | 479 | So our investigation has revealed that the transfer has been created on the ledger. We need to go back to our shop service and figure out why it wasn't received. 480 | 481 | ## Step 4: Accepting the payment 482 | 483 | If you look through what we've got in `shop.js` so far you'll notice a comment `//Handle incoming transfers...`. That's our first hint that we're missing some code. 484 | 485 | This is our first introduction to some of the events that can be raised by ledger plugins. There are a number of these and the one we are interested in is called `incoming_prepare`. This event is raised whenever a transfer is prepared on the ledger and your plugin's account is the intended recipient. 486 | 487 | As soon as yur plugin connects to the ledger it will start listening for incoming transfers and should raise this event every time one occurs. 488 | 489 | Replace `//Handle incoming transfers...` with the following code: 490 | 491 | ```js 492 | // Handle incoming payments 493 | plugin.on('incoming_prepare', function (transfer) { 494 | if (parseInt(transfer.amount) < 10) { 495 | // Transfer amount is incorrect 496 | console.log(` - Payment received for the wrong amount ` + 497 | `(${transfer.amount})... Rejected`) 498 | 499 | const normalizedAmount = transfer.amount / 500 | Math.pow(10, parseInt(ledgerInfo.currencyScale)) 501 | 502 | plugin.rejectIncomingTransfer(transfer.id, { 503 | code: 'F04', 504 | name: 'Insufficient Destination Amount', 505 | message: `Please send at least 10 ${ledgerInfo.currencyCode},` + 506 | `you sent ${normalizedAmount}`, 507 | triggered_by: plugin.getAccount(), 508 | triggered_at: new Date().toISOString(), 509 | forwarded_by: [], 510 | additional_info: {} 511 | }) 512 | } else { 513 | // Lookup fulfillment from condition attached to incoming transfer 514 | const fulfillment = fulfillments[transfer.executionCondition] 515 | 516 | if (!fulfillment) { 517 | // We don't have a fulfillment for this condition 518 | console.log(` - Payment received with an unknown condition: ` + 519 | `${transfer.executionCondition}`) 520 | 521 | plugin.rejectIncomingTransfer(transfer.id, { 522 | code: 'F05', 523 | name: 'Wrong Condition', 524 | message: `Unable to fulfill the condition: ` + 525 | `${transfer.executionCondition}`, 526 | triggered_by: plugin.getAccount(), 527 | triggered_at: new Date().toISOString(), 528 | forwarded_by: [], 529 | additional_info: {} 530 | }) 531 | } 532 | 533 | console.log(` 4. Accepted payment with condition ` + 534 | `${transfer.executionCondition}.`) 535 | console.log(` - Fulfilling transfer on the ledger ` + 536 | `using fulfillment: ${base64url(fulfillment)}`) 537 | 538 | // The ledger will check if the fulfillment is correct and 539 | // if it was submitted before the transfer's rollback timeout 540 | plugin.fulfillCondition(transfer.id, base64url(fulfillment)) 541 | .catch(function () { 542 | console.log(` - Error fulfilling the transfer`) 543 | }) 544 | console.log(` - Payment complete`) 545 | } 546 | }) 547 | ``` 548 | 549 | In this code we first check that the incoming transfer is for the correct amount, and if it's not we raise an ILP Error and call the `rejectIncomingTransfer()` function to reject the transfer. If there were other business rules we wanted to evaluate before accepting the transfer we could do these here too. 550 | 551 | If the transfer is correct, we extract the condition and use this to look up the fulfillment from our local store. If we can't find it then we aren't able to fufill the transfer so we reject it. 552 | 553 | > **NOTE:** There are other ways to map a fulfillment to an incoming transfer, including techniques where the fulfillment is derived from the transaction data. For an example of such a technique have a look at the [PSK protocol](https://interledger.org/rfcs/0016-pre-shared-key/). 554 | 555 | The last step we need to perform is to fulfill the transfer so that the funds are released to us from the ledger escrow. We do this using the `fulfillCondition()` function on the plugin. 556 | 557 | > **NOTE:** There are only two ways to handle an `incoming_prepare` event as a the receiver. Reject it or fulfill it. This action then cascades back down the payment chain causing the transfers in the chain to either be fulfilled or rejected. 558 | 559 | Restart the shop service and run through the process of visiting the website and making the payment again. You should see that the output of the server now includes accepting and fulfilling the payment. 560 | 561 | :tada: Congratulations, that's step 4 of 5 complete for our server! 562 | 563 | But, our client is still waiting for the fulfillment. Let's go back to the client and finish this up! 564 | 565 | ## Step 5: Using the fulfillment to get the Letter 566 | 567 | As you've proabably guessed, we're missing an event listener in our client too. Go back to `pay.js` and replace `// Listen for fulfillments...` with the following code: 568 | 569 | ```js 570 | // Handle fulfillments 571 | plugin.on('outgoing_fulfill', function (transferId, fulfillmentBase64) { 572 | console.log(' - Transfer executed. Got fulfillment: ' + 573 | fulfillmentBase64) 574 | console.log(` 3. Collect your letter at ` + 575 | `http://localhost:8000/${fulfillmentBase64}`) 576 | plugin.disconnect() 577 | process.exit() 578 | }) 579 | ``` 580 | 581 | As the transfer initiator we are interested in fulfillments that come out of the ledger after being submitted by the receiver, so we listen for the `outgoing_fulfill` event. 582 | 583 | All we need to do with that fulfillment in this instance is print it to the screen so the user can go and collect their letter. 584 | 585 | Re-run your client and after a few seconds you should now get a response to that effect. 586 | 587 | :tada: Congratulations! You've now finished the client. 588 | 589 | So all that's left to do is get that letter! Go back to your browser and paste the URL that was output by the client into your address bar and you should receive the letter that was prepared for you upon your first request. 590 | 591 | If you switch back to the terminal window for your server you'll see some additional output showing that the shop server accepted the request, looked up the letter using the fulfillment, and returned this letter. 592 | 593 | :tada: Congratulations, that's step 5 of 5 and you now have a working server too. 594 | 595 | ## Bonus Step: Letter Shop Client 596 | 597 | It's very cumbersome to copy and paste the condition from your browser to your command-line terminal each time you need to pay for something online, and then to copy and paste back the fulfillment from your terminal to your browser once you have paid. 598 | 599 | But we can easily write a script that fetches http://localhost:8000, parses the body to see what payment is required, pays, and then fetches the letter for you. Now you can buy a letter from the Letter Shop right from your command line terminal, without having to switch to your web browser: 600 | 601 | ```js 602 | const IlpPacket = require('ilp-packet') 603 | const plugin = require('./plugins.js').xrp.Customer() 604 | const uuid = require('uuid/v4') 605 | const fetch = require('node-fetch') 606 | 607 | function base64url (buf) { 608 | return buf.toString('base64') 609 | .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') 610 | } 611 | 612 | plugin.connect().then(function () { 613 | return fetch('http://localhost:8000/') 614 | }).then(function (res) { 615 | return res.text() 616 | }).then(function (body) { 617 | const parts = body.split(' ') 618 | if (parts[0] === 'Please') { 619 | const destinationAddress = parts[16] 620 | const destinationAmount = parts[17] 621 | const condition = parts[18] 622 | return plugin.sendTransfer({ 623 | id: uuid(), 624 | from: plugin.getAccount(), 625 | to: destinationAddress, 626 | ledger: plugin.getInfo().prefix, 627 | expiresAt: new Date(new Date().getTime() + 1000000).toISOString(), 628 | amount: destinationAmount, 629 | executionCondition: condition, 630 | ilp: base64url(IlpPacket.serializeIlpPayment({ 631 | account: destinationAddress, 632 | amount: destinationAmount, 633 | data: '' 634 | })) 635 | }) 636 | } 637 | }) 638 | 639 | plugin.on('outgoing_fulfill', function (transferId, fulfillmentBase64) { 640 | fetch('http://localhost:8000/' + fulfillmentBase64).then(function (res) { 641 | return res.text() 642 | }).then(function (body) { 643 | console.log(body) 644 | return plugin.disconnect() 645 | }).then(function () { 646 | process.exit() 647 | }) 648 | }) 649 | ``` 650 | 651 | With the Letter Shop running, you can try out this script from your terminal: 652 | ```shell 653 | $ node ./client.js 654 | Your letter: B 655 | ``` 656 | 657 | As you can see, this code relies on the exact phrasing of the text; it checks if the text starts with 'Please', and if so, it simply looks for the 16th, 17th, and 18th word on the page. 658 | This is of course not a very robust design, it would be very brittle if the web design of the Letter Shop were to change. Therefore, it's better if we separate the machine-readable information 659 | into a http response header. We can use a recently proposed format for this, as described in the [draft-hope-bailie-http-payments internet draft](https://datatracker.ietf.org/doc/draft-hope-bailie-http-payments). For this, we add a line to the Letter Shop code, just before the `res.end` line: 660 | 661 | ```js 662 | console.log(` - Waiting for payment...`) 663 | 664 | res.setHeader("Pay", `interledger-condition ${cost} ${account} ${condition}`) 665 | 666 | res.end(`Please send an Interledger payment of ${normalizedCost} ${ledgerInfo.currencyCode} to ${account} using the condition ${condition}\n` + 667 | `> node ./pay.js ${account} ${cost} ${condition}`) 668 | ``` 669 | 670 | When our shop responds to the request for a letter with a *"Payment Required"* response it will include the details of how to make the payment in the *"Pay"* header. 671 | 672 | Clients that know how to interpret that header can process it and make the payment without relying on a specific phrasing on the human-readable web page. 673 | 674 | You can now update your client script, so that instead of parsing the words in the response body, it looks at the response headers: 675 | 676 | ```js 677 | plugin.connect().then(function () { 678 | return fetch('http://localhost:8000/') 679 | }).then(function (res) { 680 | const parts = res.headers.get('Pay').split(' ') 681 | if (parts[0] === 'interledger-condition') { 682 | const destinationAmount = parts[1] 683 | const destinationAddress = parts[2] 684 | const condition = parts[3] 685 | return plugin.sendTransfer({ 686 | // ... 687 | ``` 688 | 689 | You can see the completed `client.js` script [here](./completed/client.js). That's all for this tutorial! 690 | 691 | ## What have we learned? 692 | 693 | The plugin used in all three scripts exposes the Ledger Plugin Interface (LPI) as described in [IL-RPC-4, draft 6](https://interledger.org/rfcs/0004-ledger-plugin-interface/draft-6.html), and of that, this script uses the following methods and events: 694 | * `sendTransfer` method prepares a transfer to some other account on the same ledger. 695 | * `getInfo` method to get information about the ledger the plugin is connected to 696 | * `getAccount` method to get the account ILP Address of the plugin 697 | * `rejectIncomingTransfer` method rejects an incoming transfer if someone tries to pay the wrong amount or the condition is invalid 698 | * `fulfillCondition` method fulfills the condition of an incoming transfer 699 | * `incoming_prepare` event is triggered when someone else sends you a conditional transfer 700 | * `outgoing_fulfill` event is triggered when someone else fulfills your conditional transfer 701 | 702 | ## What's next? 703 | 704 | In the next tutorial, called 'http-ilp', we will add more features to this `Pay` header: [next tutorial](/tutorials/http-ilp) 705 | 706 | Also, if you read the paragraphs above, you will have seen quite a few new words; see the glossary in [IL-RFC-19, draft 1](https://interledger.org/rfcs/0019-glossary/draft-1.html) as a reference if you forget some of them. 707 | -------------------------------------------------------------------------------- /letter-shop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "letter-shop-tutorial", 3 | "version": "1.0.0", 4 | "description": "Interledger Letter Shop Tutorial", 5 | "main": "pay.js", 6 | "dependencies": { 7 | "ilp-packet": "^1.3.0", 8 | "ilp-plugin-payment-channel-framework": "github:interledgerjs/ilp-plugin-payment-channel-framework#bs-clp", 9 | "ilp-plugin-xrp-escrow": "^1.0.5", 10 | "jsonlint": "^1.6.2", 11 | "node-fetch": "^1.7.3", 12 | "uuid": "^3.1.0" 13 | }, 14 | "devDependencies": { 15 | "chai": "^4.1.1", 16 | "eslint": "^4.5.0", 17 | "eslint-config-standard": "^10.2.1", 18 | "eslint-plugin-import": "^2.7.0", 19 | "eslint-plugin-node": "^5.1.0", 20 | "eslint-plugin-promise": "^3.5.0", 21 | "eslint-plugin-standard": "^3.0.1", 22 | "mocha": "^3.4.1" 23 | }, 24 | "scripts": { 25 | "test": "eslint ." 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/interledger/tutorials.git" 30 | }, 31 | "author": "", 32 | "license": "ISC", 33 | "bugs": { 34 | "url": "https://github.com/interledger/tutorials/issues" 35 | }, 36 | "homepage": "https://github.com/interledger/tutorials#readme", 37 | "eslintConfig": { 38 | "rules": { 39 | "max-len": [ 40 | "error", 41 | 80 42 | ], 43 | "no-unused-vars": ["off"] 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /letter-shop/pay.js: -------------------------------------------------------------------------------- 1 | const IlpPacket = require('ilp-packet') 2 | const plugin = require('./plugins.js').xrp.Customer() 3 | const uuid = require('uuid/v4') 4 | 5 | function base64url (buf) { 6 | return buf.toString('base64') 7 | .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') 8 | } 9 | 10 | const destinationAddress = process.argv[2] 11 | const destinationAmount = process.argv[3] 12 | const condition = process.argv[4] 13 | 14 | console.log(`== Starting the payment client == `) 15 | 16 | // Do something... 17 | -------------------------------------------------------------------------------- /letter-shop/plugins.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file provides a convenient place to get access to all of the 3 | * plugins we want to use. You can add any plugins here and use them 4 | * in from other scripts. 5 | * 6 | * If you're looking for other plugins you can start with the Interledger 7 | * GitHub repo. All ILP plugin repositories on start with 'ilp-plugin-'. 8 | * 9 | * See: https://github.com/search?utf8=%E2%9C%93&q=ilp-plugin- 10 | * 11 | * We will start by using the XRP testnet, and the 'ilp-plugin-xrp-escrow' 12 | * plugin. 13 | */ 14 | 15 | /* eslint-disable no-unused-vars */ 16 | const XrpEscrowPlugin = require('ilp-plugin-xrp-escrow') 17 | /* eslint-enable no-unused-vars */ 18 | 19 | // TODO Comment this out and uncomment the block below 20 | // after adding valid plugin configurations 21 | exports.xrp = { 22 | Customer: function () { 23 | console.error(`No account configured yet for the Customer.` + 24 | `See 'plugins.js'.`) 25 | process.exit() 26 | }, 27 | Shop: function () { 28 | console.error(`No account configured yet for the Shop.` + 29 | `See 'plugins.js'.`) 30 | process.exit() 31 | } 32 | } 33 | /** 34 | * To get an account and secret to use for the tutorials: 35 | * 36 | * 1. Go to https://ripple.com/build/xrp-test-net/ 37 | * 2. Generate Credentials 38 | * 3. Copy the account and secret into one of the plugin configurations below 39 | * 4. Repeat steps 2 and 3 for the second account 40 | * 5. Copy WEBSOCKETS address from the Test Net Servers info on the same page 41 | */ 42 | 43 | // exports.xrp = { 44 | // Customer: function () { 45 | // return new XrpEscrowPlugin({ 46 | // secret: '', 47 | // account: '', 48 | // server: 'wss://s.altnet.rippletest.net:51233', 49 | // prefix: 'test.crypto.xrp.' 50 | // }) 51 | // }, 52 | // Shop: function () { 53 | // return new XrpEscrowPlugin({ 54 | // secret: '', 55 | // account: '', 56 | // server: 'wss://s.altnet.rippletest.net:51233', 57 | // prefix: 'test.crypto.xrp.' 58 | // }) 59 | // } 60 | // } 61 | -------------------------------------------------------------------------------- /letter-shop/shop.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const url = require('url') 3 | const crypto = require('crypto') 4 | const plugin = require('./plugins.js').xrp.Shop() 5 | 6 | function base64url (buf) { 7 | return buf.toString('base64') 8 | .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') 9 | } 10 | 11 | function sha256 (preimage) { 12 | return crypto.createHash('sha256').update(preimage).digest() 13 | } 14 | 15 | let fulfillments = {} 16 | let letters = {} 17 | const cost = 10 18 | 19 | console.log(`== Starting the shop server == `) 20 | 21 | // Do something... 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tutorials", 3 | "version": "1.0.0", 4 | "description": "Interledger tutorials", 5 | "main": "pay.js", 6 | "dependencies": { 7 | "ilp-packet": "^1.3.0", 8 | "ilp-plugin-payment-channel-framework": "^1.2.0", 9 | "ilp-plugin-xrp-escrow": "github:michielbdejong/ilp-plugin-xrp-escrow#72e4178", 10 | "koa": "^2.3.0", 11 | "koa-ilp": "0.0.1", 12 | "koa-router": "^7.2.1", 13 | "node-fetch": "^1.7.3", 14 | "uuid": "^3.1.0" 15 | }, 16 | "devDependencies": { 17 | "chai": "^4.1.1", 18 | "eslint": "^4.5.0", 19 | "eslint-config-standard": "^10.2.1", 20 | "eslint-plugin-import": "^2.7.0", 21 | "eslint-plugin-node": "^5.1.0", 22 | "eslint-plugin-promise": "^3.5.0", 23 | "eslint-plugin-standard": "^3.0.1", 24 | "mocha": "^3.4.1" 25 | }, 26 | "scripts": { 27 | "test": "eslint ." 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "git+https://github.com/interledger/tutorials.git" 32 | }, 33 | "author": "", 34 | "license": "ISC", 35 | "bugs": { 36 | "url": "https://github.com/interledger/tutorials/issues" 37 | }, 38 | "homepage": "https://github.com/interledger/tutorials#readme" 39 | } 40 | -------------------------------------------------------------------------------- /streaming-payments/index.md: -------------------------------------------------------------------------------- 1 | # Streaming Tutorial 2 | 3 | ## What you need before you start: 4 | 5 | * complete the [Letter Shop](/tutorials/letter-shop) and [http-ilp](/tutorials/http-ilp) tutorials first 6 | 7 | ## What you'll learn: 8 | 9 | * streaming payments 10 | 11 | ## Buying letters repeatedly 12 | 13 | In the Letter Shop tutorial we created a command-line client that can buy letters 14 | from the Letter Shop. In the http-ilp tutorial we updated the shop and the client 15 | so that instead of telling the client directly which hashlock condition to use, 16 | the shop sends a PSK shared secret, with which the client could in principle derive 17 | endless condition/fulfillment pairs. 18 | 19 | In practice, the `client2.js` script from the http-ilp tutorial only derived one 20 | (paymentId 0), so that's what we'll change first, in `streaming-client1.js`; it will 21 | send one payment per second, indefinitely: 22 | 23 | ```js 24 | if (parts[0] === 'interledger-psk') { 25 | let paymentId = 0 26 | setInterval(function () { 27 | const destinationAmount = parts[1] 28 | const destinationAddress = parts[2] + '.' + paymentId 29 | const sharedSecret = Buffer.from(parts[3], 'base64') 30 | const ilpPacket = IlpPacket.serializeIlpPayment({ 31 | account: destinationAddress, 32 | amount: destinationAmount, 33 | data: '' 34 | }) 35 | console.log('Calculating hmac using shared secret:', base64url(sharedSecret)) 36 | const fulfillmentGenerator = hmac(sharedSecret, 'ilp_psk_condition') 37 | const fulfillment = hmac(fulfillmentGenerator, ilpPacket) 38 | const condition = sha256(fulfillment) 39 | plugin.sendTransfer({ 40 | id: uuid(), 41 | from: plugin.getAccount(), 42 | to: destinationAddress, 43 | ledger: plugin.getInfo().prefix, 44 | expiresAt: new Date(new Date().getTime() + 1000000).toISOString(), 45 | amount: destinationAmount, 46 | executionCondition: base64url(condition), 47 | ilp: base64url(ilpPacket) 48 | }) 49 | paymentId++ 50 | }, 1000) 51 | } 52 | ``` 53 | 54 | And the `outgoing_fulfill` handler will no longer disconnect the plugin or exit the process: 55 | 56 | ```js 57 | plugin.on('outgoing_fulfill', function (transferId, fulfillmentBase64) { 58 | fetch('http://localhost:8000/', { 59 | headers: { 60 | 'Pay-Token': base64url(sharedSecret) 61 | } 62 | }).then(function (res) { 63 | return res.text() 64 | }).then(function (body) { 65 | console.log(body) 66 | }) 67 | }) 68 | ``` 69 | 70 | To see this in action, copy your `'plugins.js'` file [from before](../letter-shop) into the 'streaming-payments/' 71 | folder of your local copy of the [tutorials repository](https://github.com/interledger/tutorials), 72 | run `node ./shop-from-before.js` in one terminal window, and `node ./streaming-client1.js` in another. 73 | 74 | ## Mid-request payments 75 | 76 | One thing you may have noticed is that this client does one initial request, and then for each letter it does 77 | one payment and one request to retrieve the new letter. This is not a very efficient way to stream letters. 78 | 79 | Therefore, we're going to change the way the shop serves its `Pay` header. Until now, it was serving it on a 402 status 80 | response, which is then terminated immediately. The client then ends up disconnected from the shop, and needs to 81 | reconnect after having completed payment. But we live in a fast world, where consumption is a basic right, 82 | and in order to consume more, faster, we of course want to be connected. 83 | 84 | So we'll change the Letter Shop from before so that instead of terminating the http response with a 402 status, 85 | it will serve up a 200 status, flush the headers, and stall the delivery of the body. The body will consist of the 86 | letters the client buys, and will keep streaming them indefinitely, until the client interrupts the connection, 87 | or the server process is terminated: 88 | 89 | 90 | We'll change the Letter Shop from the previous tutorial a bit, to `shop2.js`. Instead of 91 | using a human-readable "Payment Required" message that starts with "Please ...", we will 92 | now use a machine-readable http header, and a fulfillment/condition pair that is generated 93 | deterministically from a secret that's shared between the shop and the client. 94 | 95 | Starting with the last part, `http.createServer`, you can see the flow of the http server 96 | is a bit simpler; when a request comes in it sends headers, and 97 | then the body will be sent letter-by-letter, as payments come in: 98 | 99 | ```js 100 | res.writeHead(200, { 101 | 'Pay': [ 1, plugin.getAccount() + '.' + user, base64(secret) ].join(' ') 102 | }) 103 | // Flush the headers in a first TCP packet: 104 | res.socket.write(res._header) 105 | res._headerSent = true 106 | ``` 107 | 108 | In the client, instead of having to do a dedicated `fetch` call for each time `'outgoing_fulfill'` is triggered, 109 | we just stream the body from the initial call: 110 | 111 | ```js 112 | res.body.pipe(process.stdout) 113 | ``` 114 | 115 | To see this in action, run `node ./streaming-shop.js` in one terminal window, and `node ./streaming-client2.js` in another. 116 | You can now maybe think about whay you want to build with Interledger - a shop? or a client? and for your use case, would it 117 | be appropriate to use streaming content and streaming payments instead of one-shot requests and one-shot payments? 118 | 119 | You may have noticed that paying via the XRP testnet ledger takes a few seconds. That's why, in the previous tutorial, we introduced 120 | the `Pay-Token` / `Pay-Balance` protocol. But there is another way to make streaming Interledger payments faster, as we'll see in 121 | the next tutorial: [Hosted Ledgers](/tutorials/hosted-ledgers). 122 | -------------------------------------------------------------------------------- /streaming-payments/shop-from-before.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const url = require('url') 3 | const crypto = require('crypto') 4 | const plugin = require('./plugins.js').xrp.Shop() 5 | const IlpPacket = require('ilp-packet') 6 | 7 | function base64url (buf) { 8 | return buf.toString('base64') 9 | .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') 10 | } 11 | 12 | function sha256 (preimage) { 13 | return crypto.createHash('sha256').update(preimage).digest() 14 | } 15 | 16 | function hmac (secret, input) { 17 | return crypto.createHmac('sha256', secret).update(input).digest() 18 | } 19 | 20 | let sharedSecrets = {} 21 | let letters = {} 22 | const cost = 10 23 | 24 | console.log(`== Starting the shop server == `) 25 | console.log(` 1. Connecting to an account to accept payments...`) 26 | 27 | plugin.connect().then(function () { 28 | // Get ledger and account information from the plugin 29 | const ledgerInfo = plugin.getInfo() 30 | const account = plugin.getAccount() 31 | 32 | console.log(` - Connected to ledger: ${ledgerInfo.prefix}`) 33 | console.log(` -- Account: ${account}`) 34 | console.log(` -- Currency: ${ledgerInfo.currencyCode}`) 35 | console.log(` -- CurrencyScale: ${ledgerInfo.currencyScale}`) 36 | 37 | // Convert our cost (10) into the right format given the ledger scale 38 | const normalizedCost = cost / Math.pow(10, parseInt(ledgerInfo.currencyScale)) 39 | 40 | console.log(` 2. Starting web server to accept requests...`) 41 | console.log(` - Charging ${normalizedCost} ${ledgerInfo.currencyCode}`) 42 | 43 | // Handle incoming web requests 44 | http.createServer(function (req, res) { 45 | // Browsers are irritiating and often probe for a favicon, just ignore 46 | if (req.url.startsWith(`/favicon.ico`)) { 47 | res.statusCode = 404 48 | res.end() 49 | return 50 | } 51 | 52 | console.log(` - Incoming request to: ${req.url}`) 53 | const requestUrl = url.parse(req.url) 54 | 55 | if (requestUrl.path === `/`) { 56 | // Request for a letter with no attached fulfillment 57 | 58 | // Respond with a 402 HTTP Status Code (Payment Required) 59 | res.statusCode = 402 60 | 61 | // Generate a client ID and a shared secret from which this client 62 | // can derive fulfillment/condition pairs. 63 | const clientId = base64url(crypto.randomBytes(8)) 64 | const sharedSecret = crypto.randomBytes(32) 65 | 66 | // Store the shared secret to use when we get paid 67 | sharedSecrets[clientId] = sharedSecret 68 | 69 | console.log(` - Waiting for payment...`) 70 | 71 | res.setHeader(`Pay`, `interledger-psk ${cost} ${account}.${clientId} ${base64url(sharedSecret)}`) 72 | 73 | res.end(`Please send an Interledger-PSK payment of` + 74 | ` ${normalizedCost} ${ledgerInfo.currencyCode} to ${account}.${clientId}` + 75 | ` using the shared secret ${base64url(sharedSecret)}\n`) 76 | } else { 77 | // Request for a letter with the fulfillment in the path 78 | 79 | // Get fulfillment from the path 80 | const fulfillmentBase64 = requestUrl.path.substring(1) 81 | 82 | // Lookup the letter we stored previously for this fulfillment 83 | const letter = letters[fulfillmentBase64] 84 | 85 | if (!letter) { 86 | // We have no record of a letter that was issued for this fulfillment 87 | 88 | // Respond with a 404 HTTP Status Code (Not Found) 89 | res.statusCode = 404 90 | 91 | console.log(' - No letter found for fulfillment: ' + 92 | fulfillmentBase64) 93 | 94 | res.end(`Unrecognized fulfillment.`) 95 | } else { 96 | // Provide the customer with their letter 97 | res.end(`Your letter: ${letter}`) 98 | 99 | console.log(` 5. Providing paid letter to customer ` + 100 | `for fulfillment ${fulfillmentBase64}`) 101 | } 102 | } 103 | }).listen(8000, function () { 104 | console.log(` - Listening on http://localhost:8000`) 105 | console.log(` 3. Visit http://localhost:8000 in your browser ` + 106 | `to buy a letter`) 107 | }) 108 | 109 | // Handle incoming payments 110 | plugin.on('incoming_prepare', function (transfer) { 111 | if (parseInt(transfer.amount) < 10) { 112 | // Transfer amount is incorrect 113 | console.log(` - Payment received for the wrong amount ` + 114 | `(${transfer.amount})... Rejected`) 115 | 116 | const normalizedAmount = transfer.amount / 117 | Math.pow(10, parseInt(ledgerInfo.currencyScale)) 118 | 119 | plugin.rejectIncomingTransfer(transfer.id, { 120 | code: 'F04', 121 | name: 'Insufficient Destination Amount', 122 | message: `Please send at least 10 ${ledgerInfo.currencyCode},` + 123 | `you sent ${normalizedAmount}`, 124 | triggered_by: plugin.getAccount(), 125 | triggered_at: new Date().toISOString(), 126 | forwarded_by: [], 127 | additional_info: {} 128 | }) 129 | return 130 | } 131 | // Generate fulfillment from packet and this client's shared secret 132 | const ilpPacket = Buffer.from(transfer.ilp, 'base64') 133 | const payment = IlpPacket.deserializeIlpPayment(ilpPacket) 134 | const clientId = payment.account.substring(plugin.getAccount().length + 1).split('.')[0] 135 | const secret = sharedSecrets[clientId] 136 | 137 | if (!clientId || !secret) { 138 | // We don't have a fulfillment for this condition 139 | console.log(` - Payment received with an unknown condition: ` + 140 | `${transfer.executionCondition}`) 141 | 142 | plugin.rejectIncomingTransfer(transfer.id, { 143 | code: 'F05', 144 | name: 'Wrong Condition', 145 | message: `Unable to fulfill the condition: ` + 146 | `${transfer.executionCondition}`, 147 | triggered_by: plugin.getAccount(), 148 | triggered_at: new Date().toISOString(), 149 | forwarded_by: [], 150 | additional_info: {} 151 | }) 152 | return 153 | } 154 | console.log(` - Calculating hmac; for clientId ${clientId}, the shared secret is ${base64url(secret)}.`) 155 | const fulfillmentGenerator = hmac(secret, 'ilp_psk_condition') 156 | const fulfillment = hmac(fulfillmentGenerator, ilpPacket) 157 | 158 | // Get the letter that we are selling 159 | const letter = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ') 160 | .split('')[(Math.floor(Math.random() * 26))] 161 | 162 | console.log(` - Generated letter (${letter}) `) 163 | 164 | // Store the letter (indexed by the fulfillment) to use when the customer 165 | // requests it 166 | letters[base64url(fulfillment)] = letter 167 | 168 | console.log(` 4. Accepted payment with condition ` + 169 | `${transfer.executionCondition}.`) 170 | console.log(` - Fulfilling transfer on the ledger ` + 171 | `using fulfillment: ${base64url(fulfillment)}`) 172 | 173 | // The ledger will check if the fulfillment is correct and 174 | // if it was submitted before the transfer's rollback timeout 175 | plugin.fulfillCondition(transfer.id, base64url(fulfillment)) 176 | .catch(function () { 177 | console.log(` - Error fulfilling the transfer`) 178 | }) 179 | console.log(` - Payment complete`) 180 | 181 | }) 182 | }) 183 | -------------------------------------------------------------------------------- /streaming-payments/streaming-client1.js: -------------------------------------------------------------------------------- 1 | const IlpPacket = require('ilp-packet') 2 | const plugin = require('./plugins.js').xrp.Customer() 3 | const uuid = require('uuid/v4') 4 | const fetch = require('node-fetch') 5 | const crypto = require('crypto') 6 | 7 | function base64url (buf) { 8 | return buf.toString('base64') 9 | .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') 10 | } 11 | 12 | function sha256 (preimage) { 13 | return crypto.createHash('sha256').update(preimage).digest() 14 | } 15 | 16 | function hmac (secret, input) { 17 | return crypto.createHmac('sha256', secret).update(input).digest() 18 | } 19 | 20 | plugin.connect().then(function () { 21 | return fetch('http://localhost:8000/') 22 | }).then(function (res) { 23 | const parts = res.headers.get('Pay').split(' ') 24 | if (parts[0] === 'interledger-psk') { 25 | let paymentId = 0 26 | setInterval(function () { 27 | const destinationAmount = parts[1] 28 | const destinationAddress = parts[2] + '.' + paymentId 29 | const sharedSecret = Buffer.from(parts[3], 'base64') 30 | const ilpPacket = IlpPacket.serializeIlpPayment({ 31 | account: destinationAddress, 32 | amount: destinationAmount, 33 | data: '' 34 | }) 35 | console.log('Calculating hmac using shared secret:', base64url(sharedSecret)) 36 | const fulfillmentGenerator = hmac(sharedSecret, 'ilp_psk_condition') 37 | const fulfillment = hmac(fulfillmentGenerator, ilpPacket) 38 | const condition = sha256(fulfillment) 39 | plugin.sendTransfer({ 40 | id: uuid(), 41 | from: plugin.getAccount(), 42 | to: destinationAddress, 43 | ledger: plugin.getInfo().prefix, 44 | expiresAt: new Date(new Date().getTime() + 1000000).toISOString(), 45 | amount: destinationAmount, 46 | executionCondition: base64url(condition), 47 | ilp: base64url(ilpPacket) 48 | }) 49 | paymentId++ 50 | }, 1000) 51 | } 52 | }) 53 | 54 | plugin.on('outgoing_fulfill', function (transferId, fulfillmentBase64) { 55 | fetch('http://localhost:8000/' + fulfillmentBase64).then(function (res) { 56 | return res.text() 57 | }).then(function (body) { 58 | console.log(body) 59 | }) 60 | }) 61 | -------------------------------------------------------------------------------- /streaming-payments/streaming-client2.js: -------------------------------------------------------------------------------- 1 | const IlpPacket = require('ilp-packet') 2 | const plugin = require('./plugins.js').xrp.Customer() 3 | const uuid = require('uuid/v4') 4 | const fetch = require('node-fetch') 5 | const crypto = require('crypto') 6 | 7 | function base64url (buf) { 8 | return buf.toString('base64') 9 | .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') 10 | } 11 | 12 | function sha256 (preimage) { 13 | return crypto.createHash('sha256').update(preimage).digest() 14 | } 15 | 16 | function hmac (secret, input) { 17 | return crypto.createHmac('sha256', secret).update(input).digest() 18 | } 19 | 20 | plugin.connect().then(function () { 21 | return fetch('http://localhost:8000/') 22 | }).then(function (res) { 23 | const parts = res.headers.get('Pay').split(' ') 24 | if (parts[0] === 'interledger-psk') { 25 | let paymentId = 0 26 | setInterval(function () { 27 | const destinationAmount = parts[1] 28 | const destinationAddress = parts[2] + '.' + paymentId 29 | const sharedSecret = Buffer.from(parts[3], 'base64') 30 | const ilpPacket = IlpPacket.serializeIlpPayment({ 31 | account: destinationAddress, 32 | amount: destinationAmount, 33 | data: '' 34 | }) 35 | process.stdout.write('.') 36 | const fulfillmentGenerator = hmac(sharedSecret, 'ilp_psk_condition') 37 | const fulfillment = hmac(fulfillmentGenerator, ilpPacket) 38 | const condition = sha256(fulfillment) 39 | plugin.sendTransfer({ 40 | id: uuid(), 41 | from: plugin.getAccount(), 42 | to: destinationAddress, 43 | ledger: plugin.getInfo().prefix, 44 | expiresAt: new Date(new Date().getTime() + 1000000).toISOString(), 45 | amount: destinationAmount, 46 | executionCondition: base64url(condition), 47 | ilp: base64url(ilpPacket) 48 | }) 49 | paymentId++ 50 | }, 1000) 51 | } 52 | res.body.pipe(process.stdout) 53 | }) 54 | -------------------------------------------------------------------------------- /streaming-payments/streaming-shop.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const url = require('url') 3 | const crypto = require('crypto') 4 | const plugin = require('./plugins.js').xrp.Shop() 5 | const IlpPacket = require('ilp-packet') 6 | 7 | function base64url (buf) { 8 | return buf.toString('base64') 9 | .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') 10 | } 11 | 12 | function sha256 (preimage) { 13 | return crypto.createHash('sha256').update(preimage).digest() 14 | } 15 | 16 | function hmac (secret, input) { 17 | return crypto.createHmac('sha256', secret).update(input).digest() 18 | } 19 | 20 | let sharedSecrets = {} 21 | const cost = 10 22 | 23 | console.log(`== Starting the shop server == `) 24 | console.log(` 1. Connecting to an account to accept payments...`) 25 | 26 | plugin.connect().then(function () { 27 | // Get ledger and account information from the plugin 28 | const ledgerInfo = plugin.getInfo() 29 | const account = plugin.getAccount() 30 | 31 | console.log(` - Connected to ledger: ${ledgerInfo.prefix}`) 32 | console.log(` -- Account: ${account}`) 33 | console.log(` -- Currency: ${ledgerInfo.currencyCode}`) 34 | console.log(` -- CurrencyScale: ${ledgerInfo.currencyScale}`) 35 | 36 | // Convert our cost (10) into the right format given the ledger scale 37 | const normalizedCost = cost / Math.pow(10, parseInt(ledgerInfo.currencyScale)) 38 | 39 | console.log(` 2. Starting web server to accept requests...`) 40 | console.log(` - Charging ${normalizedCost} ${ledgerInfo.currencyCode}`) 41 | 42 | // Handle incoming web requests 43 | http.createServer(function (req, res) { 44 | // Generate a client ID and a shared secret from which this client 45 | // can derive fulfillment/condition pairs. 46 | const clientId = base64url(crypto.randomBytes(8)) 47 | const sharedSecret = crypto.randomBytes(32) 48 | 49 | // Store the shared secret and the http request context to use when we get paid 50 | sharedSecrets[clientId] = { sharedSecret, res } 51 | 52 | console.log(` - Waiting for payments...`) 53 | 54 | res.writeHead(200, { 55 | Pay: `interledger-psk ${cost} ${account}.${clientId} ${base64url(sharedSecret)}` 56 | }) 57 | // Flush the headers in a first TCP packet: 58 | res.socket.write(res._header) 59 | res._headerSent = true 60 | }).listen(8000, function () { 61 | console.log(` - Listening on http://localhost:8000`) 62 | }) 63 | 64 | // Handle incoming payments 65 | plugin.on('incoming_prepare', function (transfer) { 66 | if (parseInt(transfer.amount) < 10) { 67 | // Transfer amount is incorrect 68 | console.log(` - Payment received for the wrong amount ` + 69 | `(${transfer.amount})... Rejected`) 70 | 71 | const normalizedAmount = transfer.amount / 72 | Math.pow(10, parseInt(ledgerInfo.currencyScale)) 73 | 74 | plugin.rejectIncomingTransfer(transfer.id, { 75 | code: 'F04', 76 | name: 'Insufficient Destination Amount', 77 | message: `Please send at least 10 ${ledgerInfo.currencyCode},` + 78 | `you sent ${normalizedAmount}`, 79 | triggered_by: plugin.getAccount(), 80 | triggered_at: new Date().toISOString(), 81 | forwarded_by: [], 82 | additional_info: {} 83 | }) 84 | return 85 | } 86 | // Generate fulfillment from packet and this client's shared secret 87 | const ilpPacket = Buffer.from(transfer.ilp, 'base64') 88 | const payment = IlpPacket.deserializeIlpPayment(ilpPacket) 89 | const clientId = payment.account.substring(plugin.getAccount().length + 1).split('.')[0] 90 | const secret = sharedSecrets[clientId].sharedSecret 91 | const res = sharedSecrets[clientId].res 92 | 93 | if (!clientId || !secret) { 94 | // We don't have a fulfillment for this condition 95 | console.log(` - Payment received with an unknown condition: ` + 96 | `${transfer.executionCondition}`) 97 | 98 | plugin.rejectIncomingTransfer(transfer.id, { 99 | code: 'F05', 100 | name: 'Wrong Condition', 101 | message: `Unable to fulfill the condition: ` + 102 | `${transfer.executionCondition}`, 103 | triggered_by: plugin.getAccount(), 104 | triggered_at: new Date().toISOString(), 105 | forwarded_by: [], 106 | additional_info: {} 107 | }) 108 | return 109 | } 110 | console.log(` - Calculating hmac; for clientId ${clientId}, the shared secret is ${base64url(secret)}.`) 111 | const fulfillmentGenerator = hmac(secret, 'ilp_psk_condition') 112 | const fulfillment = hmac(fulfillmentGenerator, ilpPacket) 113 | 114 | // Get the letter that we are selling 115 | const letter = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ') 116 | .split('')[(Math.floor(Math.random() * 26))] 117 | 118 | console.log(` - Generated letter (${letter}) `) 119 | res.write(letter) 120 | 121 | console.log(` 4. Accepted payment with condition ` + 122 | `${transfer.executionCondition}.`) 123 | console.log(` - Fulfilling transfer on the ledger ` + 124 | `using fulfillment: ${base64url(fulfillment)}`) 125 | 126 | // The ledger will check if the fulfillment is correct and 127 | // if it was submitted before the transfer's rollback timeout 128 | plugin.fulfillCondition(transfer.id, base64url(fulfillment)) 129 | .catch(function () { 130 | console.log(` - Error fulfilling the transfer`) 131 | }) 132 | console.log(` - Payment complete`) 133 | 134 | }) 135 | }) 136 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | env: 2 | node: true 3 | mocha: true 4 | --------------------------------------------------------------------------------