diff --git a/README.md b/README.md index 995aed8..581d241 100644 --- a/README.md +++ b/README.md @@ -188,49 +188,6 @@ const ƨ = require('sologenic-xrpl-stream-js'); )(); ``` -#### Initialization of the Sologenic XRPL stream with the xumm signer - -The xumm signing mechanism implements xumm signing within the sologenic-xrpl-stream-js library. The xumm signing is initialized by passing a `signingMechansim` parameter to the sologenic TX handler constructor. At the current point in time, the xumm integration requires user interaction between the xumm application and sologenic-xrpl-stream-js. When the xumm functionality is enabled, the sologenic-xrpl-stream-js prints out the `next_url` parameter that the user should follow within their web browser to sign the transaction. The signing of the transaction should happen before the `maximumExecutionTime` times out, otherwise the promise object will be rejected and the transaction will need to be retried with a new xumm transaction URL. - -All existing transaction submission functionality remains the same in this library, the only difference is the signing mechanism requires third-party confirmation. - -```typescript - -'use strict'; - -const ƨ = require('sologenic-xrpl-stream-js'); - - -(async () => { - - try { - const sologenic = await new ƨ.SologenicTxHandler( - // RippleAPI Options - { - server: 'wss://testnet.xrpl-labs.com', // Kudos to Wietse Wind - }, - // Sologenic Options, hash or redis (see SologenicOptions in documentation) - { - // Clear the cache before accessing the queue, since this is a hash-based - // queue it will be initialized empty, so this will have no effect. - clearCache: true, - queueType: "hash", - hash: {}, - signingMechanism: new XummSigner({ - // Xumm API key and secret, inherited by the XUMM_API_KEY and XUMM_API_SECRET environment variables - xummApiKey: process.env.XUMM_API_KEY, - xummApiSecret: process.env.XUMM_API_SECRET, - // The maximum execution time is the time in milliseconds that a TX - // will wait for the transaction - maximumExecutionTime: 10000 - - }) - }).connect(); - } -); - -``` - ### Intializing the Sologenic XRPL stream with a redis-based queue ```typescript @@ -312,11 +269,14 @@ async () => { // IMPORTANT: This method HAS to be called on User interaction, otherwise, the LedgerDevice communication library will throw an exception. establishConnection().then(async () => { // Set the Communication Object as the SigningMechanism. - await s.setSigningMechanism(new s.LedgerDeviceSigner()); - - // Optional. Get SigningMechanism and fetch for the Address and Public Key; - const signingMechanism = s.getSigningMechanism(); - const { address, publicKey } = signingMechanism.getWalletAddress(); + await sologenic.setSigningMechanism( + new s.LedgerDeviceSigner({ + ripple_server: RIPPLE_SERVER_WEBSOCKET // `i.e.: wss://s1.ripple.com/ + }) + ); + + // At the moment of connection, Ledger Devices return an array of accounts. This accounts are always going to be all the active accounts plus 1 inactive. + const { accounts } = await sologenic.connectSigner(); }); ``` @@ -370,20 +330,23 @@ const establishConnection = () => { ### Using SOLO Wallet as SigningMechanism -The SOLO Wallet signing mechanims implements the feature to sign transactions with a SOLO Wallet. (Available for Android and iOS). The SOLO Wallet signer is initialized by passing a `signingMechanism` parameter to the SologenicTxHandler after initialization. As with the other SigningMechanism on Sologenic XRPL Stream, the library will handle the signing process/requests. - -This SigningMechanism requires the next arguments to be passed on its constructor. +The SOLO Wallet signing mechanism implements the feature to sign transactions with a SOLO Wallet. (Available for Android and iOS). The SOLO Wallet signer is initialized by passing a `signingMechanism` parameter to the SologenicTxHandler after initialization. As with the other SigningMechanism on Sologenic XRPL Stream, the library will handle the signing process/requests. ```js new ƨ.SoloWalletSigner({ // This is the server that will handle the signing requests with the Wallet. - server: 'https://api.sologenic.org/api/v1/', + server: 'https://api.sologenic.org/api/v1/', // REQUIRED // HTML element where the QR Code to Sign in and establish connection with the wallet // will be prompt. - container_id: HTML_ELEMENT_ID, + container_id: HTML_ELEMENT_ID, // REQUIRED // HTML element where the QR Code to sign a transaction will be shown as fallback // in case the SOLO Wallet didn't receive the notification. - fallback_container_id: HTML_ELEMENT_ID + fallback_container_id: HTML_ELEMENT_ID, // REQUIRED + // Link to enable deeplinking to the SOLO WALLET when the webapp is being open in a mobile browser + deeplink_url: DEEPLINK_URL, // OPTIONAL + // Works together with deeplink_url. Both are needed in order for deeplinking to work, but not required + // for the signing to work + is_mobile: true || false // OPTIONAL }); ``` @@ -419,12 +382,12 @@ const establishConnection = () => { new ƨ.SoloWalletSigner({ server: 'https://api.sologenic.org/api/v1/', container_id: 'container_id', - fallback_container_id: 'fallback_container_id' + fallback_container_id: 'fallback_container_id', }) ); // In this moment, the Sologenic XRPL should prompt the SignIn QR Code on the element - // declared on the contructor. Then, user must scan this QR Code with their SOLO Wallet + // declared on the constructor. Then, user must scan this QR Code with their SOLO Wallet // and sign the "transaction". After the transaction is signed. SOLO Wallet will send back // the connection confirmation to the library as well as the address. Therefore, if the // address exists, connection can be considered successful. @@ -441,13 +404,16 @@ This SigningMechanism requires the next arguments to be passed on its constructo ```js new ƨ.XummWalletSigner({ // This is the server that will handle the signing requests with the Wallet. - server: 'https://api.sologenic.org/api/v1/', + server: 'https://api.sologenic.org/api/v1/', // REQUIRED // HTML element where the QR Code to Sign in and establish connection with the wallet // will be prompt. - container_id: HTML_ELEMENT_ID, + container_id: HTML_ELEMENT_ID, // REQUIRED // HTML element where the QR Code to sign a transaction will be shown as fallback // in case the XUMM Wallet didn't receive the notification. - fallback_container_id: HTML_ELEMENT_ID + fallback_container_id: HTML_ELEMENT_ID, // REQUIRED + // This property determines if the deeplinking feature will be enabled if the webapp is being accessed + // by a mobile browser. If you don't want to have this feature at all, just ignore this property. + is_mobile: true || false // OPTIONAL }); ``` @@ -532,7 +498,9 @@ const establishConnection = () => { // Set the desired signingMechanism. - // await sologenic.setSigngMechanism(new ƨ.LedgerDeviceSigner()); + // await sologenic.setSigngMechanism(new ƨ.LedgerDeviceSigner({ + // ripple_server: RIPPLE_SERVER // i.e.: wss://s1.ripple.com/ + // })); // await sologenic.setSigngMechanism(new ƨ.DcentSigner()); @@ -550,7 +518,6 @@ const establishConnection = () => { // Events have their own types now. - sologenic.on('queued', (event: SologenicTypes.QueuedEvent) => { console.log('GLOBAL QUEUED: ', event); }); @@ -577,8 +544,7 @@ const establishConnection = () => { // Get Signing Mechanism and fetch for Wallet Address. - const { address } = sologenic.getSigningMechanism().getWalletAddress(); - + const { address } = await sologenic.connectSigner(); await sologenic.setAccount({ address: address @@ -586,7 +552,7 @@ const establishConnection = () => { const tx = sologenic.submit({ TransactionType: 'Payment', - Account: 'rNbe8nh1K6nDC5XNsdAzHMtgYDXHZB486G', + Account: address, Destination: 'rUwty6Pf4gzUmCLVuKwrRWPYaUiUiku8Rg', Amount: { currency: '534F4C4F00000000000000000000000000000000', diff --git a/package.json b/package.json index 6f48c70..e9d0867 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "sologenic-xrpl-stream-js-nightly", - "version": "1.0.62", + "name": "sologenic-xrpl-stream-js", + "version": "1.1.0", "description": "Persistent transaction handling for the XRP Ledger", "main": "build/main/index.js", "typings": "build/main/index.d.ts", diff --git a/src/lib/signing/solo_signer.ts b/src/lib/signing/solo_signer.ts index 0161a10..142964a 100644 --- a/src/lib/signing/solo_signer.ts +++ b/src/lib/signing/solo_signer.ts @@ -2,7 +2,7 @@ import XrplAccount from '../account'; import * as SologenicTypes from '../../types'; import { SologenicTxSigner } from './index'; import { SologenicError } from '../error'; -import { httpRequest, wait } from '../utils'; +import { httpRequest, wait, getToken } from '../utils'; import { SoloWalletSignerSubmitPayload } from '../../types/api_signer'; import moment from 'moment'; @@ -12,6 +12,9 @@ export class SoloWalletSigner extends SologenicTxSigner { protected address: string = ''; protected signing_refs: any; protected fallback_container_id: string = ''; + protected is_mobile: boolean = false; + protected deeplink_url: string = ''; + signerID: string = 'solo_wallet'; constructor(options: any) { @@ -35,6 +38,14 @@ export class SoloWalletSigner extends SologenicTxSigner { throw new Error('Fallback container ID missing.'); } + if (options.hasOwnProperty('is_mobile')) { + this.is_mobile = options['is_mobile']; + } + + if (options.hasOwnProperty('deeplink_url')) { + this.deeplink_url = options['deeplink_url']; + } + this.includeSequence = true; } @@ -68,7 +79,17 @@ export class SoloWalletSigner extends SologenicTxSigner { let container: any = document.getElementById(this.container_id); container.appendChild(qrCode); - // console.log(connectionRefs); + + if (this.is_mobile && this.deeplink_url) { + let deepLink = document.createElement('a'); + + deepLink.setAttribute('href', connectionRefs.refs.deeplink); + deepLink.setAttribute('target', '_blank'); + deepLink.setAttribute('rel', 'noopener noreferrer'); + deepLink.innerText = 'SOLO Wallet >'; + + container.appendChild(deepLink); + } const socket: WebSocket = new WebSocket(connectionRefs.refs.ws); var isSocketOpen = false; @@ -94,8 +115,6 @@ export class SoloWalletSigner extends SologenicTxSigner { if (message.data !== 'pong') { const { data } = message; - console.log('SOLO Data', data); - if ( JSON.parse(data).hasOwnProperty('meta') && (JSON.parse(data).meta.hasOwnProperty('signed') || @@ -149,13 +168,25 @@ export class SoloWalletSigner extends SologenicTxSigner { {}, '' ); + + if (sessionStorage.mode && sessionStorage.mode === '_testnet') { + localStorage.swToken_testnet = JSON.stringify({ + push_token: meta.push_token, + signer: signed_tx.signer + }); + } else { + localStorage.swToken = JSON.stringify({ + push_token: meta.push_token, + signer: signed_tx.signer + }); + } } } - this.address = signed_tx.signer; + this.address = signed_tx.signer ? signed_tx.signer : ''; return { - address: signed_tx.signer + address: signed_tx.signer ? signed_tx.signer : null }; } @@ -173,6 +204,8 @@ export class SoloWalletSigner extends SologenicTxSigner { if (txJson.LastLedgerSequence) txJson.LastLedgerSequence = Number(txJson.LastLedgerSequence) + 100; + var pushToken = getToken(txJson.Account, 'solo'); + const tx_init = await httpRequest( this.server_url + 'issuer/transactions', 'post', @@ -180,7 +213,7 @@ export class SoloWalletSigner extends SologenicTxSigner { JSON.stringify({ tx_json: txJson, options: { - signer: txJson.Account, + ...(pushToken ? { push_token: pushToken } : {}), expires_at: moment() .add(10, 'm') .toISOString() @@ -191,7 +224,7 @@ export class SoloWalletSigner extends SologenicTxSigner { var signed_tx: any; if (tx_init.refs) { - this.signing_refs = tx_init.refs; + this.signing_refs = tx_init; const socket: WebSocket = new WebSocket(tx_init.refs.ws); var isSocketOpen = false; @@ -280,6 +313,18 @@ export class SoloWalletSigner extends SologenicTxSigner { {}, '' ); + + if (sessionStorage.mode && sessionStorage.mode === '_testnet') { + localStorage.swToken_testnet = JSON.stringify({ + push_token: meta.push_token, + signer: signed_tx.signer + }); + } else { + localStorage.swToken = JSON.stringify({ + push_token: meta.push_token, + signer: signed_tx.signer + }); + } } } @@ -300,10 +345,22 @@ export class SoloWalletSigner extends SologenicTxSigner { showSigningQRcode() { let qrCode = document.createElement('img'); - qrCode.setAttribute('src', this.signing_refs.qr); + + qrCode.setAttribute('src', this.signing_refs.refs.qr); qrCode.setAttribute('alt', 'QR Code'); let container: any = document.getElementById(this.fallback_container_id); container.appendChild(qrCode); + + if (this.is_mobile && this.deeplink_url) { + let deepLink = document.createElement('a'); + + deepLink.setAttribute('href', this.signing_refs.refs.deeplink); + deepLink.setAttribute('target', '_blank'); + deepLink.setAttribute('rel', 'noopener noreferrer'); + deepLink.innerText = 'SOLO Wallet >'; + + container.appendChild(deepLink); + } } } diff --git a/src/lib/signing/xumm_signer.ts b/src/lib/signing/xumm_signer.ts index 1a388c8..ab663d0 100644 --- a/src/lib/signing/xumm_signer.ts +++ b/src/lib/signing/xumm_signer.ts @@ -2,7 +2,7 @@ import XrplAccount from '../account'; import * as SologenicTypes from '../../types'; import { SologenicTxSigner } from './index'; import { SologenicError } from '../error'; -import { httpRequest, wait } from '../utils'; +import { getToken, httpRequest, wait } from '../utils'; import { XummWalletSignerSubmitPayload } from '../../types/api_signer'; import moment from 'moment'; @@ -141,51 +141,16 @@ export class XummWalletSigner extends SologenicTxSigner { } if (socketResponse.updated) { - let waitTime = 100; let data: any = socketResponse.data; if (data.hasOwnProperty('signed') && !data.signed) { throw new SologenicError('2004'); } - async function checkForSigned(url: string) { - while (true) { - try { - const signedTx = await httpRequest( - url + 'xumm/payload/' + connectionRefs.meta.identifier, - 'get', - {}, - '' - ); - - if (signedTx.hasOwnProperty('signer')) { - signed_tx = signedTx; - break; - } - - if ( - signedTx.hasOwnProperty('meta') && - !signedTx.meta.signed && - waitTime < 1000 - ) { - waitTime *= 2; - throw new Error('try again'); - } else { - throw new Error('throw unspecified error'); - } - } catch (e) { - if (e === 'try again') { - await wait(waitTime); - } - - if (e === 'throw error') { - throw new SologenicError('1001'); - } - } - } - } - - await checkForSigned(this.server_url); + signed_tx = await this.checkForSigned( + this.server_url, + connectionRefs.meta.identifier + ); } } @@ -210,6 +175,8 @@ export class XummWalletSigner extends SologenicTxSigner { if (txJson.LastLedgerSequence) txJson.LastLedgerSequence = Number(txJson.LastLedgerSequence) + 100; + var pushToken = getToken(txJson.Account, 'xumm'); + const tx_init = await httpRequest( this.server_url + 'xumm/payload', 'post', @@ -218,7 +185,7 @@ export class XummWalletSigner extends SologenicTxSigner { tx_json: txJson, options: { submit: false, - signer: txJson.Account, + ...(pushToken ? { push_token: pushToken } : {}), expires_at: moment() .add(10, 'm') .toISOString() @@ -302,53 +269,15 @@ export class XummWalletSigner extends SologenicTxSigner { if (socketResponse.updated) { let data: any = socketResponse.data; - let waitTime = 100; if (data.hasOwnProperty('signed') && !data.signed) { throw new SologenicError('2003'); } - async function checkForSigned(url: string, id: string) { - while (true) { - try { - const signedTx = await httpRequest< - XummWalletSignerSubmitPayload - >(url + 'xumm/payload/' + id, 'get', {}, ''); - - if (signedTx.hasOwnProperty('signer')) { - signed_tx = signedTx; - break; - } - - if ( - signedTx.hasOwnProperty('meta') && - !signedTx.meta.signed && - waitTime < 1000 - ) { - waitTime *= 2; - throw new Error('try again'); - } else { - throw new Error('throw unspecified error'); - } - } catch (e) { - if (e === 'throw error') { - throw new SologenicError('1001'); - } - - if ( - e.message === 'Error: Request failed with status code 500' - ) { - throw new SologenicError('1003'); - } - - if (e === 'try again') { - await wait(waitTime); - } - } - } - } - - await checkForSigned(this.server_url, data.payload_uuidv4); + signed_tx = await this.checkForSigned( + this.server_url, + data.payload_uuidv4 + ); } } @@ -372,7 +301,6 @@ export class XummWalletSigner extends SologenicTxSigner { } showSigningQRcode() { - console.log('clike', this.is_mobile); let qrCode = document.createElement('img'); qrCode.setAttribute('src', this.signing_refs.qr); qrCode.setAttribute('alt', 'QR Code'); @@ -390,4 +318,58 @@ export class XummWalletSigner extends SologenicTxSigner { container.appendChild(deepLink); } } + + async checkForSigned(url: string, id: string) { + let waitTime = 100; + + while (true) { + try { + const signedTx = await httpRequest( + url + 'xumm/payload/' + id, + 'get', + {}, + '' + ); + + if (signedTx.hasOwnProperty('signer')) { + if (sessionStorage.mode && sessionStorage.mode === '_testnet') { + localStorage.xummToken_testnet = JSON.stringify({ + push_token: signedTx.meta.push_token, + signer: signedTx.signer + }); + } else { + localStorage.xummToken = JSON.stringify({ + push_token: signedTx.meta.push_token, + signer: signedTx.signer + }); + } + + return signedTx; + } + + if ( + signedTx.hasOwnProperty('meta') && + !signedTx.meta.signed && + waitTime < 1000 + ) { + waitTime *= 2; + throw new Error('try again'); + } else { + throw new Error('throw unspecified error'); + } + } catch (e) { + if (e === 'throw error') { + throw new SologenicError('1001'); + } + + if (e.message === 'Error: Request failed with status code 500') { + throw new SologenicError('1003'); + } + + if (e === 'try again') { + await wait(waitTime); + } + } + } + } } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index b71da19..cf2a993 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -74,3 +74,24 @@ export const promiseTimeout = function(milliseconds: number, promise: any) { // Returns a race between our timeout and the passed in promise return Promise.race([promise, timeout]); }; + +/** Retrieve return push token if its the correct one */ +export const getToken = (signerAddress: string, wallet: string) => { + const sessionNet = sessionStorage.mode ? sessionStorage.mode : '_mainnet'; + const tokenStorage = + wallet === 'solo' + ? sessionNet === '_mainnet' + ? localStorage.swToken + : localStorage.swToken_testnet + : sessionNet === '_mainnet' + ? localStorage.xummToken + : localStorage.xummToken_testnet; + + if (!tokenStorage) return null; + + const lsSWToken = JSON.parse(tokenStorage); + + if (signerAddress === lsSWToken.signer) return lsSWToken.push_token; + + return null; +}; diff --git a/src/types/api_signer.ts b/src/types/api_signer.ts index 193f906..53db180 100644 --- a/src/types/api_signer.ts +++ b/src/types/api_signer.ts @@ -35,6 +35,7 @@ export interface XummWalletSignerSubmitPayload { signed: boolean; cancelled: boolean; expired: boolean; + push_token: string; }; refs?: { qr: string;