├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── circle.yml ├── docs └── README.template.md ├── ilp_logo.png ├── npmrc-env ├── package.json └── src ├── index.js └── payment.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | docs/*.intermediate.md 3 | 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Interledger.js Repositories 2 | 3 | ## Getting Involved 4 | 5 | Detailed contribution guidelines are coming soon... 6 | 7 | ## Contributor License Agreement 8 | 9 | This project uses the [JS Foundation](https://js.foundation) CLA for licensing contributions. 10 | 11 | Please submit your pull request as you would with any other open source project on GitHub and our CLA Assistant bot will guide you through the signing process. 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 JS Foundation and contributors 2 | Copyright 2015-2016 Ripple, Inc. and contributors 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 5 | this file except in compliance with the License. You may obtain a copy of the 6 | License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED - Use the [`ilp` module](https://github.com/interledgerjs/ilp) instead 2 | 3 |

4 | 5 |
6 | Five Bells Wallet Client 7 |

8 | 9 |

10 | A high-level JS library for sending and receiving Interledger payments. 11 |

12 | 13 |
14 | 15 | [![npm][npm-image]][npm-url] [![standard][standard-image]][standard-url] [![circle][circle-image]][circle-url] 16 | 17 | [npm-image]: https://img.shields.io/npm/v/five-bells-wallet-client.svg?style=flat 18 | [npm-url]: https://npmjs.org/package/five-bells-wallet-client 19 | [standard-image]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat 20 | [standard-url]: http://standardjs.com/ 21 | [circle-image]: https://img.shields.io/circleci/project/interledgerjs/five-bells-wallet-client/master.svg?style=flat 22 | [circle-url]: https://circleci.com/gh/interledgerjs/five-bells-wallet-client 23 | 24 | ## Installation 25 | 26 | `npm install five-bells-wallet-client --save` 27 | 28 | ## Usage 29 | 30 | This is a client for the [`five-bells-wallet`](https://github.com/interledgerjs/five-bells-wallet). 31 | 32 | To use it with a hosted demo wallet, create an account on [red.ilpdemo.org](https://red.ilpdemo.org) or [blue.ilpdemo.org](https://blue.ilpdemo.org) (it doesn't matter which because they're connected via the [Interledger Protocol](https://interledger.org)!). 33 | 34 | 35 | ### Sending 36 | 37 | ```js 38 | const WalletClient = require('five-bells-wallet-client') 39 | 40 | const sender = new WalletClient({ 41 | address: 'alice@red.ilpdemo.org', 42 | password: 'alice' 43 | }) 44 | 45 | sender.on('connect', () => { 46 | console.log('Sender connected') 47 | }) 48 | 49 | sender.send({ 50 | destination: 'bob@blue.ilpdemo.org', 51 | destinationAmount: '0.01', 52 | message: 'Still love you!', 53 | onQuote: (payment) => { 54 | console.log('Received a quote; this will cost us: ' + payment.sourceAmount) 55 | } 56 | }).then((payment) => { 57 | console.log('Sent payment:', payment) 58 | console.log('') 59 | }).catch((err) => { 60 | console.error(err.stack) 61 | }) 62 | ``` 63 | 64 | ### Receiving 65 | 66 | ```js 67 | const WalletClient = require('five-bells-wallet-client') 68 | 69 | const receiver = new WalletClient({ 70 | address: 'bob@blue.ilpdemo.org', 71 | password: 'bobbob' 72 | }) 73 | 74 | receiver.on('connect', () => { 75 | console.log('Receiver connected') 76 | }) 77 | 78 | receiver.on('incoming', (payment) => { 79 | console.log('Received ' + payment.destinationAmount + ' bucks!') 80 | console.log(payment.sourceAccount + ' says: ' + payment.message) 81 | }) 82 | ``` 83 | 84 | ### Combined Example 85 | 86 | ```js 87 | const WalletClient = require('five-bells-wallet-client') 88 | 89 | const sender = new WalletClient({ 90 | address: 'alice@red.ilpdemo.org', 91 | password: 'alice' 92 | }) 93 | 94 | const receiver = new WalletClient({ 95 | address: 'bob@blue.ilpdemo.org', 96 | password: 'bobbob' 97 | }) 98 | 99 | sender.on('connect', () => { 100 | console.log('Sender connected') 101 | }) 102 | 103 | receiver.on('connect', () => { 104 | console.log('Receiver connected') 105 | }) 106 | 107 | receiver.on('incoming', (payment) => { 108 | console.log('Received ' + payment.destinationAmount + ' bucks!') 109 | console.log(payment.sourceAccount + ' says: ' + payment.message) 110 | }) 111 | 112 | sender.send({ 113 | destination: 'bob@blue.ilpdemo.org', 114 | destinationAmount: '0.01', 115 | message: 'Still love you!', 116 | onQuote: (payment) => { 117 | console.log('Received a quote; this will cost us: ' + payment.sourceAmount) 118 | } 119 | }).then((payment) => { 120 | console.log('Sent payment:', payment) 121 | console.log('') 122 | }).catch((err) => { 123 | console.error(err.stack) 124 | }) 125 | ``` 126 | 127 | ## API Reference 128 | 129 | Client for connecting to the five-bells-wallet 130 | 131 | 132 | 133 | ### WalletClient~WalletClient 134 | **Kind**: inner class of [WalletClient](#module_WalletClient) 135 | 136 | * [~WalletClient](#module_WalletClient..WalletClient) 137 | * [new WalletClient(opts)](#new_module_WalletClient..WalletClient_new) 138 | * [.connect()](#module_WalletClient..WalletClient+connect) ⇒ Promise.<null> 139 | * [.isConnected()](#module_WalletClient..WalletClient+isConnected) ⇒ Boolean 140 | * [.getAccount()](#module_WalletClient..WalletClient+getAccount) ⇒ Promise.<String> 141 | * [.disconnect()](#module_WalletClient..WalletClient+disconnect) ⇒ null 142 | * [.payment(params)](#module_WalletClient..WalletClient+payment) ⇒ [Payment](#module_Payment..Payment) 143 | * [.send(params)](#module_WalletClient..WalletClient+send) ⇒ Promise.<Object> 144 | * [.convertAmount()](#module_WalletClient..WalletClient+convertAmount) ⇒ Promise.<BigNumber> 145 | 146 | 147 | 148 | #### new WalletClient(opts) 149 | 150 | | Param | Type | Default | Description | 151 | | --- | --- | --- | --- | 152 | | opts | Object | | WalletClient options | 153 | | opts.address | String | | Account at five-bells-wallet in the form user@wallet-url.example | 154 | | opts.password | String | | Account password for five-bells-wallet | 155 | | [opts.autoConnect] | Boolean | true | Subscribe to WebSocket notifications automatically when new event listeners are added | 156 | 157 | 158 | 159 | #### walletClient.connect() ⇒ Promise.<null> 160 | Login to wallet and subscribe to WebSocket notifications 161 | 162 | **Kind**: instance method of [WalletClient](#module_WalletClient..WalletClient) 163 | **Returns**: Promise.<null> - Resolves once client is subscribed 164 | 165 | 166 | #### walletClient.isConnected() ⇒ Boolean 167 | Check if the client is currently subscribed to wallet notifications 168 | 169 | **Kind**: instance method of [WalletClient](#module_WalletClient..WalletClient) 170 | 171 | 172 | #### walletClient.getAccount() ⇒ Promise.<String> 173 | Get the ledger account URI corresponding to the user's address 174 | 175 | **Kind**: instance method of [WalletClient](#module_WalletClient..WalletClient) 176 | 177 | 178 | #### walletClient.disconnect() ⇒ null 179 | Unsubscribe from wallet notifications 180 | 181 | **Kind**: instance method of [WalletClient](#module_WalletClient..WalletClient) 182 | 183 | 184 | #### walletClient.payment(params) ⇒ [Payment](#module_Payment..Payment) 185 | Create a new Payment object 186 | 187 | **Kind**: instance method of [WalletClient](#module_WalletClient..WalletClient) 188 | 189 | | Param | Type | Description | 190 | | --- | --- | --- | 191 | | params | [PaymentParams](#module_Payment..PaymentParams) | Payment parameters | 192 | 193 | 194 | 195 | #### walletClient.send(params) ⇒ Promise.<Object> 196 | Create a new Payment object, get a quote, and send the payment. Resolves when the payment is complete. 197 | 198 | **Kind**: instance method of [WalletClient](#module_WalletClient..WalletClient) 199 | **Returns**: Promise.<Object> - Payment result 200 | 201 | | Param | Type | Description | 202 | | --- | --- | --- | 203 | | params | [PaymentParams](#module_Payment..PaymentParams) | Payment parameters | 204 | | [params.onQuote] | function | Function to call when a quote is received | 205 | | [params.onSent] | function | Function to call when payment is sent (before it is complete) | 206 | 207 | 208 | 209 | #### walletClient.convertAmount() ⇒ Promise.<BigNumber> 210 | Convert the given destination amount into the local asset 211 | 212 | **Kind**: instance method of [WalletClient](#module_WalletClient..WalletClient) 213 | **Returns**: Promise.<BigNumber> - Source amount as a [BigNumber](https://mikemcl.github.io/bignumber.js/) 214 | 215 | | Param | Type | Description | 216 | | --- | --- | --- | 217 | | params.destinationAmount | String | Number | The destination amount to convert | 218 | | params.destinationAccount | String | Destination account to convert amount for | 219 | 220 | 221 | Class for quoting and sending payments 222 | 223 | 224 | 225 | ### Payment~Payment 226 | **Kind**: inner class of [Payment](#module_Payment) 227 | 228 | * [~Payment](#module_Payment..Payment) 229 | * [new Payment(walletClient, params)](#new_module_Payment..Payment_new) 230 | * [.quote()](#module_Payment..Payment+quote) ⇒ [Promise.<PaymentParams>](#module_Payment..PaymentParams) 231 | * [.send()](#module_Payment..Payment+send) ⇒ Promise.<Object> 232 | 233 | 234 | 235 | #### new Payment(walletClient, params) 236 | 237 | | Param | Type | Description | 238 | | --- | --- | --- | 239 | | walletClient | WalletClient | WalletClient instance used for quoting and sending | 240 | | params | [PaymentParams](#module_Payment..PaymentParams) | Payment parameters | 241 | 242 | 243 | 244 | #### payment.quote() ⇒ [Promise.<PaymentParams>](#module_Payment..PaymentParams) 245 | Get a quote to fill in either the sourceAmount or destinationAmount, whichever was not given. 246 | 247 | **Kind**: instance method of [Payment](#module_Payment..Payment) 248 | **Returns**: [Promise.<PaymentParams>](#module_Payment..PaymentParams) - Original payment params with sourceAmount or destinationAmount filled in 249 | **Emits**: [quote](#Payment+event_quote) 250 | 251 | 252 | #### payment.send() ⇒ Promise.<Object> 253 | Execute the payment 254 | 255 | **Kind**: instance method of [Payment](#module_Payment..Payment) 256 | **Returns**: Promise.<Object> - Resolves when the payment is complete 257 | **Emits**: [sent](#Payment+event_sent) 258 | 259 | 260 | ### Payment~PaymentParams : Object 261 | **Kind**: inner typedef of [Payment](#module_Payment) 262 | 263 | | Param | Type | Default | Description | 264 | | --- | --- | --- | --- | 265 | | destinationAccount | String | | Receiver account URI | 266 | | [sourceAmount] | String | Number | BigNumber | (quoted from destinationAmount) | Either sourceAmount or destinationAmount must be supplied | 267 | | [destinationAmount] | String | Number | BigNumber | (quoted from sourceAmount) | Either sourceAmount or destinationAmount must be supplied | 268 | | [message] | String | "" | Message to send to recipient | 269 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 4.4.0 4 | deployment: 5 | production: 6 | branch: master 7 | commands: 8 | # Push NPM package if not yet published 9 | - mv npmrc-env .npmrc 10 | - if [ -z "$(npm info $(npm ls --depth=-1 2>/dev/null | head -1 | cut -f 1 -d " ") 2>/dev/null)" ] ; then npm publish ; fi 11 | -------------------------------------------------------------------------------- /docs/README.template.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |
4 | Five Bells Wallet Client 5 |

6 | 7 |

8 | A high-level JS library for sending and receiving Interledger payments. 9 |

10 | 11 |
12 | 13 | [![npm][npm-image]][npm-url] [![standard][standard-image]][standard-url] [![circle][circle-image]][circle-url] 14 | 15 | [npm-image]: https://img.shields.io/npm/v/five-bells-wallet-client.svg?style=flat 16 | [npm-url]: https://npmjs.org/package/five-bells-wallet-client 17 | [standard-image]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat 18 | [standard-url]: http://standardjs.com/ 19 | [circle-image]: https://img.shields.io/circleci/project/interledgerjs/five-bells-wallet-client/master.svg?style=flat 20 | [circle-url]: https://circleci.com/gh/interledgerjs/five-bells-wallet-client 21 | 22 | ## Installation 23 | 24 | `npm install five-bells-wallet-client --save` 25 | 26 | ## Usage 27 | 28 | This is a client for the [`five-bells-wallet`](https://github.com/interledgerjs/five-bells-wallet). 29 | 30 | To use it with a hosted demo wallet, create an account on [red.ilpdemo.org](https://red.ilpdemo.org) or [blue.ilpdemo.org](https://blue.ilpdemo.org) (it doesn't matter which because they're connected via the [Interledger Protocol](https://interledger.org)!). 31 | 32 | 33 | ### Sending 34 | 35 | ```js 36 | const WalletClient = require('five-bells-wallet-client') 37 | 38 | const sender = new WalletClient({ 39 | address: 'alice@red.ilpdemo.org', 40 | password: 'super-secret-password' 41 | }) 42 | 43 | sender.on('connect', () => { 44 | console.log('Sender connected') 45 | }) 46 | 47 | sender.send({ 48 | destination: 'bob@blue.ilpdemo.org', 49 | destinationAmount: '0.01', 50 | message: 'Still love you!', 51 | onQuote: (payment) => { 52 | console.log('Received a quote; this will cost us: ' + payment.sourceAmount) 53 | } 54 | }).then((payment) => { 55 | console.log('Sent payment:', payment) 56 | console.log('') 57 | }).catch((err) => { 58 | console.error(err.stack) 59 | }) 60 | ``` 61 | 62 | ### Receiving 63 | 64 | ```js 65 | const WalletClient = require('five-bells-wallet-client') 66 | 67 | const receiver = new WalletClient({ 68 | address: 'bob@blue.ilpdemo.org', 69 | password: 'ultra-secret-password' 70 | }) 71 | 72 | receiver.on('connect', () => { 73 | console.log('Receiver connected') 74 | }) 75 | 76 | receiver.on('incoming', (payment) => { 77 | console.log('Received ' + payment.destinationAmount + ' bucks!') 78 | console.log(payment.sourceAccount + ' says: ' + payment.message) 79 | }) 80 | ``` 81 | 82 | ## API Reference 83 | 84 | {{#module name="WalletClient"~}} 85 | {{>body~}} 86 | {{>members~}} 87 | {{/module}} 88 | 89 | {{#module name="Payment"~}} 90 | {{>body~}} 91 | {{>members~}} 92 | {{/module}} 93 | -------------------------------------------------------------------------------- /ilp_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/interledger-deprecated/five-bells-wallet-client/536e4dfa7aa90b3ece1abb68593effef5e1daf1b/ilp_logo.png -------------------------------------------------------------------------------- /npmrc-env: -------------------------------------------------------------------------------- 1 | //registry.npmjs.org/:_authToken=${NPM_TOKEN} 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "five-bells-wallet-client", 3 | "version": "1.0.1", 4 | "description": "Client for the five-bells-wallet", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\"", 8 | "lint": "standard src/*", 9 | "docs:api": "jsdoc2md --global-index-format none \"src/*.js\" --template docs/README.template.md > docs/README.intermediate.md", 10 | "docs:toc": "md-toc-filter docs/README.intermediate.md > README.md", 11 | "docs": "npm run docs:api && npm run docs:toc" 12 | }, 13 | "keywords": [ 14 | "interledger", 15 | "ilp", 16 | "micropayment", 17 | "payment" 18 | ], 19 | "author": "Evan Schwartz ", 20 | "license": "Apache-2.0", 21 | "dependencies": { 22 | "bignumber.js": "^2.3.0", 23 | "inherits": "^2.0.1", 24 | "moment": "^2.12.0", 25 | "node-uuid": "^1.4.7", 26 | "socket.io-client": "^1.4.6", 27 | "superagent": "^1.8.1", 28 | "webfinger.js": "^2.3.2" 29 | }, 30 | "devDependencies": { 31 | "jsdoc-to-markdown": "^1.3.6", 32 | "md-toc-filter": "^0.9.0", 33 | "standard": "^6.0.8" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const socket = require('socket.io-client') 4 | const EventEmitter = require('events').EventEmitter 5 | const request = require('superagent') 6 | const WebFinger = require('webfinger.js') 7 | const Debug = require('debug') 8 | const debug = Debug('WalletClient') 9 | const moment = require('moment') 10 | const BigNumber = require('bignumber.js') 11 | const url = require('url') 12 | const inherits = require('inherits') 13 | const uuid = require('node-uuid') 14 | const Payment = require('./payment').Payment 15 | 16 | const RATE_CACHE_REFRESH = 60000 17 | 18 | const WEBFINGER_RELS = { 19 | 'https://interledger.org/rel/ledgerAccount': 'ledgerAccount', 20 | 'https://interledger.org/rel/socketIOUri': 'socketIOUri', 21 | 'https://interledger.org/rel/sender/payment': 'paymentUri', 22 | 'https://interledger.org/rel/sender/pathfind': 'pathfindUri' 23 | } 24 | 25 | /** 26 | * Client for connecting to the five-bells-wallet 27 | * @module WalletClient 28 | */ 29 | 30 | /** 31 | * @class 32 | * @param {Object} opts WalletClient options 33 | * @param {String} opts.address Account at five-bells-wallet in the form user@wallet-url.example 34 | * @param {String} opts.password Account password for five-bells-wallet 35 | * @param {Boolean} [opts.autoConnect=true] Subscribe to WebSocket notifications automatically when new event listeners are added 36 | */ 37 | function WalletClient (opts) { 38 | EventEmitter.call(this) 39 | 40 | this.address = opts.address 41 | this.password = opts.password 42 | this.autoConnect = opts.autoConnect !== false 43 | 44 | if (!this.address) { 45 | throw new Error('Must instantiate WalletClient with five-bells-wallet address') 46 | } 47 | if (!this.password) { 48 | throw new Error('Must instantiate WalletClient with five-bells-wallet password') 49 | } 50 | 51 | this.accountUri = null 52 | this.socketIOUri = null 53 | this.paymentUri = null 54 | this.pathfindUri = null 55 | // TODO get the username from the WebFinger results 56 | this.username = opts.address.split('@')[0] 57 | this.socket = null 58 | // : { : { sourceAmount: 10, expiresAt: '' } } 59 | this.ratesCache = {} 60 | this.connected = false 61 | 62 | if (this.autoConnect) { 63 | this.once('newListener', function () { 64 | this.connect() 65 | }) 66 | } 67 | } 68 | inherits(WalletClient, EventEmitter) 69 | 70 | /** 71 | * Login to wallet and subscribe to WebSocket notifications 72 | * @return {Promise} Resolves once client is subscribed 73 | */ 74 | WalletClient.prototype.connect = function () { 75 | const _this = this 76 | 77 | if (_this.connected) { 78 | return Promise.resolve() 79 | } 80 | 81 | return new Promise(function (resolve, reject) { 82 | WalletClient._webfingerAddress(_this.address) 83 | .then(function (webFingerDetails) { 84 | _this.accountUri = webFingerDetails.ledgerAccount 85 | _this.socketIOUri = webFingerDetails.socketIOUri 86 | _this.paymentUri = webFingerDetails.paymentUri 87 | _this.pathfindUri = webFingerDetails.pathfindUri || _this.socketIOUri.replace('socket.io', 'payments/findPath') 88 | 89 | // It's important to parse the URL and pass the parts in separately 90 | // otherwise, socket.io thinks the path is a namespace http://socket.io/docs/rooms-and-namespaces/ 91 | const parsed = url.parse(_this.socketIOUri) 92 | const host = parsed.protocol + '//' + parsed.host 93 | debug('Attempting to connect to wallet host: ' + host + ' path: ' + parsed.path) 94 | _this.socket = socket(host, { path: parsed.path }) 95 | _this.socket.on('connect', function () { 96 | // If we're already connected, don't do anything here 97 | if (!_this.connected) { 98 | debug('Connected to wallet API socket.io') 99 | _this.socket.emit('unsubscribe', _this.username) 100 | _this.socket.emit('subscribe', _this.username) 101 | _this.connected = true 102 | _this.emit('connect') 103 | resolve() 104 | } 105 | }) 106 | _this.socket.on('disconnect', function () { 107 | _this.connected = false 108 | debug('Disconnected from wallet') 109 | reject() 110 | }) 111 | _this.socket.on('connect_error', function (err) { 112 | debug('Connection error', err, err.stack) 113 | reject(err) 114 | }) 115 | _this.socket.on('payment', _this._handleNotification.bind(_this)) 116 | }) 117 | .catch(function (err) { 118 | debug(err) 119 | }) 120 | }) 121 | } 122 | 123 | /** 124 | * Check if the client is currently subscribed to wallet notifications 125 | * @return {Boolean} 126 | */ 127 | WalletClient.prototype.isConnected = function () { 128 | return this.connected 129 | } 130 | 131 | /** 132 | * Get the ledger account URI corresponding to the user's address 133 | * @return {Promise} 134 | */ 135 | WalletClient.prototype.getAccount = function () { 136 | const _this = this 137 | if (this.accountUri) { 138 | return Promise.resolve(this.accountUri) 139 | } else { 140 | return new Promise(function (resolve, reject) { 141 | _this.once('connect', function () { 142 | resolve(this.accountUri) 143 | }) 144 | }) 145 | } 146 | } 147 | 148 | /** 149 | * Unsubscribe from wallet notifications 150 | * @return {null} 151 | */ 152 | WalletClient.prototype.disconnect = function () { 153 | this.socket.emit('unsubscribe', this.username) 154 | } 155 | 156 | /** 157 | * Create a new Payment object 158 | * @param {module:Payment~PaymentParams} params Payment parameters 159 | * @return {module:Payment~Payment} 160 | */ 161 | WalletClient.prototype.payment = function (params) { 162 | return new Payment(this, params) 163 | } 164 | 165 | /** 166 | * Create a new Payment object, get a quote, and send the payment. Resolves when the payment is complete. 167 | * 168 | * @param {module:Payment~PaymentParams} params Payment parameters 169 | * @param {Function} [params.onQuote] Function to call when a quote is received 170 | * @param {Function} [params.onSent] Function to call when payment is sent (before it is complete) 171 | * @return {Promise} Payment result 172 | */ 173 | WalletClient.prototype.send = function (params) { 174 | const payment = this.payment(params) 175 | 176 | if (typeof params.onQuote === 'function') { 177 | payment.on('quote', params.onQuote) 178 | } 179 | if (typeof params.onSent === 'function') { 180 | payment.on('sent', params.onSent) 181 | } 182 | 183 | return payment.quote() 184 | .then(function () { 185 | return payment.send() 186 | }) 187 | } 188 | 189 | WalletClient.prototype._findPath = function (params) { 190 | const _this = this 191 | 192 | if (!_this.connected) { 193 | _this.connect() 194 | return new Promise(function (resolve, reject) { 195 | _this.once('connect', resolve) 196 | }) 197 | .then(_this._findPath.bind(_this, params)) 198 | } 199 | 200 | const pathfindParams = { 201 | destination: params.destinationAccount || params.destination, 202 | destination_amount: params.destinationAmount, 203 | source_amount: params.sourceAmount 204 | } 205 | 206 | debug('_findPath', pathfindParams) 207 | 208 | return new Promise(function (resolve, reject) { 209 | request.post(_this.pathfindUri) 210 | .auth(_this.username, _this.password) 211 | .send(pathfindParams) 212 | .end(function (err, res) { 213 | if (err || !res.ok) { 214 | return reject(err || res.body) 215 | } 216 | 217 | debug('_findPath result:', res.body) 218 | 219 | let result = {} 220 | if (!params.sourceAmount) { 221 | result.sourceAmount = res.body.source_amount || res.body.debits[0].amount 222 | } 223 | if (!params.destinationAmount) { 224 | result.destinationAmount = res.body.destination_amount || res.body.credits[0].memo.ilp_header.amount 225 | } 226 | // We use the transfer as the "path" 227 | result.path = res.body 228 | resolve(result) 229 | }) 230 | }) 231 | } 232 | 233 | /** 234 | * Convert the given destination amount into the local asset 235 | * @param {String|Number} params.destinationAmount The destination amount to convert 236 | * @param {String} params.destinationAccount Destination account to convert amount for 237 | * @return {Promise} Source amount as a [BigNumber](https://mikemcl.github.io/bignumber.js/) 238 | */ 239 | WalletClient.prototype.convertAmount = function (params) { 240 | const _this = this 241 | // TODO clean up this caching system 242 | const cacheRateThreshold = (new BigNumber(params.destinationAmount)).div(100) 243 | if (this.ratesCache[params.destinationAccount]) { 244 | const destinationAmounts = Object.keys(this.ratesCache[params.destinationAccount]) 245 | for (let destinationAmount of destinationAmounts) { 246 | const cache = this.ratesCache[params.destinationAccount][destinationAmount] 247 | if (cache.expiresAt.isBefore(moment())) { 248 | delete this.ratesCache[params.destinationAccount][destinationAmount] 249 | continue 250 | } 251 | if ((new BigNumber(destinationAmount)).minus(params.destinationAmount).abs().lessThan(cacheRateThreshold)) { 252 | return Promise.resolve(cache.sourceAmount) 253 | } 254 | } 255 | } 256 | 257 | return _this._findPath(params) 258 | .then(function (result) { 259 | if (result) { 260 | // TODO cache rate by ledger instead of by account 261 | if (!_this.ratesCache[params.destinationAccount]) { 262 | _this.ratesCache[params.destinationAccount] = {} 263 | } 264 | _this.ratesCache[params.destinationAccount][params.destinationAmount] = { 265 | sourceAmount: new BigNumber(result.sourceAmount), 266 | expiresAt: moment().add(RATE_CACHE_REFRESH, 'milliseconds') 267 | } 268 | 269 | return result.sourceAmount 270 | } else { 271 | throw new Error('No path found %o', path) 272 | } 273 | }) 274 | .catch(function (err) { 275 | debug('Error finding path %o %o', params, err) 276 | throw err 277 | }) 278 | } 279 | 280 | WalletClient.prototype._sendPayment = function (params) { 281 | const _this = this 282 | 283 | if (!_this.connected) { 284 | _this.connect() 285 | return new Promise(function (resolve, reject) { 286 | _this.once('connect', resolve) 287 | }) 288 | .then(_this._sendPayment.bind(_this, params)) 289 | } 290 | 291 | let paramsToSend = { 292 | destination_account: params.destinationAccount || params.destination, 293 | destination_amount: params.destinationAmount, 294 | source_amount: params.sourceAmount, 295 | source_memo: params.sourceMemo, 296 | destination_memo: params.destinationMemo, 297 | message: params.message, 298 | path: params.path 299 | } 300 | if (paramsToSend.destination_amount) { 301 | paramsToSend.destination_amount = paramsToSend.destination_amount.toString() 302 | } 303 | if (paramsToSend.source_amount) { 304 | paramsToSend.source_amount = paramsToSend.source_amount.toString() 305 | } 306 | debug('_sendPayment', paramsToSend) 307 | return new Promise(function (resolve, reject) { 308 | request.put(_this.paymentUri + '/' + uuid.v4()) 309 | .auth(_this.username, _this.password) 310 | .send(paramsToSend) 311 | .end(function (err, res) { 312 | if (err || !res.ok) { 313 | return reject(err || res.error || res.body) 314 | } 315 | resolve(res.body) 316 | }) 317 | }) 318 | } 319 | 320 | WalletClient.prototype._handleNotification = function (notification) { 321 | const _this = this 322 | debug('Got notification %o', notification) 323 | if (!notification) { 324 | return 325 | } 326 | 327 | const notificationToEmit = { 328 | sourceAccount: notification.source_account, 329 | destinationAccount: notification.destination_account, 330 | sourceAmount: notification.source_amount, 331 | destinationAmount: notification.destination_amount, 332 | message: notification.message 333 | } 334 | 335 | if (notification.source_account === this.accountUri) { 336 | this.emit('outgoing', notificationToEmit) 337 | 338 | if (this.listenerCount('outgoing_fulfillment') > 0) { 339 | Promise.all([ 340 | this._getTransfer(notification.transfers), 341 | this._getTransferFulfillment(notification.transfers) 342 | ]) 343 | .then(function (results) { 344 | _this.emit('outgoing_fulfillment', results[0], results[1]) 345 | }) 346 | .catch(function (err) { 347 | debug('Error getting outgoing_fulfillment: ' + err.message || err) 348 | }) 349 | } 350 | } else if (notification.destination_account === this.accountUri) { 351 | this.emit('incoming', notificationToEmit) 352 | 353 | if (this.listenerCount('incoming_transfer') > 0) { 354 | this._getTransfer(notification.transfers) 355 | .then(function (transfer) { 356 | _this.emit('incoming_transfer', transfer) 357 | }) 358 | .catch(function (err) { 359 | debug('Error getting incoming_transfer: ' + err.message || err) 360 | }) 361 | } 362 | } 363 | } 364 | 365 | WalletClient.prototype._getTransfer = function (transferId) { 366 | const _this = this 367 | return new Promise(function (resolve, reject) { 368 | request.get(transferId) 369 | .auth(_this.username, _this.password) 370 | .end(function (err, res) { 371 | if (err) { 372 | return reject(err) 373 | } 374 | 375 | resolve(res.body) 376 | }) 377 | }) 378 | } 379 | 380 | WalletClient.prototype._getTransferFulfillment = function (transferId) { 381 | const fulfillmentUri = transferId + '/fulfillment' 382 | const _this = this 383 | return new Promise(function (resolve, reject) { 384 | request.get(fulfillmentUri) 385 | .auth(_this.username, _this.password) 386 | .end(function (err, res) { 387 | if (err) { 388 | return reject(err) 389 | } 390 | 391 | resolve(res.body) 392 | }) 393 | }) 394 | } 395 | 396 | // Returns a promise that resolves to the account details 397 | WalletClient._webfingerAddress = function (address) { 398 | const WebFingerConstructor = (typeof window === 'object' && window.WebFinger ? window.WebFinger : WebFinger) 399 | const webfinger = new WebFingerConstructor() 400 | return new Promise(function (resolve, reject) { 401 | webfinger.lookup(address, function (err, res) { 402 | if (err) { 403 | return reject(new Error('Error looking up wallet address: ' + err.message)) 404 | } 405 | 406 | let webFingerDetails = {} 407 | try { 408 | for (let link of res.object.links) { 409 | const key = WEBFINGER_RELS[link.rel] 410 | if (key) { 411 | webFingerDetails[key] = link.href 412 | } 413 | } 414 | } catch (err) { 415 | return reject(new Error('Error parsing webfinger response' + err.message)) 416 | } 417 | debug('Got webfinger response %o', webFingerDetails) 418 | resolve(webFingerDetails) 419 | }) 420 | }) 421 | } 422 | 423 | module.exports = WalletClient 424 | -------------------------------------------------------------------------------- /src/payment.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const EventEmitter = require('events').EventEmitter 4 | const inherits = require('inherits') 5 | 6 | /** 7 | * Class for quoting and sending payments 8 | * @module Payment 9 | */ 10 | 11 | /** 12 | * @typedef {Object} PaymentParams 13 | * @param {String} destinationAccount Receiver account URI 14 | * @param {String|Number|BigNumber} [sourceAmount=(quoted from destinationAmount)] Either sourceAmount or destinationAmount must be supplied 15 | * @param {String|Number|BigNumber} [destinationAmount=(quoted from sourceAmount)] Either sourceAmount or destinationAmount must be supplied 16 | * @param {String} [message=""] Message to send to recipient 17 | */ 18 | 19 | /** 20 | * Quote event 21 | * @event Payment#quote 22 | * @type {Payment~PaymentParams} 23 | */ 24 | 25 | /** 26 | * Payment sent event 27 | * @event Payment#sent 28 | * @type {Object} Payment result 29 | */ 30 | 31 | /** 32 | * @class 33 | * @param {WalletClient} walletClient - WalletClient instance used for quoting and sending 34 | * @param {module:Payment~PaymentParams} params - Payment parameters 35 | */ 36 | function Payment (walletClient, params) { 37 | EventEmitter.call(this) 38 | 39 | this.walletClient = walletClient 40 | this.params = params 41 | } 42 | inherits(Payment, EventEmitter) 43 | 44 | /** 45 | * Get a quote to fill in either the sourceAmount or destinationAmount, whichever was not given. 46 | * @fires Payment#quote 47 | * @return {Promise} Original payment params with sourceAmount or destinationAmount filled in 48 | */ 49 | Payment.prototype.quote = function () { 50 | const _this = this 51 | return this.walletClient._findPath(this.params) 52 | .then(function (result) { 53 | if (!_this.params.sourceAmount) { 54 | // TODO the result will be made more consistent when the wallet is updated to the latest connector 55 | _this.params.sourceAmount = result.sourceAmount 56 | } 57 | if (!_this.params.destinationAmount) { 58 | _this.params.destinationAmount = result.destinationAmount 59 | } 60 | 61 | _this.params.path = result.path 62 | 63 | _this.emit('quote', _this.params) 64 | return _this.params 65 | }) 66 | } 67 | 68 | /** 69 | * Execute the payment 70 | * @fires Payment#sent 71 | * @return {Promise} Resolves when the payment is complete 72 | */ 73 | Payment.prototype.send = function () { 74 | const _this = this 75 | return this.walletClient._sendPayment(this.params) 76 | .then(function (result) { 77 | _this.emit('sent', result) 78 | _this.result = result 79 | return result 80 | }) 81 | } 82 | 83 | exports.Payment = Payment 84 | --------------------------------------------------------------------------------