├── .gitignore ├── LICENSE ├── README.md ├── assets └── blue-white.png ├── docs ├── AuthError.html ├── MoneyButtonClient.html ├── RestError.html ├── auth-error.js.html ├── errors.js.html ├── fonts │ ├── OpenSans-Bold-webfont.eot │ ├── OpenSans-Bold-webfont.svg │ ├── OpenSans-Bold-webfont.woff │ ├── OpenSans-BoldItalic-webfont.eot │ ├── OpenSans-BoldItalic-webfont.svg │ ├── OpenSans-BoldItalic-webfont.woff │ ├── OpenSans-Italic-webfont.eot │ ├── OpenSans-Italic-webfont.svg │ ├── OpenSans-Italic-webfont.woff │ ├── OpenSans-Light-webfont.eot │ ├── OpenSans-Light-webfont.svg │ ├── OpenSans-Light-webfont.woff │ ├── OpenSans-LightItalic-webfont.eot │ ├── OpenSans-LightItalic-webfont.svg │ ├── OpenSans-LightItalic-webfont.woff │ ├── OpenSans-Regular-webfont.eot │ ├── OpenSans-Regular-webfont.svg │ └── OpenSans-Regular-webfont.woff ├── get-money-button-client.js.html ├── index.html ├── index.js.html ├── module.html#.exports ├── rest-error.js.html ├── scripts │ ├── linenumber.js │ └── prettify │ │ ├── Apache-License-2.0.txt │ │ ├── lang-css.js │ │ └── prettify.js └── styles │ ├── jsdoc-default.css │ ├── prettify-jsdoc.css │ └── prettify-tomorrow.css ├── package.json ├── rollup.config.js ├── src ├── auth-error.js ├── banner.js ├── config.js ├── get-money-button-client.js ├── index.browser.js ├── index.js └── rest-error.js └── test ├── babel.js ├── config.js ├── index.js ├── mocha.opts ├── resources ├── not-found.json └── user.json └── util.js /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Open BSV License 2 | Copyright (c) 2019 Yours Inc. 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | 1 - The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 2 - The Software, and any software that is derived from the Software or parts thereof, 14 | can only be used on the Bitcoin SV blockchains. The Bitcoin SV blockchains are defined, 15 | for purposes of this license, as the Bitcoin blockchain containing block height #556767 16 | with the hash "000000000000000001d956714215d96ffc00e0afda4cd0a96c96f8d802b1662b" and 17 | the test blockchains that are supported by the un-modified Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @moneybutton/api-client 2 | -------------------------------------------------------------------------------- /assets/blue-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moneybutton/api-client/9d152aa94bedbd8b470ca3ec15336da3c2863fc6/assets/blue-white.png -------------------------------------------------------------------------------- /docs/AuthError.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |/**
30 | * Authentication API error.
31 | */
32 | export default class AuthError {
33 | /**
34 | * @param {string} title - Error title.
35 | * @param {string} detail - Error detail.
36 | */
37 | constructor (title, detail) {
38 | this.title = title
39 | this.detail = detail
40 | this.message = detail !== undefined ? detail : title
41 | }
42 | }
43 |
44 | /**
30 | *
31 | */
32 | export class AuthError {
33 | constructor (title, detail) {
34 | this.title = title
35 | this.detail = detail
36 | this.message = detail !== undefined ? detail : title
37 | }
38 | }
39 |
40 | /**
41 | *
42 | */
43 | export class RestError {
44 | constructor (status, title, detail) {
45 | this.status = status
46 | this.title = title
47 | this.detail = detail
48 | this.message = detail !== undefined ? detail : title
49 | }
50 | }
51 |
52 | import {
30 | toResourceObject,
31 | toNewResourceObject,
32 | fromResourceObject,
33 | fromResourceObjectsOfType,
34 | toJsonApiData,
35 | toJsonApiDataIncluding,
36 | fromJsonApiData,
37 | fromJsonApiDataIncluding
38 | } from '@moneybutton/json-api'
39 | import fetch from 'isomorphic-fetch'
40 | import moment from 'moment'
41 | import queryString from 'query-string'
42 | import uuid from 'uuid'
43 |
44 | import AuthError from './auth-error'
45 | import config from './config'
46 | import RestError from './rest-error'
47 |
48 | const API_REST_URI = config.get('MONEY_BUTTON_API_REST_URI')
49 | const API_AUTH_URI = config.get('MONEY_BUTTON_API_AUTH_URI')
50 |
51 | const LOGIN_PASSWORD_HMAC_KEY = 'yours login password'
52 |
53 | const STORAGE_NAMESPACE = 'mb_js_client'
54 | const OAUTH_REDIRECT_URI_KEY = [STORAGE_NAMESPACE, 'oauth_redirect_uri'].join(':')
55 | const OAUTH_STATE_KEY = [STORAGE_NAMESPACE, 'oauth_state'].join(':')
56 | const OAUTH_ACCESS_TOKEN_KEY = [STORAGE_NAMESPACE, 'oauth_access_token'].join(':')
57 | const OAUTH_EXPIRATION_TIME_KEY = [STORAGE_NAMESPACE, 'oauth_expiration_time'].join(':')
58 | const OAUTH_REFRESH_TOKEN_KEY = [STORAGE_NAMESPACE, 'oauth_refresh_token'].join(':')
59 |
60 | /**
61 | * @param {Storage} webStorage - Object conforming to the Storage Web API.
62 | * @param {Crypto} webCrypto - Object conforming to the Crypto Web API.
63 | * @param {Location} webLocation - Object conforming to the Location Web API.
64 | */
65 | export default function getMoneyButtonClient (webStorage, webCrypto, webLocation) {
66 | if (!webStorage) {
67 | throw new Error('Missing required web storage object.')
68 | }
69 | if (!webCrypto || !webCrypto.subtle) {
70 | throw new Error('Missing required web crypto object.')
71 | }
72 | if (!webLocation) {
73 | throw new Error('Missing required web location object.')
74 | }
75 | /**
76 | *
77 | */
78 | class MoneyButtonClient {
79 | /**
80 | * Creates an instance of Money Button for the given OAuth client.
81 | *
82 | * @param {string} clientId - OAuth client's identifier.
83 | * @param {string} clientSecret - OAuth client's secret.
84 | */
85 | constructor (clientId, clientSecret = null) {
86 | this.clientId = clientId
87 | this.clientSecret = clientSecret
88 | }
89 |
90 | /**
91 | * Logs in the user with the given email and password.
92 | *
93 | * @param {string} email
94 | * @param {string} password
95 | * @returns {undefined}
96 | */
97 | async logIn (email, password) {
98 | if (await this.isLoggedIn()) {
99 | this.logOut()
100 | }
101 | const loginPassword = await MoneyButtonClient._computeHmac256(
102 | LOGIN_PASSWORD_HMAC_KEY,
103 | password
104 | )
105 | await this._doResourceOwnerPasswordCredentialsGrantAccessTokenRequest(
106 | email,
107 | loginPassword,
108 | 'general_access:write'
109 | )
110 | }
111 |
112 | /**
113 | * Determines whether a user is currently logged-in.
114 | *
115 | * @returns {boolean}
116 | */
117 | async isLoggedIn () {
118 | const accessToken = await this.getValidAccessToken()
119 | return accessToken !== null
120 | }
121 |
122 | /**
123 | * Retrieves a valid access token for the currently logged-in user.
124 | * Returns null if no user is currently logged-in.
125 | *
126 | * @returns {string|null}
127 | */
128 | async getValidAccessToken () {
129 | let accessToken = this.getAccessToken()
130 | if (
131 | accessToken !== null &&
132 | moment().isBefore(moment(this.getExpirationTime()))
133 | ) {
134 | return accessToken
135 | }
136 | const refreshToken = this.getRefreshToken()
137 | if (refreshToken === null) {
138 | return null
139 | }
140 | accessToken = null
141 | try {
142 | await this._doRefreshAccessTokenRequest(refreshToken)
143 | accessToken = this.getAccessToken()
144 | } catch (err) {
145 | if (!(err instanceof AuthError)) {
146 | throw err
147 | }
148 | }
149 | return accessToken
150 | }
151 |
152 | /**
153 | * Logs out the current logged-in user, if any.
154 | */
155 | logOut () {
156 | this.clearAccessToken()
157 | this.clearExpirationTime()
158 | this.clearRefreshToken()
159 | }
160 |
161 | /**
162 | * Finishes the email verification process with the access token generated
163 | * during signup.
164 | *
165 | * @param {string} accessToken - auth API access token
166 | * @returns {object}
167 | */
168 | async verifyEmail (accessToken) {
169 | const json = await this._doPostRequest(
170 | '/v1/auth/email_verification',
171 | {},
172 | {},
173 | accessToken
174 | )
175 | return fromResourceObject(fromJsonApiData(json), 'email_verifications')
176 | }
177 |
178 | /**
179 | * Retrieves the currently logged user's identity.
180 | *
181 | * @returns {object}
182 | */
183 | async getIdentity () {
184 | const json = await this._doGetRequest('/v1/auth/user_identity')
185 | return fromResourceObject(fromJsonApiData(json), 'user_identities')
186 | }
187 |
188 | /**
189 | * Returns an object with two keys:
190 | * - loggedIn: a boolean indicating whether there is a currently logged-in user.
191 | * - user: if loggedIn is true, this is an object with the user's attributes.
192 | *
193 | * @returns {object}
194 | */
195 | async whoAmI () {
196 | const loggedIn = await this.isLoggedIn()
197 | if (!loggedIn) {
198 | return { loggedIn }
199 | }
200 | const { id } = await this.getIdentity()
201 | const user = await this.getUser(id)
202 | return { loggedIn, user }
203 | }
204 |
205 | /**
206 | * Changes the currently logged-in user's password.
207 | *
208 | * @param {string} password
209 | * @param {string} encryptedMnemonic
210 | * @param {string} xpub
211 | * @param {string} language
212 | * @returns {object}
213 | */
214 | async changePassword (
215 | password,
216 | encryptedMnemonic,
217 | xpub,
218 | language
219 | ) {
220 | const loginPassword = await MoneyButtonClient._computeHmac256(
221 | LOGIN_PASSWORD_HMAC_KEY,
222 | password
223 | )
224 | const body = toJsonApiDataIncluding(
225 | toNewResourceObject('users', {
226 | password: loginPassword
227 | }),
228 | [
229 | toNewResourceObject('wallets', {
230 | encryptedMnemonic,
231 | xpub,
232 | language
233 | })
234 | ]
235 | )
236 | const json = await this._doPostRequest('/v1/auth/password_change', body)
237 | return fromResourceObject(fromJsonApiData(json), 'password_changes')
238 | }
239 |
240 | /**
241 | * Resets the currently logged-in user's password by using the access token
242 | * generated during the "I forgot my password" flow.
243 | *
244 | * @param {string} accessToken - auth API access token
245 | * @param {string} password
246 | * @param {string} encryptedMnemonic
247 | * @param {string} xpub
248 | * @param {boolean} forceCreate
249 | * @param {string} walletLanguage
250 | * @returns {object}
251 | */
252 | async resetPassword (
253 | accessToken,
254 | password,
255 | encryptedMnemonic,
256 | xpub,
257 | forceCreate,
258 | walletLanguage
259 | ) {
260 | const loginPassword = await MoneyButtonClient._computeHmac256(
261 | LOGIN_PASSWORD_HMAC_KEY,
262 | password
263 | )
264 | const body = toJsonApiDataIncluding(
265 | toNewResourceObject('users', {
266 | password: loginPassword
267 | }),
268 | [
269 | toNewResourceObject('wallets', {
270 | encryptedMnemonic,
271 | xpub,
272 | language: walletLanguage
273 | })
274 | ]
275 | )
276 | const query = forceCreate ? { forceCreate: 'true' } : {}
277 | const json = await this._doPostRequest(
278 | '/v1/auth/password_reset',
279 | body,
280 | query,
281 | accessToken
282 | )
283 | return fromResourceObject(fromJsonApiData(json), 'password_resets')
284 | }
285 |
286 | /**
287 | * Sends a password reset email to begin the "I forgot my password" flow.
288 | *
289 | * @param {string} email
290 | * @returns {object}
291 | */
292 | async sendPasswordReset (email) {
293 | if (await this.isLoggedIn()) {
294 | this.logOut()
295 | }
296 | await this._doClientCredentialsGrantAccessTokenRequest(
297 | 'auth.password_reset_email:write'
298 | )
299 | const attributes = { email }
300 | const body = toJsonApiData(toNewResourceObject('users', attributes))
301 | const json = await this._doPostRequest('/v1/auth/password_reset_email', body)
302 | this.logOut()
303 | return fromResourceObject(fromJsonApiData(json), 'password_reset_emails')
304 | }
305 |
306 | /**
307 | * Creates a new user account with the given email and password.
308 | *
309 | * @param {string} email
310 | * @param {string} password
311 | * @returns {object}
312 | */
313 | async signUp (email, password) {
314 | if (await this.isLoggedIn()) {
315 | this.logOut()
316 | }
317 | await this._doClientCredentialsGrantAccessTokenRequest(
318 | 'auth.signup:write'
319 | )
320 | const loginPassword = await MoneyButtonClient._computeHmac256(
321 | LOGIN_PASSWORD_HMAC_KEY,
322 | password
323 | )
324 | const attributes = {
325 | email,
326 | password: loginPassword
327 | }
328 | const body = toJsonApiData(toNewResourceObject('users', attributes))
329 | const json = await this._doPostRequest('/v1/auth/signup', body)
330 | await this.logIn(email, password)
331 | return fromResourceObject(fromJsonApiData(json), 'signups')
332 | }
333 |
334 | /**
335 | * [Browser only] Starts the authorization flow which allows third-party applications
336 | * to request access to user resources on their behalf. This function will
337 | * redirect the user's window to the Money Button's authorization flow page.
338 | *
339 | * @param {string} scope - scope to be requested to the user.
340 | * @param {string} redirectUri - URI where the authorization response will be handled.
341 | * @returns {undefined}
342 | */
343 | requestAuthorization (
344 | scope,
345 | redirectUri
346 | ) {
347 | if (typeof scope !== 'string' || scope.length === 0) {
348 | throw new Error(`Invalid scope requested: ${scope}.`)
349 | }
350 | if (typeof redirectUri !== 'string' || redirectUri.length === 0) {
351 | throw new Error(`Invalid return URI: ${redirectUri}.`)
352 | }
353 | this._doAuthorizationCodeGrantAuthorizationRequest(redirectUri, scope)
354 | }
355 |
356 | /**
357 | * [Browser only] Finishes the authorization flow started by {@link requestAuthorization}.
358 | * If successful, after calling this function, the client will be able to perform requests
359 | * on behalf of the user as long as they are within the scope requested when starting the
360 | * authorization flow.
361 | *
362 | * @returns {undefined}
363 | */
364 | async handleAuthorizationResponse () {
365 | const { error, code, state } = this._getUrlQuery()
366 | await this._handleAuthorizationCodeGrantAuthorizationResponse(error, code, state)
367 | }
368 |
369 | /**
370 | * See: https://tools.ietf.org/html/rfc6749#page-24.
371 | *
372 | * @private
373 | * @param {string} redirectUri
374 | * @param {string} scope
375 | */
376 | _doAuthorizationCodeGrantAuthorizationRequest (
377 | redirectUri,
378 | scope
379 | ) {
380 | if (this.clientSecret !== null) {
381 | throw new Error([
382 | 'Grant `authentication_code` can only be performed by ',
383 | 'a public client (that is, a client with no client secret).'
384 | ].join(''))
385 | }
386 | const state = uuid.v4()
387 | this._setRedirectUri(redirectUri)
388 | this._setState(state)
389 | const authorizationUri = [
390 | `${API_AUTH_URI}/oauth/v1/authorize`,
391 | queryString.stringify({
392 | response_type: 'code',
393 | client_id: this.clientId,
394 | redirect_uri: redirectUri,
395 | scope,
396 | state
397 | })
398 | ].join('?')
399 | this._redirectToUri(authorizationUri)
400 | }
401 |
402 | /**
403 | * See: https://tools.ietf.org/html/rfc6749#page-26.
404 | *
405 | * @private
406 | */
407 | async _handleAuthorizationCodeGrantAuthorizationResponse (
408 | error,
409 | code,
410 | state
411 | ) {
412 | const expectedState = this._getState()
413 | if (expectedState === null || state !== expectedState) {
414 | throw new Error('Invalid OAuth state.')
415 | }
416 | if (error !== undefined) {
417 | throw new AuthError('Authorization failed.', error)
418 | }
419 | if (code === undefined) {
420 | throw new Error('Missing OAuth authorization code.')
421 | }
422 | await this._doAuthorizationCodeGrantAccessTokenRequest(code)
423 | }
424 |
425 | /**
426 | * See: https://tools.ietf.org/html/rfc6749#page-29.
427 | *
428 | * @private
429 | */
430 | async _doAuthorizationCodeGrantAccessTokenRequest (
431 | code
432 | ) {
433 | const redirectUri = this._getRedirectUri()
434 | if (redirectUri === null) {
435 | throw new Error('Required OAuth redirect URI not found in storage.')
436 | }
437 | await this._doAccessTokenRequest(
438 | {
439 | grant_type: 'authorization_code',
440 | code,
441 | redirect_uri: redirectUri,
442 | client_id: this.clientId
443 | }
444 | )
445 | }
446 |
447 | /**
448 | * See: https://tools.ietf.org/html/rfc6749#page-37.
449 | *
450 | * @private
451 | */
452 | async _doResourceOwnerPasswordCredentialsGrantAccessTokenRequest (
453 | username,
454 | password,
455 | scope
456 | ) {
457 | await this._doAccessTokenRequest(
458 | {
459 | grant_type: 'password',
460 | username,
461 | password,
462 | scope
463 | },
464 | this._buildBasicAuthHeaders()
465 | )
466 | }
467 |
468 | /**
469 | * See: https://tools.ietf.org/html/rfc6749#page-41.
470 | *
471 | * @private
472 | */
473 | async _doClientCredentialsGrantAccessTokenRequest (scope) {
474 | await this._doAccessTokenRequest(
475 | {
476 | grant_type: 'client_credentials',
477 | scope
478 | },
479 | this._buildBasicAuthHeaders()
480 | )
481 | }
482 |
483 | /**
484 | * @private
485 | * @param {string} refreshToken
486 | */
487 | async _doRefreshAccessTokenRequest (refreshToken) {
488 | await this._doAccessTokenRequest(
489 | {
490 | grant_type: 'refresh_token',
491 | refresh_token: refreshToken
492 | },
493 | this._buildBasicAuthHeaders()
494 | )
495 | }
496 |
497 | /**
498 | * @private
499 | */
500 | _buildBasicAuthHeaders () {
501 | const credentials = `${this.clientId}:${this.clientSecret}`
502 | return {
503 | Authorization: `Basic ${Buffer.from(credentials).toString('base64')}`
504 | }
505 | }
506 |
507 | /**
508 | * @private
509 | * @param {object} body
510 | * @param {object} headers
511 | */
512 | async _doAccessTokenRequest (body = {}, headers = {}) {
513 | const res = await fetch(
514 | `${API_AUTH_URI}/oauth/v1/token`,
515 | {
516 | method: 'POST',
517 | body: queryString.stringify(body),
518 | headers: {
519 | ...headers,
520 | 'Content-Type': 'application/x-www-form-urlencoded'
521 | }
522 | }
523 | )
524 | await this._handleAccessTokenResponse(res)
525 | }
526 |
527 | /**
528 | * @private
529 | * @param {Response} res - Express.js response object.
530 | */
531 | async _handleAccessTokenResponse (res) {
532 | const {
533 | error,
534 | error_description: errorDescription,
535 | access_token: accessToken,
536 | token_type: tokenType,
537 | expires_in: expiresIn,
538 | refresh_token: refreshToken
539 | } = await res.json()
540 | if (error !== undefined) {
541 | throw new AuthError(error, errorDescription)
542 | }
543 | if (tokenType !== 'Bearer') {
544 | throw new Error('Unexpected token type.')
545 | }
546 | if (accessToken !== undefined) {
547 | this.setAccessToken(accessToken)
548 | } else {
549 | this.clearAccessToken()
550 | }
551 | if (expiresIn !== undefined) {
552 | const expirationTime = moment().add(expiresIn, 'seconds')
553 | this.setExpirationTime(expirationTime.format())
554 | } else {
555 | this.clearExpirationTime()
556 | }
557 | if (refreshToken !== undefined) {
558 | this.setRefreshToken(refreshToken)
559 | } else {
560 | this.clearRefreshToken()
561 | }
562 | }
563 |
564 | /**
565 | * Get basic information from the OAuth client with the given identifier.
566 | *
567 | * @param {string} clientIdentifier
568 | * @returns {object}
569 | */
570 | async getClientByIdentifier (clientIdentifier) {
571 | const json = await this._doGetRequest(`/v1/clients/client_identifier=${clientIdentifier}`)
572 | return fromResourceObject(fromJsonApiData(json), 'clients')
573 | }
574 |
575 | /**
576 | * Retrives the user with the given user id.
577 | *
578 | * @param {string} userId
579 | * @returns {object}
580 | */
581 | async getUser (userId) {
582 | let json = await this._doGetRequest(`/v1/users/${userId}`)
583 | return fromResourceObject(fromJsonApiData(json), 'users')
584 | }
585 |
586 | /**
587 | * Updates the user with the given user id.
588 | *
589 | * @param {string} userId
590 | * @param {object} attributes
591 | * @returns {object}
592 | */
593 | async updateUser (userId, attributes = {}) {
594 | const body = toJsonApiData(toResourceObject(userId, 'users', attributes))
595 | const json = await this._doPatchRequest(`/v1/users/${userId}`, body)
596 | return fromResourceObject(fromJsonApiData(json), 'users')
597 | }
598 |
599 | /**
600 | * Retrives the transaction history of the user with the given user id.
601 | *
602 | * @param {string} userId
603 | * @param {object} query
604 | * @returns {object}
605 | */
606 | async getUserTransactionHistory (userId, query = {}) {
607 | const json = await this._doGetRequest(
608 | `/v1/users/${userId}/transaction_history`,
609 | query
610 | )
611 | return fromResourceObjectsOfType(
612 | fromJsonApiData(json),
613 | 'transaction_history'
614 | )
615 | }
616 |
617 | /**
618 | * Retrives the OAuth clients of the user with the given user id.
619 | *
620 | * @param {string} userId
621 | * @param {object} query
622 | * @returns {object}
623 | */
624 | async getUserClients (userId, query = {}) {
625 | const json = await this._doGetRequest(
626 | `/v1/users/${userId}/clients`,
627 | query
628 | )
629 | return fromResourceObjectsOfType(fromJsonApiData(json), 'clients')
630 | }
631 |
632 | /**
633 | * Creates an OAuth client for the user with the given user id.
634 | *
635 | * @param {string} userId
636 | * @param {object} attributes
637 | * @returns {object}
638 | */
639 | async createUserClient (userId, attributes) {
640 | let body = toJsonApiData(toNewResourceObject('clients', attributes))
641 | const json = await this._doPostRequest(`/v1/users/${userId}/clients`, body)
642 | return fromResourceObject(fromJsonApiData(json), 'clients')
643 | }
644 |
645 | /**
646 | * Updates an OAuth client for the user with the given user id.
647 | *
648 | * @param {string} userId
649 | * @param {string} clientId
650 | * @param {object} attributes
651 | * @returns {object}
652 | */
653 | async updateUserClient (userId, clientId, attributes = {}) {
654 | const body = toJsonApiData(
655 | toResourceObject(clientId, 'clients', attributes)
656 | )
657 | await this._doPatchRequest(
658 | `/v1/users/${userId}/clients/${clientId}`,
659 | body
660 | )
661 | }
662 |
663 | /**
664 | * Retrives the handles of the user with the given user id.
665 | *
666 | * @param {string} userId
667 | * @param {object} query
668 | * @returns {object}
669 | */
670 | async getUserHandles (userId, query = {}) {
671 | const json = await this._doGetRequest(
672 | `/v1/users/${userId}/handles`,
673 | query
674 | )
675 | return fromResourceObjectsOfType(fromJsonApiData(json), 'handles')
676 | }
677 |
678 | /**
679 | * Creates a handle for the user with the given user id.
680 | *
681 | * @param {string} userId
682 | * @param {object} attributes
683 | * @returns {object}
684 | */
685 | async createUserHandle (userId, attributes) {
686 | let body = toJsonApiData(toNewResourceObject('handles', attributes))
687 | const json = await this._doPostRequest(`/v1/users/${userId}/handles`, body)
688 | return fromResourceObject(fromJsonApiData(json), 'handles')
689 | }
690 |
691 | /**
692 | * Retrives the wallet with the given wallet id for the user with
693 | * the given user id.
694 | *
695 | * @param {string} userId
696 | * @param {string} walletId
697 | * @returns {object}
698 | */
699 | async getUserWallet (userId, walletId) {
700 | let json = await this._doGetRequest(
701 | `/v1/users/${userId}/wallets/${walletId}`
702 | )
703 | return fromResourceObject(fromJsonApiData(json), 'wallets')
704 | }
705 |
706 | /**
707 | * Retrives the wallets of the user with the given user id.
708 | *
709 | * @param {string} userId
710 | * @returns {object}
711 | */
712 | async getUserWallets (userId) {
713 | let json = await this._doGetRequest(`/v1/users/${userId}/wallets/`)
714 | return fromResourceObjectsOfType(fromJsonApiData(json), 'wallets')
715 | }
716 |
717 | /**
718 | * Retrives the max withdrawal amount for the wallet with the given wallet id,
719 | * belonging to the user with the given user id.
720 | *
721 | * @param {string} userId
722 | * @param {string} walletId
723 | * @returns {object}
724 | */
725 | async getMaxWithdrawalForWallet (userId, walletId) {
726 | let json = await this._doGetRequest(`/v1/users/${userId}/wallets/${walletId}/max_withdrawal`)
727 | return fromResourceObject(fromJsonApiData(json), 'amounts')
728 | }
729 |
730 | /**
731 | * Creates a wallet for the user with the given user id.
732 | *
733 | * @param {string} userId
734 | * @param {object} attributes
735 | * @returns {object}
736 | */
737 | async createUserWallet (userId, attributes) {
738 | let body = toJsonApiData(toNewResourceObject('wallets', attributes))
739 | let json = await this._doPostRequest(`/v1/users/${userId}/wallets`, body)
740 | return fromResourceObject(fromJsonApiData(json), 'wallets')
741 | }
742 |
743 | /**
744 | * Retrieves the balance from the user with given user id.
745 | *
746 | * @param {string} userId
747 | * @returns {object}
748 | */
749 | async getBalance (userId) {
750 | const json = await this._doGetRequest(`/v1/users/${userId}/balance`)
751 | return fromResourceObject(fromJsonApiData(json), 'amounts')
752 | }
753 |
754 | /**
755 | * Retrives the max withdrawal amount the user with the given user id.
756 | *
757 | * @param {string} userId
758 | * @returns {object}
759 | */
760 | async getMaxWithdrawal (userId) {
761 | let json = await this._doGetRequest(`/v1/users/${userId}/max_withdrawal`)
762 | return fromResourceObject(fromJsonApiData(json), 'amounts')
763 | }
764 |
765 | /**
766 | * Retrives a recieve address for the user with the given user id.
767 | *
768 | * @param {string} userId
769 | * @param {string} walletId
770 | * @returns {object}
771 | */
772 | async getReceiveAddress (userId, walletId) {
773 | let json = await this._doPostRequest(
774 | `/v1/users/${userId}/wallets/${walletId}/receive_address`
775 | )
776 | let { address } = fromResourceObject(fromJsonApiData(json), 'addresses')
777 | return address
778 | }
779 |
780 | /**
781 | * Converts a (curreny,amount) pair into the given user's default currency.
782 | *
783 | * @param {string} userId
784 | * @param {object} attributes
785 | * @returns {object}
786 | */
787 | async getCurrencyAmount (userId, attributes) {
788 | const body = toJsonApiData(toNewResourceObject('currency', attributes))
789 | const json = await this._doPostRequest(
790 | `/v1/users/${userId}/currency`,
791 | body
792 | )
793 | const { amount, currency } = fromResourceObject(
794 | fromJsonApiData(json),
795 | 'currency'
796 | )
797 | return { amount, currency }
798 | }
799 |
800 | /**
801 | * Retrives the balance for the wallet with the given wallet id,
802 | * belonging to the user with the given user id.
803 | *
804 | * @param {string} userId
805 | * @param {string} walletId
806 | * @returns {object}
807 | */
808 | async getWalletBalance (userId, walletId) {
809 | const json = await this._doGetRequest(
810 | `/v1/users/${userId}/wallets/${walletId}/balance`
811 | )
812 | return fromResourceObject(fromJsonApiData(json), 'amounts')
813 | }
814 |
815 | /**
816 | * Updates the wallet with the given wallet id, belonging to the user
817 | * with the given user id.
818 | *
819 | * @param {string} userId
820 | * @param {string} walletId
821 | * @param {object} attributes
822 | * @returns {object}
823 | */
824 | async updateWallet (userId, walletId, attributes) {
825 | const body = toJsonApiData(
826 | toResourceObject(walletId, 'wallets', attributes)
827 | )
828 | await this._doPatchRequest(
829 | `/v1/users/${userId}/wallets/${walletId}`,
830 | body
831 | )
832 | }
833 |
834 | /**
835 | * Retrieves the payments from the user with the given user id.
836 | *
837 | * @param {string} userId
838 | * @param {object} paginate
839 | * @returns {object}
840 | */
841 | async getUserPayments (userId, paginate) {
842 | const json = await this._doGetRequest(
843 | `/v1/users/${userId}/payments?${this._paginateUri(paginate)}`
844 | )
845 | return {
846 | pages: json.meta['total-pages'],
847 | payments: json.data.map(payment =>
848 | fromResourceObject(payment, 'payments')
849 | )
850 | }
851 | }
852 |
853 | /**
854 | * @private
855 | * @returns {string}
856 | */
857 | _paginateUri ({ number, size, sort }) {
858 | // NOTE: query-string does not support the nesting format used in JsonApi
859 | // https://github.com/sindresorhus/query-string#nesting
860 | // http://jsonapi.org/examples/#pagination
861 | // http://jsonapi.org/format/#fetching-pagination
862 | const url = []
863 | if (number) url.push(`page[number]=${number}`)
864 | if (size) url.push(`page[size]=${size}`)
865 | if (sort) url.push(`sort=${sort}`)
866 | return url.join('&')
867 | }
868 |
869 | /**
870 | * Creates a payment for the user with the given user id to the specified payment
871 | * outputs.
872 | *
873 | * @param {string} userId
874 | * @param {object} attributes
875 | * @param {array} paymentOutputs
876 | * @returns {object}
877 | */
878 | async createUserPayment (userId, attributes, paymentOutputs) {
879 | let body = toJsonApiDataIncluding(
880 | toNewResourceObject('payments', attributes),
881 | paymentOutputs.map(paymentOutput => {
882 | return toNewResourceObject('payment_outputs', paymentOutput)
883 | })
884 | )
885 | let json = await this._doPostRequest(`/v1/users/${userId}/payments`, body)
886 | let { data, included } = fromJsonApiDataIncluding(json)
887 | let payment = fromResourceObject(data, 'payments')
888 | let [bsvTransaction] = fromResourceObjectsOfType(included, 'bsv_transactions')
889 | let addressIndexes = fromResourceObjectsOfType(
890 | included,
891 | 'address_indexes'
892 | )
893 | .sort((a, b) => a.index - b.index)
894 | .map(addressIndex => addressIndex.addressIndex)
895 | return {
896 | payment,
897 | paymentOutputs: fromResourceObjectsOfType(included, 'payment_outputs'),
898 | bsvTransaction,
899 | addressIndexes
900 | }
901 | }
902 |
903 | /**
904 | * Retrives the payment with the given payment id, belonging to the user with
905 | * the given user id.
906 | *
907 | * @param {string} userId
908 | * @param {string} paymentId
909 | * @returns {object}
910 | */
911 | async getUserPayment (userId, paymentId) {
912 | let json = await this._doGetRequest(`/v1/users/${userId}/payments/${paymentId}`)
913 | let { data, included } = fromJsonApiDataIncluding(json)
914 | const payment = fromResourceObject(data, 'payments')
915 | payment.outputs = fromResourceObjectsOfType(included, 'payment_outputs')
916 | return payment
917 | }
918 |
919 | /**
920 | * Updates the payment with the given payment id, belonging to the user with
921 | * the given user id.
922 | *
923 | * @param {string} userId
924 | * @param {string} paymentId
925 | * @param {object} attributes
926 | * @param {bsv.Transaction} bsvTransaction
927 | * @returns {object}
928 | */
929 | async updateUserPaymentWithTransaction (
930 | userId,
931 | paymentId,
932 | attributes,
933 | bsvTransaction
934 | ) {
935 | let body = toJsonApiDataIncluding(
936 | toResourceObject(paymentId, 'payments', attributes),
937 | [toResourceObject(
938 | bsvTransaction.hash,
939 | 'bsv_transactions',
940 | bsvTransaction
941 | )]
942 | )
943 | let json = await this._doPatchRequest(
944 | `/v1/users/${userId}/payments/${paymentId}`,
945 | body
946 | )
947 | return fromResourceObject(fromJsonApiData(json), 'payments')
948 | }
949 |
950 | /**
951 | * Creates a deposit for the user with the given id.
952 | *
953 | * @param {string} userId
954 | * @param {object} attributes
955 | * @returns {object}
956 | */
957 | async createUserDeposit (userId, attributes) {
958 | const body = toJsonApiData(toNewResourceObject('deposits', attributes))
959 | const json = await this._doPostRequest(
960 | `/v1/users/${userId}/deposits`,
961 | body
962 | )
963 | return fromResourceObject(fromJsonApiData(json), 'deposits')
964 | }
965 |
966 | /**
967 | * Retrives the deposit with the given deposit id, belonging to the user with
968 | * the given user id.
969 | *
970 | * @param {string} userId
971 | * @param {string} depositId
972 | * @returns {object}
973 | */
974 | async getUserDeposit (userId, depositId) {
975 | const json = await this._doGetRequest(
976 | `/v1/users/${userId}/deposits/${depositId}`
977 | )
978 | return fromResourceObject(fromJsonApiData(json), 'deposits')
979 | }
980 |
981 | /**
982 | *
983 | * @param {string} userId
984 | * @param {object} attributes
985 | * @returns {object}
986 | */
987 | async createUserWithdrawal (userId, attributes) {
988 | let body = toJsonApiData(toNewResourceObject('withdrawals', attributes))
989 | let json = await this._doPostRequest(
990 | `/v1/users/${userId}/withdrawals`,
991 | body
992 | )
993 | let { data, included } = fromJsonApiDataIncluding(json)
994 | let withdrawal = fromResourceObject(data, 'withdrawals')
995 | let [bsvTransaction] = fromResourceObjectsOfType(included, 'bsv_transactions')
996 | let addressIndexes = fromResourceObjectsOfType(
997 | included,
998 | 'address_indexes'
999 | )
1000 | .sort((a, b) => a.index - b.index)
1001 | .map(addressIndex => addressIndex.addressIndex)
1002 | return {
1003 | withdrawal,
1004 | bsvTransaction,
1005 | addressIndexes
1006 | }
1007 | }
1008 |
1009 | /**
1010 | * Retrives the withdrawal with the given withdrawal id, belonging to the user with
1011 | * the given user id.
1012 | *
1013 | * @param {string} userId
1014 | * @param {string} withdrawalId
1015 | * @returns {object}
1016 | */
1017 | async getUserWithdrawal (userId, withdrawalId) {
1018 | const json = await this._doGetRequest(
1019 | `/v1/users/${userId}/withdrawals/${withdrawalId}`
1020 | )
1021 | return fromResourceObject(fromJsonApiData(json), 'withdrawals')
1022 | }
1023 |
1024 | /**
1025 | * Updates the withdrawal with the given withdrawal id, belonging to the user with
1026 | * the given user id.
1027 | *
1028 | * @param {string} userId
1029 | * @param {string} withdrawalId
1030 | * @param {object} attributes
1031 | * @param {object} transaction
1032 | * @returns {object}
1033 | */
1034 | async updateUserWithdrawalWithTransaction (
1035 | userId,
1036 | withdrawalId,
1037 | attributes,
1038 | transaction
1039 | ) {
1040 | let body = toJsonApiDataIncluding(
1041 | toResourceObject(withdrawalId, 'withdrawals', attributes),
1042 | [toResourceObject(uuid.v1(), 'transactions', transaction)]
1043 | )
1044 | let json = await this._doPatchRequest(
1045 | `/v1/users/${userId}/withdrawals/${withdrawalId}`,
1046 | body
1047 | )
1048 | return fromResourceObject(fromJsonApiData(json), 'withdrawals')
1049 | }
1050 |
1051 | /**
1052 | * Broadcasts the given bsv transaction. The transaction must be fully signed.
1053 | *
1054 | * @param {bsv.Transaction} bsvTransaction
1055 | * @returns {object}
1056 | */
1057 | async broadcastTransaction (bsvTransaction) {
1058 | const body = toJsonApiData(toResourceObject(
1059 | bsvTransaction.hash,
1060 | 'bsv_transactions',
1061 | bsvTransaction
1062 | ))
1063 | const json = await this._doPostRequest(
1064 | '/v1/transactions/broadcast',
1065 | body
1066 | )
1067 | return fromResourceObject(fromJsonApiData(json), 'txids')
1068 | }
1069 |
1070 | /**
1071 | * Retrieves the list of supported cryptocurrencies.
1072 | *
1073 | * @param {object} query
1074 | * @returns {array}
1075 | */
1076 | async getSupportedCryptocurrencies (query = {}) {
1077 | const json = await this._doGetRequest('/v1/currencies/crypto', query)
1078 | return fromResourceObjectsOfType(fromJsonApiData(json), 'currencies')
1079 | }
1080 |
1081 | /**
1082 | * Retrieves the list of supported fiat currencies.
1083 | *
1084 | * @param {object} query
1085 | * @returns {array}
1086 | */
1087 | async getSupportedFiatCurrencies (query = {}) {
1088 | const json = await this._doGetRequest('/v1/currencies/fiat', query)
1089 | return fromResourceObjectsOfType(fromJsonApiData(json), 'currencies')
1090 | }
1091 |
1092 | /**
1093 | * @private
1094 | * @param {string} endpoint - REST API relative endpoint.
1095 | * @param {object} query - URL query parameters.
1096 | * @param {string} accessToken - auth API access token
1097 | * @returns {object}
1098 | */
1099 | async _doGetRequest (endpoint, query = {}, accessToken = null) {
1100 | let opts = {
1101 | method: 'GET'
1102 | }
1103 | return this._doRequest(endpoint, opts, query, accessToken)
1104 | }
1105 |
1106 | /**
1107 | * @private
1108 | * @param {string} endpoint - REST API relative endpoint.
1109 | * @param {object} body - fetch request's body.
1110 | * @param {object} query - URL query parameters.
1111 | * @param {string} accessToken - auth API access token
1112 | * @returns {object}
1113 | */
1114 | async _doPostRequest (endpoint, body = {}, query = {}, accessToken = null) {
1115 | let opts = {
1116 | method: 'POST',
1117 | body: JSON.stringify(body)
1118 | }
1119 | return this._doRequest(endpoint, opts, query, accessToken)
1120 | }
1121 |
1122 | /**
1123 | * @private
1124 | * @param {string} endpoint - REST API relative endpoint.
1125 | * @param {object} body - fetch request's body.
1126 | * @param {string} accessToken - auth API access token
1127 | * @returns {object}
1128 | */
1129 | async _doPatchRequest (endpoint, body = {}, accessToken = null) {
1130 | let opts = {
1131 | method: 'PATCH',
1132 | body: JSON.stringify(body)
1133 | }
1134 | return this._doRequest(endpoint, opts, {}, accessToken)
1135 | }
1136 |
1137 | /**
1138 | * @private
1139 | * @param {string} endpoint - REST API relative endpoint.
1140 | * @param {object} body - fetch request's body.
1141 | * @param {string} accessToken - auth API access token
1142 | * @returns {object}
1143 | */
1144 | async _doPutRequest (endpoint, body = {}, accessToken = null) {
1145 | let opts = {
1146 | method: 'PUT',
1147 | body: JSON.stringify(body)
1148 | }
1149 | return this._doRequest(endpoint, opts, {}, accessToken)
1150 | }
1151 |
1152 | /**
1153 | *
1154 | * @param {string} endpoint - REST API relative endpoint.
1155 | * @param {object} opts - fetch request options.
1156 | * @param {object} query - URL query parameters.
1157 | * @param {string} accessToken - auth API access token
1158 | * @returns {object}
1159 | */
1160 | async _doRequest (endpoint, opts = {}, query = {}, accessToken = null) {
1161 | const url = this._appendQuery(`${API_REST_URI}/api${endpoint}`, query)
1162 | let headers = {
1163 | 'Content-Type': 'application/vnd.api+json',
1164 | Accept: 'application/vnd.api+json'
1165 | }
1166 | accessToken = accessToken === null
1167 | ? await this.getValidAccessToken()
1168 | : accessToken
1169 | if (accessToken !== null) {
1170 | headers['Authorization'] = `Bearer ${accessToken}`
1171 | }
1172 | const res = await fetch(url, { ...opts, headers })
1173 | let json = await res.json()
1174 | let { errors } = json
1175 | if (errors instanceof Array) {
1176 | let error = errors[0]
1177 | if (error.status) {
1178 | let { status, title, detail } = error
1179 | throw new RestError(status, title, detail)
1180 | }
1181 | throw new Error(error.title)
1182 | }
1183 | return json
1184 | }
1185 |
1186 | /**
1187 | * @private
1188 | * @param {string} url - base URL where query will be appended.
1189 | * @param {object} query - URL query parameters.
1190 | * @returns {string}
1191 | */
1192 | _appendQuery (url, query = {}) {
1193 | if (Object.keys(query).length === 0) {
1194 | return url
1195 | }
1196 | const { page, ...queryWithoutPage } = query
1197 | if (page !== undefined) {
1198 | for (const key in page) {
1199 | queryWithoutPage[`page[${key}]`] = page[key]
1200 | }
1201 | }
1202 | return `${url}?${queryString.stringify(queryWithoutPage)}`
1203 | }
1204 |
1205 | /**
1206 | *
1207 | * Web location utilities.
1208 | *
1209 | */
1210 |
1211 | /**
1212 | *
1213 | */
1214 | _getUrlQuery () {
1215 | return queryString.parse(webLocation.search)
1216 | }
1217 |
1218 | /**
1219 | *
1220 | * @param {string} uri - URI where the browser will be redirected to.
1221 | */
1222 | _redirectToUri (uri) {
1223 | webLocation.href = uri
1224 | }
1225 |
1226 | /**
1227 | *
1228 | * Web storage utilities.
1229 | *
1230 | */
1231 |
1232 | /**
1233 | * @private
1234 | * @returns {string}
1235 | */
1236 | _getRedirectUri () {
1237 | return webStorage.getItem(OAUTH_REDIRECT_URI_KEY)
1238 | }
1239 |
1240 | /**
1241 | * @private
1242 | * @param {string} redirectUri - OAuth redirect URI from authorization grant flow.
1243 | * @returns {undefined}
1244 | */
1245 | _setRedirectUri (redirectUri) {
1246 | webStorage.setItem(OAUTH_REDIRECT_URI_KEY, redirectUri)
1247 | }
1248 |
1249 | /**
1250 | * @private
1251 | * @returns {undefined}
1252 | */
1253 | _clearRedirectUri () {
1254 | webStorage.removeItem(OAUTH_REDIRECT_URI_KEY)
1255 | }
1256 |
1257 | /**
1258 | * @private
1259 | * @returns {undefined}
1260 | */
1261 | _getState () {
1262 | return webStorage.getItem(OAUTH_STATE_KEY)
1263 | }
1264 |
1265 | /**
1266 | * @private
1267 | * @param {string} state - OAuth state from authorization grant flow.
1268 | * @returns {undefined}
1269 | */
1270 | _setState (state) {
1271 | webStorage.setItem(OAUTH_STATE_KEY, state)
1272 | }
1273 |
1274 | /**
1275 | * @private
1276 | * @returns {undefined}
1277 | */
1278 | _clearState () {
1279 | webStorage.removeItem(OAUTH_STATE_KEY)
1280 | }
1281 |
1282 | /**
1283 | * Retrieves the currently-set access token.
1284 | *
1285 | * @returns {string}
1286 | */
1287 | getAccessToken () {
1288 | return webStorage.getItem(OAUTH_ACCESS_TOKEN_KEY)
1289 | }
1290 |
1291 | /**
1292 | * Sets the given access token.
1293 | *
1294 | * @param {string} accessToken - auth API access token
1295 | * @returns {undefined}
1296 | */
1297 | setAccessToken (accessToken) {
1298 | webStorage.setItem(OAUTH_ACCESS_TOKEN_KEY, accessToken)
1299 | }
1300 |
1301 | /**
1302 | * Clears the currently-set access token.
1303 | *
1304 | * @returns {undefined}
1305 | */
1306 | clearAccessToken () {
1307 | webStorage.removeItem(OAUTH_ACCESS_TOKEN_KEY)
1308 | }
1309 |
1310 | /**
1311 | * Returns the currently-set token's expiration time in the following
1312 | * format: 'YYYY-MM-DDTHH:mm:ssZ'.
1313 | * For example, '2018-10-25T13:08:58-03:00'.
1314 | *
1315 | * @returns {string}
1316 | */
1317 | getExpirationTime () {
1318 | return webStorage.getItem(OAUTH_EXPIRATION_TIME_KEY)
1319 | }
1320 |
1321 | /**
1322 | * Sets the currently-set token's expiration time. The argument must be
1323 | * in the following format: 'YYYY-MM-DDTHH:mm:ssZ'.
1324 | * For example, '2018-10-25T13:08:58-03:00'.
1325 | *
1326 | * @param {string} expirationTime
1327 | * @returns {undefined}
1328 | */
1329 | setExpirationTime (expirationTime) {
1330 | webStorage.setItem(OAUTH_EXPIRATION_TIME_KEY, expirationTime)
1331 | }
1332 |
1333 | /**
1334 | * Clears the currently-set access token's expiration time.
1335 | *
1336 | * @returns {undefined}
1337 | */
1338 | clearExpirationTime () {
1339 | webStorage.removeItem(OAUTH_EXPIRATION_TIME_KEY)
1340 | }
1341 |
1342 | /**
1343 | * Retrieves the currently-set refresh token.
1344 | *
1345 | * @returns {string}
1346 | */
1347 | getRefreshToken () {
1348 | return webStorage.getItem(OAUTH_REFRESH_TOKEN_KEY)
1349 | }
1350 |
1351 | /**
1352 | * Sets the given refresh token.
1353 | *
1354 | * @param {string} refreshToken - auth API refresh token
1355 | * @returns {undefined}
1356 | */
1357 | setRefreshToken (refreshToken) {
1358 | webStorage.setItem(OAUTH_REFRESH_TOKEN_KEY, refreshToken)
1359 | }
1360 |
1361 | /**
1362 | * Clears the currently-set refresh token.
1363 | * @returns {undefined}
1364 | */
1365 | clearRefreshToken () {
1366 | webStorage.removeItem(OAUTH_REFRESH_TOKEN_KEY)
1367 | }
1368 |
1369 | /**
1370 | *
1371 | * Web crypto utilities.
1372 | *
1373 | */
1374 |
1375 | /**
1376 | * @private
1377 | * @param {string} key - HMAC key.
1378 | * @param {string} message- HMAC message.
1379 | * @returns {string}
1380 | */
1381 | static async _computeHmac256 (key, message) {
1382 | let cryptoKey = await webCrypto.subtle.importKey(
1383 | 'raw',
1384 | Buffer.from(key),
1385 | {
1386 | name: 'HMAC',
1387 | hash: { name: 'SHA-256' }
1388 | },
1389 | false,
1390 | ['sign', 'verify']
1391 | )
1392 | let signature = await webCrypto.subtle.sign(
1393 | 'HMAC',
1394 | cryptoKey,
1395 | Buffer.from(message)
1396 | )
1397 | return Buffer.from(new Uint8Array(signature)).toString('hex')
1398 | }
1399 | }
1400 |
1401 | return MoneyButtonClient
1402 | }
1403 |
1404 | import {
30 | toResourceObject,
31 | toNewResourceObject,
32 | fromResourceObject,
33 | fromResourceObjectsOfType,
34 | toJsonApiData,
35 | toJsonApiDataIncluding,
36 | fromJsonApiData,
37 | fromJsonApiDataIncluding
38 | } from '@moneybutton/json-api'
39 | import uuid from 'uuid'
40 | import fetch from 'isomorphic-fetch'
41 | import moment from 'moment'
42 | import queryString from 'query-string'
43 |
44 | import { AuthError, RestError } from './errors'
45 | import config from './config'
46 |
47 | const API_REST_URI = config.get('MONEY_BUTTON_API_REST_URI')
48 | const API_AUTH_URI = config.get('MONEY_BUTTON_API_AUTH_URI')
49 |
50 | const LOGIN_PASSWORD_HMAC_KEY = 'yours login password'
51 |
52 | const STORAGE_NAMESPACE = 'mb_js_client'
53 | const OAUTH_REDIRECT_URI_KEY = [STORAGE_NAMESPACE, 'oauth_redirect_uri'].join(':')
54 | const OAUTH_STATE_KEY = [STORAGE_NAMESPACE, 'oauth_state'].join(':')
55 | const OAUTH_ACCESS_TOKEN_KEY = [STORAGE_NAMESPACE, 'oauth_access_token'].join(':')
56 | const OAUTH_EXPIRATION_TIME_KEY = [STORAGE_NAMESPACE, 'oauth_expiration_time'].join(':')
57 | const OAUTH_REFRESH_TOKEN_KEY = [STORAGE_NAMESPACE, 'oauth_refresh_token'].join(':')
58 |
59 | /**
60 | * @param {Location} webLocation - Object conforming to the Location Web API.
61 | * @param {Storage} webStorage - Object conforming to the Storage Web API.
62 | * @param {Crypto} webCrypto - Object conforming to the Crypto Web API.
63 | */
64 | export default function (webLocation, webStorage, webCrypto) {
65 | /**
66 | *
67 | */
68 | class MoneyButtonClient {
69 | /**
70 | * Creates an instance of Money Button for the given OAuth client.
71 | *
72 | * @param {string} clientId - OAuth client's identifier.
73 | * @param {string} clientSecret - OAuth client's secret.
74 | */
75 | constructor (clientId, clientSecret = null) {
76 | this.clientId = clientId
77 | this.clientSecret = clientSecret
78 | }
79 |
80 | /**
81 | * Logs in the user with the given email and password.
82 | *
83 | * @param {string} email
84 | * @param {string} password
85 | * @returns {undefined}
86 | */
87 | async logIn (email, password) {
88 | if (await this.isLoggedIn()) {
89 | this.logOut()
90 | }
91 | const loginPassword = await MoneyButtonClient._computeHmac256(
92 | LOGIN_PASSWORD_HMAC_KEY,
93 | password
94 | )
95 | await this._doResourceOwnerPasswordCredentialsGrantAccessTokenRequest(
96 | email,
97 | loginPassword,
98 | 'general_access:write'
99 | )
100 | }
101 |
102 | /**
103 | * Determines whether a user is currently logged-in.
104 | *
105 | * @returns {boolean}
106 | */
107 | async isLoggedIn () {
108 | const accessToken = await this.getValidAccessToken()
109 | return accessToken !== null
110 | }
111 |
112 | /**
113 | * Retrieves a valid access token for the currently logged-in user.
114 | * Returns null if no user is currently logged-in.
115 | *
116 | * @returns {string|null}
117 | */
118 | async getValidAccessToken () {
119 | let accessToken = this.getAccessToken()
120 | if (
121 | accessToken !== null &&
122 | moment().isBefore(moment(this.getExpirationTime()))
123 | ) {
124 | return accessToken
125 | }
126 | const refreshToken = this.getRefreshToken()
127 | if (refreshToken === null) {
128 | return null
129 | }
130 | accessToken = null
131 | try {
132 | await this._doRefreshAccessTokenRequest(refreshToken)
133 | accessToken = this.getAccessToken()
134 | } catch (err) {
135 | if (!(err instanceof AuthError)) {
136 | throw err
137 | }
138 | }
139 | return accessToken
140 | }
141 |
142 | /**
143 | * Logs out the current logged-in user, if any.
144 | */
145 | logOut () {
146 | this.clearAccessToken()
147 | this.clearExpirationTime()
148 | this.clearRefreshToken()
149 | }
150 |
151 | /**
152 | * Finishes the email verification process with the access token generated
153 | * during signup.
154 | *
155 | * @param {string} accessToken - auth API access token
156 | * @returns {object}
157 | */
158 | async verifyEmail (accessToken) {
159 | const json = await this._doPostRequest(
160 | '/v1/auth/email_verification',
161 | {},
162 | {},
163 | accessToken
164 | )
165 | return fromResourceObject(fromJsonApiData(json), 'email_verifications')
166 | }
167 |
168 | /**
169 | * Retrieves the currently logged user's identity.
170 | *
171 | * @returns {object}
172 | */
173 | async getIdentity () {
174 | const json = await this._doGetRequest('/v1/auth/user_identity')
175 | return fromResourceObject(fromJsonApiData(json), 'user_identities')
176 | }
177 |
178 | /**
179 | * Returns an object with two keys:
180 | * - loggedIn: a boolean indicating whether there is a currently logged-in user.
181 | * - user: if loggedIn is true, this is an object with the user's attributes.
182 | *
183 | * @returns {object}
184 | */
185 | async whoAmI () {
186 | const loggedIn = await this.isLoggedIn()
187 | if (!loggedIn) {
188 | return { loggedIn }
189 | }
190 | const { id } = await this.getIdentity()
191 | const user = await this.getUser(id)
192 | return { loggedIn, user }
193 | }
194 |
195 | /**
196 | * Changes the currently logged-in user's password.
197 | *
198 | * @param {string} password
199 | * @param {string} encryptedMnemonic
200 | * @param {string} xpub
201 | * @param {string} language
202 | * @returns {object}
203 | */
204 | async changePassword (
205 | password,
206 | encryptedMnemonic,
207 | xpub,
208 | language
209 | ) {
210 | const loginPassword = await MoneyButtonClient._computeHmac256(
211 | LOGIN_PASSWORD_HMAC_KEY,
212 | password
213 | )
214 | const body = toJsonApiDataIncluding(
215 | toNewResourceObject('users', {
216 | password: loginPassword
217 | }),
218 | [
219 | toNewResourceObject('wallets', {
220 | encryptedMnemonic,
221 | xpub,
222 | language
223 | })
224 | ]
225 | )
226 | const json = await this._doPostRequest('/v1/auth/password_change', body)
227 | return fromResourceObject(fromJsonApiData(json), 'password_changes')
228 | }
229 |
230 | /**
231 | * Resets the currently logged-in user's password by using the access token
232 | * generated during the "I forgot my password" flow.
233 | *
234 | * @param {string} accessToken - auth API access token
235 | * @param {string} password
236 | * @param {string} encryptedMnemonic
237 | * @param {string} xpub
238 | * @param {boolean} forceCreate
239 | * @param {string} walletLanguage
240 | * @returns {object}
241 | */
242 | async resetPassword (
243 | accessToken,
244 | password,
245 | encryptedMnemonic,
246 | xpub,
247 | forceCreate,
248 | walletLanguage
249 | ) {
250 | const loginPassword = await MoneyButtonClient._computeHmac256(
251 | LOGIN_PASSWORD_HMAC_KEY,
252 | password
253 | )
254 | const body = toJsonApiDataIncluding(
255 | toNewResourceObject('users', {
256 | password: loginPassword
257 | }),
258 | [
259 | toNewResourceObject('wallets', {
260 | encryptedMnemonic,
261 | xpub,
262 | language: walletLanguage
263 | })
264 | ]
265 | )
266 | const query = forceCreate ? { forceCreate: 'true' } : {}
267 | const json = await this._doPostRequest(
268 | '/v1/auth/password_reset',
269 | body,
270 | query,
271 | accessToken
272 | )
273 | return fromResourceObject(fromJsonApiData(json), 'password_resets')
274 | }
275 |
276 | /**
277 | * Sends a password reset email to begin the "I forgot my password" flow.
278 | *
279 | * @param {string} email
280 | * @returns {object}
281 | */
282 | async sendPasswordReset (email) {
283 | if (await this.isLoggedIn()) {
284 | this.logOut()
285 | }
286 | await this._doClientCredentialsGrantAccessTokenRequest(
287 | 'auth.password_reset_email:write'
288 | )
289 | const attributes = { email }
290 | const body = toJsonApiData(toNewResourceObject('users', attributes))
291 | const json = await this._doPostRequest('/v1/auth/password_reset_email', body)
292 | this.logOut()
293 | return fromResourceObject(fromJsonApiData(json), 'password_reset_emails')
294 | }
295 |
296 | /**
297 | * Creates a new user account with the given email and password.
298 | *
299 | * @param {string} email
300 | * @param {string} password
301 | * @returns {object}
302 | */
303 | async signUp (email, password) {
304 | if (await this.isLoggedIn()) {
305 | this.logOut()
306 | }
307 | await this._doClientCredentialsGrantAccessTokenRequest(
308 | 'auth.signup:write'
309 | )
310 | const loginPassword = await MoneyButtonClient._computeHmac256(
311 | LOGIN_PASSWORD_HMAC_KEY,
312 | password
313 | )
314 | const attributes = {
315 | email,
316 | password: loginPassword
317 | }
318 | const body = toJsonApiData(toNewResourceObject('users', attributes))
319 | const json = await this._doPostRequest('/v1/auth/signup', body)
320 | await this.logIn(email, password)
321 | return fromResourceObject(fromJsonApiData(json), 'signups')
322 | }
323 |
324 | /**
325 | * [Browser only] Starts the authorization flow which allows third-party applications
326 | * to request access to user resources on their behalf. This function will
327 | * redirect the user's window to the Money Button's authorization flow page.
328 | *
329 | * @param {string} scope - scope to be requested to the user.
330 | * @param {string} redirectUri - URI where the authorization response will be handled.
331 | * @returns {undefined}
332 | */
333 | requestAuthorization (
334 | scope,
335 | redirectUri
336 | ) {
337 | if (typeof scope !== 'string' || scope.length === 0) {
338 | throw new Error(`Invalid scope requested: ${scope}.`)
339 | }
340 | if (typeof redirectUri !== 'string' || redirectUri.length === 0) {
341 | throw new Error(`Invalid return URI: ${redirectUri}.`)
342 | }
343 | this._doAuthorizationCodeGrantAuthorizationRequest(redirectUri, scope)
344 | }
345 |
346 | /**
347 | * [Browser only] Finishes the authorization flow started by {@link requestAuthorization}.
348 | * If successful, after calling this function, the client will be able to perform requests
349 | * on behalf of the user as long as they are within the scope requested when starting the
350 | * authorization flow.
351 | *
352 | * @returns {undefined}
353 | */
354 | async handleAuthorizationResponse () {
355 | const { error, code, state } = this._getUrlQuery()
356 | await this._handleAuthorizationCodeGrantAuthorizationResponse(error, code, state)
357 | }
358 |
359 | /**
360 | * See: https://tools.ietf.org/html/rfc6749#page-24.
361 | *
362 | * @private
363 | * @param {string} redirectUri
364 | * @param {string} scope
365 | */
366 | _doAuthorizationCodeGrantAuthorizationRequest (
367 | redirectUri,
368 | scope
369 | ) {
370 | if (this.clientSecret !== null) {
371 | throw new Error([
372 | 'Grant `authentication_code` can only be performed by ',
373 | 'a public client (that is, a client with no client secret).'
374 | ].join(''))
375 | }
376 | const state = uuid.v4()
377 | this._setRedirectUri(redirectUri)
378 | this._setState(state)
379 | const authorizationUri = [
380 | `${API_AUTH_URI}/oauth/v1/authorize`,
381 | queryString.stringify({
382 | response_type: 'code',
383 | client_id: this.clientId,
384 | redirect_uri: redirectUri,
385 | scope,
386 | state
387 | })
388 | ].join('?')
389 | this._redirectToUri(authorizationUri)
390 | }
391 |
392 | /**
393 | * See: https://tools.ietf.org/html/rfc6749#page-26.
394 | *
395 | * @private
396 | */
397 | async _handleAuthorizationCodeGrantAuthorizationResponse (
398 | error,
399 | code,
400 | state
401 | ) {
402 | const expectedState = this._getState()
403 | if (expectedState === null || state !== expectedState) {
404 | throw new Error('Invalid OAuth state.')
405 | }
406 | if (error !== undefined) {
407 | throw new AuthError('Authorization failed.', error)
408 | }
409 | if (code === undefined) {
410 | throw new Error('Missing OAuth authorization code.')
411 | }
412 | await this._doAuthorizationCodeGrantAccessTokenRequest(code)
413 | }
414 |
415 | /**
416 | * See: https://tools.ietf.org/html/rfc6749#page-29.
417 | *
418 | * @private
419 | */
420 | async _doAuthorizationCodeGrantAccessTokenRequest (
421 | code
422 | ) {
423 | const redirectUri = this._getRedirectUri()
424 | if (redirectUri === null) {
425 | throw new Error('Required OAuth redirect URI not found in storage.')
426 | }
427 | await this._doAccessTokenRequest(
428 | {
429 | grant_type: 'authorization_code',
430 | code,
431 | redirect_uri: redirectUri,
432 | client_id: this.clientId
433 | }
434 | )
435 | }
436 |
437 | /**
438 | * See: https://tools.ietf.org/html/rfc6749#page-37.
439 | *
440 | * @private
441 | */
442 | async _doResourceOwnerPasswordCredentialsGrantAccessTokenRequest (
443 | username,
444 | password,
445 | scope
446 | ) {
447 | await this._doAccessTokenRequest(
448 | {
449 | grant_type: 'password',
450 | username,
451 | password,
452 | scope
453 | },
454 | this._buildBasicAuthHeaders()
455 | )
456 | }
457 |
458 | /**
459 | * See: https://tools.ietf.org/html/rfc6749#page-41.
460 | *
461 | * @private
462 | */
463 | async _doClientCredentialsGrantAccessTokenRequest (scope) {
464 | await this._doAccessTokenRequest(
465 | {
466 | grant_type: 'client_credentials',
467 | scope
468 | },
469 | this._buildBasicAuthHeaders()
470 | )
471 | }
472 |
473 | /**
474 | * @private
475 | * @param {string} refreshToken
476 | */
477 | async _doRefreshAccessTokenRequest (refreshToken) {
478 | await this._doAccessTokenRequest(
479 | {
480 | grant_type: 'refresh_token',
481 | refresh_token: refreshToken
482 | },
483 | this._buildBasicAuthHeaders()
484 | )
485 | }
486 |
487 | /**
488 | * @private
489 | */
490 | _buildBasicAuthHeaders () {
491 | const credentials = `${this.clientId}:${this.clientSecret}`
492 | return {
493 | Authorization: `Basic ${Buffer.from(credentials).toString('base64')}`
494 | }
495 | }
496 |
497 | /**
498 | * @private
499 | * @param {object} body
500 | * @param {object} headers
501 | */
502 | async _doAccessTokenRequest (body = {}, headers = {}) {
503 | const res = await fetch(
504 | `${API_AUTH_URI}/oauth/v1/token`,
505 | {
506 | method: 'POST',
507 | body: queryString.stringify(body),
508 | headers: {
509 | ...headers,
510 | 'Content-Type': 'application/x-www-form-urlencoded'
511 | }
512 | }
513 | )
514 | await this._handleAccessTokenResponse(res)
515 | }
516 |
517 | /**
518 | * @private
519 | * @param {Response} res - Express.js response object.
520 | */
521 | async _handleAccessTokenResponse (res) {
522 | const {
523 | error,
524 | error_description: errorDescription,
525 | access_token: accessToken,
526 | token_type: tokenType,
527 | expires_in: expiresIn,
528 | refresh_token: refreshToken
529 | } = await res.json()
530 | if (error !== undefined) {
531 | throw new AuthError(error, errorDescription)
532 | }
533 | if (tokenType !== 'Bearer') {
534 | throw new Error('Unexpected token type.')
535 | }
536 | if (accessToken !== undefined) {
537 | this.setAccessToken(accessToken)
538 | } else {
539 | this.clearAccessToken()
540 | }
541 | if (expiresIn !== undefined) {
542 | const expirationTime = moment().add(expiresIn, 'seconds')
543 | this.setExpirationTime(expirationTime.format())
544 | } else {
545 | this.clearExpirationTime()
546 | }
547 | if (refreshToken !== undefined) {
548 | this.setRefreshToken(refreshToken)
549 | } else {
550 | this.clearRefreshToken()
551 | }
552 | }
553 |
554 | /**
555 | * Get basic information from the OAuth client with the given identifier.
556 | *
557 | * @param {string} clientIdentifier
558 | * @returns {object}
559 | */
560 | async getClientByIdentifier (clientIdentifier) {
561 | const json = await this._doGetRequest(`/v1/clients/client_identifier=${clientIdentifier}`)
562 | return fromResourceObject(fromJsonApiData(json), 'clients')
563 | }
564 |
565 | /**
566 | * Retrives the user with the given user id.
567 | *
568 | * @param {string} userId
569 | * @returns {object}
570 | */
571 | async getUser (userId) {
572 | let json = await this._doGetRequest(`/v1/users/${userId}`)
573 | return fromResourceObject(fromJsonApiData(json), 'users')
574 | }
575 |
576 | /**
577 | * Updates the user with the given user id.
578 | *
579 | * @param {string} userId
580 | * @param {object} attributes
581 | * @returns {object}
582 | */
583 | async updateUser (userId, attributes = {}) {
584 | const body = toJsonApiData(toResourceObject(userId, 'users', attributes))
585 | const json = await this._doPatchRequest(`/v1/users/${userId}`, body)
586 | return fromResourceObject(fromJsonApiData(json), 'users')
587 | }
588 |
589 | /**
590 | * Retrives the transaction history of the user with the given user id.
591 | *
592 | * @param {string} userId
593 | * @param {object} query
594 | * @returns {object}
595 | */
596 | async getUserTransactionHistory (userId, query = {}) {
597 | const json = await this._doGetRequest(
598 | `/v1/users/${userId}/transaction_history`,
599 | query
600 | )
601 | return fromResourceObjectsOfType(
602 | fromJsonApiData(json),
603 | 'transaction_history'
604 | )
605 | }
606 |
607 | /**
608 | * Retrives the OAuth clients of the user with the given user id.
609 | *
610 | * @param {string} userId
611 | * @param {object} query
612 | * @returns {object}
613 | */
614 | async getUserClients (userId, query = {}) {
615 | const json = await this._doGetRequest(
616 | `/v1/users/${userId}/clients`,
617 | query
618 | )
619 | return fromResourceObjectsOfType(fromJsonApiData(json), 'clients')
620 | }
621 |
622 | /**
623 | * Creates an OAuth client for the user with the given user id.
624 | *
625 | * @param {string} userId
626 | * @param {object} attributes
627 | * @returns {object}
628 | */
629 | async createUserClient (userId, attributes) {
630 | let body = toJsonApiData(toNewResourceObject('clients', attributes))
631 | const json = await this._doPostRequest(`/v1/users/${userId}/clients`, body)
632 | return fromResourceObject(fromJsonApiData(json), 'clients')
633 | }
634 |
635 | /**
636 | * Updates an OAuth client for the user with the given user id.
637 | *
638 | * @param {string} userId
639 | * @param {string} clientId
640 | * @param {object} attributes
641 | * @returns {object}
642 | */
643 | async updateUserClient (userId, clientId, attributes = {}) {
644 | const body = toJsonApiData(
645 | toResourceObject(clientId, 'clients', attributes)
646 | )
647 | await this._doPatchRequest(
648 | `/v1/users/${userId}/clients/${clientId}`,
649 | body
650 | )
651 | }
652 |
653 | /**
654 | * Retrives the wallet with the given wallet id for the user with
655 | * the given user id.
656 | *
657 | * @param {string} userId
658 | * @param {string} walletId
659 | * @returns {object}
660 | */
661 | async getUserWallet (userId, walletId) {
662 | let json = await this._doGetRequest(
663 | `/v1/users/${userId}/wallets/${walletId}`
664 | )
665 | return fromResourceObject(fromJsonApiData(json), 'wallets')
666 | }
667 |
668 | /**
669 | * Retrives the wallets of the user with the given user id.
670 | *
671 | * @param {string} userId
672 | * @returns {object}
673 | */
674 | async getUserWallets (userId) {
675 | let json = await this._doGetRequest(`/v1/users/${userId}/wallets/`)
676 | return fromResourceObjectsOfType(fromJsonApiData(json), 'wallets')
677 | }
678 |
679 | /**
680 | * Retrives the max withdrawal amount for the wallet with the given wallet id,
681 | * belonging to the user with the given user id.
682 | *
683 | * @param {string} userId
684 | * @param {string} walletId
685 | * @returns {object}
686 | */
687 | async getMaxWithdrawalForWallet (userId, walletId) {
688 | let json = await this._doGetRequest(`/v1/users/${userId}/wallets/${walletId}/max_withdrawal`)
689 | return fromResourceObject(fromJsonApiData(json), 'amounts')
690 | }
691 |
692 | /**
693 | * Creates a wallet for the user with the given user id.
694 | *
695 | * @param {string} userId
696 | * @param {object} attributes
697 | * @returns {object}
698 | */
699 | async createUserWallet (userId, attributes) {
700 | let body = toJsonApiData(toNewResourceObject('wallets', attributes))
701 | let json = await this._doPostRequest(`/v1/users/${userId}/wallets`, body)
702 | return fromResourceObject(fromJsonApiData(json), 'wallets')
703 | }
704 |
705 | /**
706 | * Retrieves the balance from the user with given user id.
707 | *
708 | * @param {string} userId
709 | * @returns {object}
710 | */
711 | async getBalance (userId) {
712 | const json = await this._doGetRequest(`/v1/users/${userId}/balance`)
713 | return fromResourceObject(fromJsonApiData(json), 'amounts')
714 | }
715 |
716 | /**
717 | * Retrives the max withdrawal amount the user with the given user id.
718 | *
719 | * @param {string} userId
720 | * @returns {object}
721 | */
722 | async getMaxWithdrawal (userId) {
723 | let json = await this._doGetRequest(`/v1/users/${userId}/max_withdrawal`)
724 | return fromResourceObject(fromJsonApiData(json), 'amounts')
725 | }
726 |
727 | /**
728 | * Retrives a recieve address for the user with the given user id.
729 | *
730 | * @param {string} userId
731 | * @param {string} walletId
732 | * @returns {object}
733 | */
734 | async getReceiveAddress (userId, walletId) {
735 | let json = await this._doPostRequest(
736 | `/v1/users/${userId}/wallets/${walletId}/receive_address`
737 | )
738 | let { address } = fromResourceObject(fromJsonApiData(json), 'addresses')
739 | return address
740 | }
741 |
742 | /**
743 | * Converts a (curreny,amount) pair into the given user's default currency.
744 | *
745 | * @param {string} userId
746 | * @param {object} attributes
747 | * @returns {object}
748 | */
749 | async getCurrencyAmount (userId, attributes) {
750 | const body = toJsonApiData(toNewResourceObject('currency', attributes))
751 | const json = await this._doPostRequest(
752 | `/v1/users/${userId}/currency`,
753 | body
754 | )
755 | const { amount, currency } = fromResourceObject(
756 | fromJsonApiData(json),
757 | 'currency'
758 | )
759 | return { amount, currency }
760 | }
761 |
762 | /**
763 | * Retrives the balance for the wallet with the given wallet id,
764 | * belonging to the user with the given user id.
765 | *
766 | * @param {string} userId
767 | * @param {string} walletId
768 | * @returns {object}
769 | */
770 | async getWalletBalance (userId, walletId) {
771 | const json = await this._doGetRequest(
772 | `/v1/users/${userId}/wallets/${walletId}/balance`
773 | )
774 | return fromResourceObject(fromJsonApiData(json), 'amounts')
775 | }
776 |
777 | /**
778 | * Updates the wallet with the given wallet id, belonging to the user
779 | * with the given user id.
780 | *
781 | * @param {string} userId
782 | * @param {string} walletId
783 | * @param {object} attributes
784 | * @returns {object}
785 | */
786 | async updateWallet (userId, walletId, attributes) {
787 | const body = toJsonApiData(
788 | toResourceObject(walletId, 'wallets', attributes)
789 | )
790 | await this._doPatchRequest(
791 | `/v1/users/${userId}/wallets/${walletId}`,
792 | body
793 | )
794 | }
795 |
796 | /**
797 | * Retrieves the payments from the user with the given user id.
798 | *
799 | * @param {string} userId
800 | * @param {object} paginate
801 | * @returns {object}
802 | */
803 | async getUserPayments (userId, paginate) {
804 | const json = await this._doGetRequest(
805 | `/v1/users/${userId}/payments?${this._paginateUri(paginate)}`
806 | )
807 | return {
808 | pages: json.meta['total-pages'],
809 | payments: json.data.map(payment =>
810 | fromResourceObject(payment, 'payments')
811 | )
812 | }
813 | }
814 |
815 | /**
816 | * @private
817 | * @returns {string}
818 | */
819 | _paginateUri ({ number, size, sort }) {
820 | // NOTE: query-string does not support the nesting format used in JsonApi
821 | // https://github.com/sindresorhus/query-string#nesting
822 | // http://jsonapi.org/examples/#pagination
823 | // http://jsonapi.org/format/#fetching-pagination
824 | const url = []
825 | if (number) url.push(`page[number]=${number}`)
826 | if (size) url.push(`page[size]=${size}`)
827 | if (sort) url.push(`sort=${sort}`)
828 | return url.join('&')
829 | }
830 |
831 | /**
832 | * Creates a payment for the user with the given user id to the specified payment
833 | * outputs.
834 | *
835 | * @param {string} userId
836 | * @param {object} attributes
837 | * @param {array} paymentOutputs
838 | * @returns {object}
839 | */
840 | async createUserPayment (userId, attributes, paymentOutputs) {
841 | let body = toJsonApiDataIncluding(
842 | toNewResourceObject('payments', attributes),
843 | paymentOutputs.map(paymentOutput => {
844 | return toNewResourceObject('payment_outputs', paymentOutput)
845 | })
846 | )
847 | let json = await this._doPostRequest(`/v1/users/${userId}/payments`, body)
848 | let { data, included } = fromJsonApiDataIncluding(json)
849 | let payment = fromResourceObject(data, 'payments')
850 | let [bsvTransaction] = fromResourceObjectsOfType(included, 'bsv_transactions')
851 | let addressIndexes = fromResourceObjectsOfType(
852 | included,
853 | 'address_indexes'
854 | )
855 | .sort((a, b) => a.index - b.index)
856 | .map(addressIndex => addressIndex.addressIndex)
857 | return {
858 | payment,
859 | paymentOutputs: fromResourceObjectsOfType(included, 'payment_outputs'),
860 | bsvTransaction,
861 | addressIndexes
862 | }
863 | }
864 |
865 | /**
866 | * Retrives the payment with the given payment id, belonging to the user with
867 | * the given user id.
868 | *
869 | * @param {string} userId
870 | * @param {string} paymentId
871 | * @returns {object}
872 | */
873 | async getUserPayment (userId, paymentId) {
874 | let json = await this._doGetRequest(`/v1/users/${userId}/payments/${paymentId}`)
875 | let { data, included } = fromJsonApiDataIncluding(json)
876 | const payment = fromResourceObject(data, 'payments')
877 | payment.outputs = fromResourceObjectsOfType(included, 'payment_outputs')
878 | return payment
879 | }
880 |
881 | /**
882 | * Updates the payment with the given payment id, belonging to the user with
883 | * the given user id.
884 | *
885 | * @param {string} userId
886 | * @param {string} paymentId
887 | * @param {object} attributes
888 | * @param {bsv.Transaction} bsvTransaction
889 | * @returns {object}
890 | */
891 | async updateUserPaymentWithTransaction (
892 | userId,
893 | paymentId,
894 | attributes,
895 | bsvTransaction
896 | ) {
897 | let body = toJsonApiDataIncluding(
898 | toResourceObject(paymentId, 'payments', attributes),
899 | [toResourceObject(
900 | bsvTransaction.hash,
901 | 'bsv_transactions',
902 | bsvTransaction
903 | )]
904 | )
905 | let json = await this._doPatchRequest(
906 | `/v1/users/${userId}/payments/${paymentId}`,
907 | body
908 | )
909 | return fromResourceObject(fromJsonApiData(json), 'payments')
910 | }
911 |
912 | /**
913 | * Creates a deposit for the user with the given id.
914 | *
915 | * @param {string} userId
916 | * @param {object} attributes
917 | * @returns {object}
918 | */
919 | async createUserDeposit (userId, attributes) {
920 | const body = toJsonApiData(toNewResourceObject('deposits', attributes))
921 | const json = await this._doPostRequest(
922 | `/v1/users/${userId}/deposits`,
923 | body
924 | )
925 | return fromResourceObject(fromJsonApiData(json), 'deposits')
926 | }
927 |
928 | /**
929 | * Retrives the deposit with the given deposit id, belonging to the user with
930 | * the given user id.
931 | *
932 | * @param {string} userId
933 | * @param {string} depositId
934 | * @returns {object}
935 | */
936 | async getUserDeposit (userId, depositId) {
937 | const json = await this._doGetRequest(
938 | `/v1/users/${userId}/deposits/${depositId}`
939 | )
940 | return fromResourceObject(fromJsonApiData(json), 'deposits')
941 | }
942 |
943 | /**
944 | *
945 | * @param {string} userId
946 | * @param {object} attributes
947 | * @returns {object}
948 | */
949 | async createUserWithdrawal (userId, attributes) {
950 | let body = toJsonApiData(toNewResourceObject('withdrawals', attributes))
951 | let json = await this._doPostRequest(
952 | `/v1/users/${userId}/withdrawals`,
953 | body
954 | )
955 | let { data, included } = fromJsonApiDataIncluding(json)
956 | let withdrawal = fromResourceObject(data, 'withdrawals')
957 | let [bsvTransaction] = fromResourceObjectsOfType(included, 'bsv_transactions')
958 | let addressIndexes = fromResourceObjectsOfType(
959 | included,
960 | 'address_indexes'
961 | )
962 | .sort((a, b) => a.index - b.index)
963 | .map(addressIndex => addressIndex.addressIndex)
964 | return {
965 | withdrawal,
966 | bsvTransaction,
967 | addressIndexes
968 | }
969 | }
970 |
971 | /**
972 | * Retrives the withdrawal with the given withdrawal id, belonging to the user with
973 | * the given user id.
974 | *
975 | * @param {string} userId
976 | * @param {string} withdrawalId
977 | * @returns {object}
978 | */
979 | async getUserWithdrawal (userId, withdrawalId) {
980 | const json = await this._doGetRequest(
981 | `/v1/users/${userId}/withdrawals/${withdrawalId}`
982 | )
983 | return fromResourceObject(fromJsonApiData(json), 'withdrawals')
984 | }
985 |
986 | /**
987 | * Updates the withdrawal with the given withdrawal id, belonging to the user with
988 | * the given user id.
989 | *
990 | * @param {string} userId
991 | * @param {string} withdrawalId
992 | * @param {object} attributes
993 | * @param {object} transaction
994 | * @returns {object}
995 | */
996 | async updateUserWithdrawalWithTransaction (
997 | userId,
998 | withdrawalId,
999 | attributes,
1000 | transaction
1001 | ) {
1002 | let body = toJsonApiDataIncluding(
1003 | toResourceObject(withdrawalId, 'withdrawals', attributes),
1004 | [toResourceObject(uuid.v1(), 'transactions', transaction)]
1005 | )
1006 | let json = await this._doPatchRequest(
1007 | `/v1/users/${userId}/withdrawals/${withdrawalId}`,
1008 | body
1009 | )
1010 | return fromResourceObject(fromJsonApiData(json), 'withdrawals')
1011 | }
1012 |
1013 | /**
1014 | * Broadcasts the given bsv transaction. The transaction must be fully signed.
1015 | *
1016 | * @param {bsv.Transaction} bsvTransaction
1017 | * @returns {object}
1018 | */
1019 | async broadcastTransaction (bsvTransaction) {
1020 | const body = toJsonApiData(toResourceObject(
1021 | bsvTransaction.hash,
1022 | 'bsv_transactions',
1023 | bsvTransaction
1024 | ))
1025 | const json = await this._doPostRequest(
1026 | '/v1/transactions/broadcast',
1027 | body
1028 | )
1029 | return fromResourceObject(fromJsonApiData(json), 'txids')
1030 | }
1031 |
1032 | /**
1033 | * Retrieves the list of supported cryptocurrencies.
1034 | *
1035 | * @param {object} query
1036 | * @returns {array}
1037 | */
1038 | async getSupportedCryptocurrencies (query = {}) {
1039 | const json = await this._doGetRequest('/v1/currencies/crypto', query)
1040 | return fromResourceObjectsOfType(fromJsonApiData(json), 'currencies')
1041 | }
1042 |
1043 | /**
1044 | * Retrieves the list of supported fiat currencies.
1045 | *
1046 | * @param {object} query
1047 | * @returns {array}
1048 | */
1049 | async getSupportedFiatCurrencies (query = {}) {
1050 | const json = await this._doGetRequest('/v1/currencies/fiat', query)
1051 | return fromResourceObjectsOfType(fromJsonApiData(json), 'currencies')
1052 | }
1053 |
1054 | /**
1055 | * @private
1056 | * @param {string} endpoint - REST API relative endpoint.
1057 | * @param {object} query - URL query parameters.
1058 | * @param {string} accessToken - auth API access token
1059 | * @returns {object}
1060 | */
1061 | async _doGetRequest (endpoint, query = {}, accessToken = null) {
1062 | let opts = {
1063 | method: 'GET'
1064 | }
1065 | return this._doRequest(endpoint, opts, query, accessToken)
1066 | }
1067 |
1068 | /**
1069 | * @private
1070 | * @param {string} endpoint - REST API relative endpoint.
1071 | * @param {object} body - fetch request's body.
1072 | * @param {object} query - URL query parameters.
1073 | * @param {string} accessToken - auth API access token
1074 | * @returns {object}
1075 | */
1076 | async _doPostRequest (endpoint, body = {}, query = {}, accessToken = null) {
1077 | let opts = {
1078 | method: 'POST',
1079 | body: JSON.stringify(body)
1080 | }
1081 | return this._doRequest(endpoint, opts, query, accessToken)
1082 | }
1083 |
1084 | /**
1085 | * @private
1086 | * @param {string} endpoint - REST API relative endpoint.
1087 | * @param {object} body - fetch request's body.
1088 | * @param {string} accessToken - auth API access token
1089 | * @returns {object}
1090 | */
1091 | async _doPatchRequest (endpoint, body = {}, accessToken = null) {
1092 | let opts = {
1093 | method: 'PATCH',
1094 | body: JSON.stringify(body)
1095 | }
1096 | return this._doRequest(endpoint, opts, {}, accessToken)
1097 | }
1098 |
1099 | /**
1100 | * @private
1101 | * @param {string} endpoint - REST API relative endpoint.
1102 | * @param {object} body - fetch request's body.
1103 | * @param {string} accessToken - auth API access token
1104 | * @returns {object}
1105 | */
1106 | async _doPutRequest (endpoint, body = {}, accessToken = null) {
1107 | let opts = {
1108 | method: 'PUT',
1109 | body: JSON.stringify(body)
1110 | }
1111 | return this._doRequest(endpoint, opts, {}, accessToken)
1112 | }
1113 |
1114 | /**
1115 | *
1116 | * @param {string} endpoint - REST API relative endpoint.
1117 | * @param {object} opts - fetch request options.
1118 | * @param {object} query - URL query parameters.
1119 | * @param {string} accessToken - auth API access token
1120 | * @returns {object}
1121 | */
1122 | async _doRequest (endpoint, opts = {}, query = {}, accessToken = null) {
1123 | const url = this._appendQuery(`${API_REST_URI}/api${endpoint}`, query)
1124 | let headers = {
1125 | 'Content-Type': 'application/vnd.api+json',
1126 | Accept: 'application/vnd.api+json'
1127 | }
1128 | accessToken = accessToken === null
1129 | ? await this.getValidAccessToken()
1130 | : accessToken
1131 | if (accessToken !== null) {
1132 | headers['Authorization'] = `Bearer ${accessToken}`
1133 | }
1134 | const res = await fetch(url, { ...opts, headers })
1135 | let json = await res.json()
1136 | let { errors } = json
1137 | if (errors instanceof Array) {
1138 | let error = errors[0]
1139 | if (error.status) {
1140 | let { status, title, detail } = error
1141 | throw new RestError(status, title, detail)
1142 | }
1143 | throw new Error(error.title)
1144 | }
1145 | return json
1146 | }
1147 |
1148 | /**
1149 | * @private
1150 | * @param {string} url - base URL where query will be appended.
1151 | * @param {object} query - URL query parameters.
1152 | * @returns {string}
1153 | */
1154 | _appendQuery (url, query = {}) {
1155 | if (Object.keys(query).length === 0) {
1156 | return url
1157 | }
1158 | const { page, ...queryWithoutPage } = query
1159 | if (page !== undefined) {
1160 | for (const key in page) {
1161 | queryWithoutPage[`page[${key}]`] = page[key]
1162 | }
1163 | }
1164 | return `${url}?${queryString.stringify(queryWithoutPage)}`
1165 | }
1166 |
1167 | /**
1168 | *
1169 | * Web location utilities.
1170 | *
1171 | */
1172 |
1173 | /**
1174 | *
1175 | */
1176 | _getUrlQuery () {
1177 | return queryString.parse(webLocation.search)
1178 | }
1179 |
1180 | /**
1181 | *
1182 | * @param {string} uri - URI where the browser will be redirected to.
1183 | */
1184 | _redirectToUri (uri) {
1185 | webLocation.href = uri
1186 | }
1187 |
1188 | /**
1189 | *
1190 | * Web storage utilities.
1191 | *
1192 | */
1193 |
1194 | /**
1195 | * @private
1196 | * @returns {string}
1197 | */
1198 | _getRedirectUri () {
1199 | return webStorage.getItem(OAUTH_REDIRECT_URI_KEY)
1200 | }
1201 |
1202 | /**
1203 | * @private
1204 | * @param {string} redirectUri - OAuth redirect URI from authorization grant flow.
1205 | * @returns {undefined}
1206 | */
1207 | _setRedirectUri (redirectUri) {
1208 | webStorage.setItem(OAUTH_REDIRECT_URI_KEY, redirectUri)
1209 | }
1210 |
1211 | /**
1212 | * @private
1213 | * @returns {undefined}
1214 | */
1215 | _clearRedirectUri () {
1216 | webStorage.removeItem(OAUTH_REDIRECT_URI_KEY)
1217 | }
1218 |
1219 | /**
1220 | * @private
1221 | * @returns {undefined}
1222 | */
1223 | _getState () {
1224 | return webStorage.getItem(OAUTH_STATE_KEY)
1225 | }
1226 |
1227 | /**
1228 | * @private
1229 | * @param {string} state - OAuth state from authorization grant flow.
1230 | * @returns {undefined}
1231 | */
1232 | _setState (state) {
1233 | webStorage.setItem(OAUTH_STATE_KEY, state)
1234 | }
1235 |
1236 | /**
1237 | * @private
1238 | * @returns {undefined}
1239 | */
1240 | _clearState () {
1241 | webStorage.removeItem(OAUTH_STATE_KEY)
1242 | }
1243 |
1244 | /**
1245 | * Retrieves the currently-set access token.
1246 | *
1247 | * @returns {string}
1248 | */
1249 | getAccessToken () {
1250 | return webStorage.getItem(OAUTH_ACCESS_TOKEN_KEY)
1251 | }
1252 |
1253 | /**
1254 | * Sets the given access token.
1255 | *
1256 | * @param {string} accessToken - auth API access token
1257 | * @returns {undefined}
1258 | */
1259 | setAccessToken (accessToken) {
1260 | webStorage.setItem(OAUTH_ACCESS_TOKEN_KEY, accessToken)
1261 | }
1262 |
1263 | /**
1264 | * Clears the currently-set access token.
1265 | *
1266 | * @returns {undefined}
1267 | */
1268 | clearAccessToken () {
1269 | webStorage.removeItem(OAUTH_ACCESS_TOKEN_KEY)
1270 | }
1271 |
1272 | /**
1273 | * Returns the currently-set token's expiration time in the following
1274 | * format: 'YYYY-MM-DDTHH:mm:ssZ'.
1275 | * For example, '2018-10-25T13:08:58-03:00'.
1276 | *
1277 | * @returns {string}
1278 | */
1279 | getExpirationTime () {
1280 | return webStorage.getItem(OAUTH_EXPIRATION_TIME_KEY)
1281 | }
1282 |
1283 | /**
1284 | * Sets the currently-set token's expiration time. The argument must be
1285 | * in the following format: 'YYYY-MM-DDTHH:mm:ssZ'.
1286 | * For example, '2018-10-25T13:08:58-03:00'.
1287 | *
1288 | * @param {string} expirationTime
1289 | * @returns {undefined}
1290 | */
1291 | setExpirationTime (expirationTime) {
1292 | webStorage.setItem(OAUTH_EXPIRATION_TIME_KEY, expirationTime)
1293 | }
1294 |
1295 | /**
1296 | * Clears the currently-set access token's expiration time.
1297 | *
1298 | * @returns {undefined}
1299 | */
1300 | clearExpirationTime () {
1301 | webStorage.removeItem(OAUTH_EXPIRATION_TIME_KEY)
1302 | }
1303 |
1304 | /**
1305 | * Retrieves the currently-set refresh token.
1306 | *
1307 | * @returns {string}
1308 | */
1309 | getRefreshToken () {
1310 | return webStorage.getItem(OAUTH_REFRESH_TOKEN_KEY)
1311 | }
1312 |
1313 | /**
1314 | * Sets the given refresh token.
1315 | *
1316 | * @param {string} refreshToken - auth API refresh token
1317 | * @returns {undefined}
1318 | */
1319 | setRefreshToken (refreshToken) {
1320 | webStorage.setItem(OAUTH_REFRESH_TOKEN_KEY, refreshToken)
1321 | }
1322 |
1323 | /**
1324 | * Clears the currently-set refresh token.
1325 | * @returns {undefined}
1326 | */
1327 | clearRefreshToken () {
1328 | webStorage.removeItem(OAUTH_REFRESH_TOKEN_KEY)
1329 | }
1330 |
1331 | /**
1332 | *
1333 | * Web crypto utilities.
1334 | *
1335 | */
1336 |
1337 | /**
1338 | * @private
1339 | * @param {string} key - HMAC key.
1340 | * @param {string} message- HMAC message.
1341 | * @returns {string}
1342 | */
1343 | static async _computeHmac256 (key, message) {
1344 | let cryptoKey = await webCrypto.subtle.importKey(
1345 | 'raw',
1346 | Buffer.from(key),
1347 | {
1348 | name: 'HMAC',
1349 | hash: { name: 'SHA-256' }
1350 | },
1351 | false,
1352 | ['sign', 'verify']
1353 | )
1354 | let signature = await webCrypto.subtle.sign(
1355 | 'HMAC',
1356 | cryptoKey,
1357 | Buffer.from(message)
1358 | )
1359 | return Buffer.from(new Uint8Array(signature)).toString('hex')
1360 | }
1361 | }
1362 |
1363 | return MoneyButtonClient
1364 | }
1365 |
1366 | // TODO(ealmansi): fix warning "(!) Mixing named and default exports."
1367 | export { AuthError, RestError }
1368 |
1369 | Name | 71 | 72 | 73 |Type | 74 | 75 | 76 | 77 | 78 | 79 |Description | 80 |
---|---|---|
title |
89 |
90 |
91 | 92 | 93 | 94 | string 95 | 96 | 97 | 98 | | 99 | 100 | 101 | 102 | 103 | 104 |Error title. | 105 |
detail |
112 |
113 |
114 | 115 | 116 | 117 | string 118 | 119 | 120 | 121 | | 122 | 123 | 124 | 125 | 126 | 127 |Error detail. | 128 |
Name | 272 | 273 | 274 |Type | 275 | 276 | 277 | 278 | 279 | 280 |Description | 281 |
---|---|---|
status |
290 |
291 |
292 | 293 | 294 | 295 | number 296 | 297 | 298 | 299 | | 300 | 301 | 302 | 303 | 304 | 305 |HTTP status code. | 306 |
title |
313 |
314 |
315 | 316 | 317 | 318 | string 319 | 320 | 321 | 322 | | 323 | 324 | 325 | 326 | 327 | 328 |Error title. | 329 |
detail |
336 |
337 |
338 | 339 | 340 | 341 | string 342 | 343 | 344 | 345 | | 346 | 347 | 348 | 349 | 350 | 351 |Error detail. | 352 |
/**
30 | * REST API error.
31 | */
32 | export default class RestError {
33 | /**
34 | *
35 | * @param {number} status - HTTP status code.
36 | * @param {string} title - Error title.
37 | * @param {string} detail - Error detail.
38 | */
39 | constructor (status, title, detail) {
40 | this.status = status
41 | this.title = title
42 | this.detail = detail
43 | this.message = detail !== undefined ? detail : title
44 | }
45 | }
46 |
47 |