diff --git a/.gitignore b/.gitignore index 049b3db..1c6b2ac 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ package-lock.json keys/index.js node_modules/ *.log +private-tests/ diff --git a/README.md b/README.md index 933f65a..ad90c1c 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ THe gateway temporarily stores the segments. When all segments related to a tran The gateway is provided with a default storage system using a transient memory cache. Additional types of storage may be implemented in the future (temporary storage in a database, etc). Note that a single storage can be used at once. -For pushing transactions on the Bitcoin network, the gateway currently supports pushes through the RPC API of a bitcoin node or through the pushTx service provided by an instance of the Samourai backend. Additional pushTx services may be implemented in the future. Multiple pushTx services can be activated. The Gateway randomly selects a service for each push. +For pushing transactions on the Bitcoin network, the gateway currently supports pushes through the RPC API of a bitcoin node or through the pushTx service provided by an instance of the Samourai backend (Dojo). Additional pushTx services may be implemented in the future. Multiple pushTx services can be activated. The Gateway randomly selects a service for each push. ## Known limitations diff --git a/keys/index-example.js b/keys/index-example.js index 693b409..03c4af6 100644 --- a/keys/index-example.js +++ b/keys/index-example.js @@ -103,8 +103,8 @@ module.exports = { * name:'samourai backend mainnet', * // Configuration of the wrapper * options: { - * // URL of the pushtx endpoint - * url: "https://api.samouraiwallet.com/v2/pushtx", + * // Base url of the Samourai backend + * url: "https://api.samouraiwallet.com/v2", * // API key requested by the backend * // or null if the backend doesn't require authentication * apiKey: null diff --git a/lib/pushtx-wrappers/samourai-backend/wrapper.js b/lib/pushtx-wrappers/samourai-backend/wrapper.js index 565357d..493daf1 100644 --- a/lib/pushtx-wrappers/samourai-backend/wrapper.js +++ b/lib/pushtx-wrappers/samourai-backend/wrapper.js @@ -20,6 +20,15 @@ class Wrapper extends AbstractPushTxWrapper { */ constructor(options, name) { super(options, name) + + this._jwtAccessToken = null + this._jwtRefreshToken = null + this.refreshTimeout = null + + // Authentication to backend + if (this.options.apiKey != null) { + this.getAuthTokens() + } } /** @@ -32,7 +41,7 @@ class Wrapper extends AbstractPushTxWrapper { try { const params = { - url: `${this.options.url}`, + url: `${this.options.url}/pushtx/`, method: 'POST', form: { tx: `${rawtx}` @@ -42,8 +51,15 @@ class Wrapper extends AbstractPushTxWrapper { }, timeout: 30000 } + + // Add access token to params + if (this._jwtAccessToken != null) { + params['form']['at'] = this._jwtAccessToken + } + const res = await rp(params) const result = JSON.parse(res) + if (result.status == 'ok') { // Returned data is "" Logger.info(`Successfully pushed ${result.data} over ${this.name}`) @@ -71,6 +87,114 @@ class Wrapper extends AbstractPushTxWrapper { } } + /** + * Authentication to the backend thanks to an API key + * @returns {boolean} returns true if authentication was successful, false otherwise + */ + async getAuthTokens() { + Logger.info(`Trying to authenticate to the backend`) + + try { + this.clearRefreshTimeout() + + const params = { + url: `${this.options.url}/auth/login?apikey=${this.options.apiKey}`, + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded' + }, + timeout: 30000 + } + const res = await rp(params) + const result = JSON.parse(res) + + this._jwtAccessToken = result['authorizations']['access_token'] + this._jwtRefreshToken = result['authorizations']['refresh_token'] + + // Schedule next refresh of the access token + await this.scheduleNextRefresh() + + Logger.info(`Successfully authenticated to the backend`) + return true + + } catch(err) { + try { + const error = JSON.parse(err.error) + Logger.error( + error.error.message, + `A problem (error code ${error.error.code}) was met while trying to authenticate to the backend` + ) + } catch(e) { + Logger.error( + null, + `A problem was met while trying to authenticate to the backend` + ) + } finally { + return false + } + } + } + + /** + * Refresh the JWT access token + * @returns {boolean} returns true if refresh was successful, false otherwise + */ + async refreshAuthToken() { + Logger.info(`Trying to refresh the access token`) + + try { + if (this._jwtRefreshToken == null) + return this.getAuthTokens() + + const params = { + url: `${this.options.url}/auth/refresh?rt=${this._jwtRefreshToken}`, + method: 'POST', + headers: { + 'content-type': 'application/x-www-form-urlencoded' + }, + timeout: 30000 + } + const res = await rp(params) + const result = JSON.parse(res) + + this._jwtAccessToken = result['authorizations']['access_token'] + + // Schedule next refresh of the access token + this.clearRefreshTimeout() + await this.scheduleNextRefresh() + + Logger.info(`Successfully refreshed the access token`) + return true + + } catch(err) { + Logger.error( + null, + `A problem was met while trying to refresh the access token` + ) + // Try a new authentication + return this.getAuthTokens() + } + } + + /** + * Clear refreshTimeout + */ + clearRefreshTimeout() { + if (this.refreshTimeout) { + clearTimeout(this.refreshTimeout) + this.refreshTimeout = null + } + } + + /** + * Schedule next refresh of the access token + */ + async scheduleNextRefresh() { + this.refreshTimeout = setTimeout(async function() { + await this.refreshAuthToken() + }.bind(this), 600000) + } + } module.exports = Wrapper