├── .prettierignore ├── assets └── img │ └── sologenic-xrpl-stream-js.png ├── src ├── types │ ├── index.ts │ ├── ledger_device.ts │ ├── account.ts │ ├── utils.ts │ ├── xrpl.ts │ ├── api_signer.ts │ ├── queues.ts │ ├── xumm.ts │ └── txhandler.ts ├── index.ts ├── lib │ ├── signing │ │ ├── index.ts │ │ ├── sologenic_tx_signer.ts │ │ ├── offline.ts │ │ ├── dcent_signer.ts │ │ ├── xumm.ts │ │ ├── ledger_device.ts │ │ ├── xumm_signer.ts │ │ └── solo_signer.ts │ ├── error.ts │ ├── utils.ts │ ├── queues │ │ ├── hash.ts │ │ ├── redis.ts │ │ └── index.ts │ └── account.ts └── tests │ ├── signing │ ├── offline.spec.ts │ └── xumm.spec.ts │ ├── error.spec.ts │ ├── account.spec.ts │ ├── compatibility.spec.ts │ ├── queues │ ├── hash.spec.ts │ └── redis.spec.ts │ └── txhandler │ ├── redis.spec.ts │ └── hash.spec.ts ├── .gitignore ├── .npmignore ├── docker-compose.yml ├── tsconfig.module.json ├── .travis.yml ├── .github ├── CONTRIBUTING.md ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE.md ├── .editorconfig ├── tslint.json ├── LICENSE ├── tsconfig.json ├── package.json ├── CHANGELOG.md └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | # package.json is formatted by package managers, so we ignore it here 2 | package.json -------------------------------------------------------------------------------- /assets/img/sologenic-xrpl-stream-js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sologenic/sologenic-xrpl-stream-js/HEAD/assets/img/sologenic-xrpl-stream-js.png -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account'; 2 | export * from './queues'; 3 | export * from './txhandler'; 4 | export * from './utils'; 5 | export * from './xrpl'; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | test 4 | src/**.js 5 | docs/* 6 | .idea/* 7 | .DS_Store 8 | 9 | coverage 10 | .nyc_output 11 | *.log 12 | 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /src/types/ledger_device.ts: -------------------------------------------------------------------------------- 1 | export interface LedgerDeviceTransport { 2 | getAddress: any; 3 | getAppConfiguration: any; 4 | signTransaction: any; 5 | transport?: any; 6 | } 7 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/queues'; 2 | export * from './lib/signing'; 3 | export * from './lib/account'; 4 | export * from './lib/txhandler'; 5 | export * from './lib/utils'; 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | test 3 | tsconfig.json 4 | tsconfig.module.json 5 | tslint.json 6 | .travis.yml 7 | .github 8 | .prettierignore 9 | .vscode 10 | build/docs 11 | **/*.spec.* 12 | coverage 13 | .nyc_output 14 | *.log 15 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # this docker will run a local redis instance 2 | version: '3.5' 3 | 4 | services: 5 | redis: 6 | image: redis:5 7 | restart: unless-stopped 8 | ports: 9 | - 127.0.0.1:6379:6379 10 | -------------------------------------------------------------------------------- /src/types/account.ts: -------------------------------------------------------------------------------- 1 | export interface KeyPair { 2 | publicKey: string; 3 | privateKey: string; 4 | } 5 | 6 | export interface Account { 7 | address: string; 8 | secret?: string; 9 | keypair?: KeyPair; 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.module.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "outDir": "build/module", 6 | "module": "esnext" 7 | }, 8 | "exclude": [ 9 | "node_modules/**" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '10' 5 | - '12' 6 | # keep the npm cache to speed up installs 7 | cache: 8 | directories: 9 | - '$HOME/.npm' 10 | after_success: 11 | - npm run cov:send 12 | - npm run cov:check 13 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Example Contributing Guidelines 2 | 3 | This is an example of GitHub's contributing guidelines file. Check out GitHub's [CONTRIBUTING.md help center article](https://help.github.com/articles/setting-guidelines-for-repository-contributors/) for more information. 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | max_line_length = 80 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | max_line_length = 0 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * **What kind of change does this PR introduce?** (Bug fix, feature, docs update, ...) 2 | 3 | 4 | 5 | * **What is the current behavior?** (You can also link to an open issue here) 6 | 7 | 8 | 9 | * **What is the new behavior (if this is a feature change)?** 10 | 11 | 12 | 13 | * **Other information**: 14 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:latest", "tslint-config-prettier"], 3 | "rules": { 4 | "no-implicit-dependencies": [true, "dev"], 5 | "object-literal-sort-keys": false, 6 | "jsdoc-format": false, 7 | "ordered-imports": false, 8 | "no-submodule-imports": false, 9 | "array-type": [false, "array"], 10 | "no-bitwise": false 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * **I'm submitting a ...** 2 | [ ] bug report 3 | [ ] feature request 4 | [ ] question about the decisions made in the repository 5 | [ ] question about how to use this project 6 | 7 | * **Summary** 8 | 9 | 10 | 11 | * **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. StackOverflow, personal fork, etc.) 12 | -------------------------------------------------------------------------------- /src/types/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Supporting interface for our faucet account so that we can 3 | * cast (deserialize) the JSON response to an object in our tests. 4 | */ 5 | export interface IFaucetAccount { 6 | xAddress: string; 7 | classicAddress: string; 8 | address: string; 9 | secret: string; 10 | } 11 | 12 | /** 13 | * Supporting interface for our faucet account so that we can 14 | * cast (deserialize) the JSON response to an object in our tests. 15 | */ 16 | export interface IFaucet { 17 | account: IFaucetAccount; 18 | balance: number; 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/signing/index.ts: -------------------------------------------------------------------------------- 1 | import SologenicTxSigner from './sologenic_tx_signer'; 2 | import { OfflineSigner } from './offline'; 3 | import { XummSigner } from './xumm'; 4 | import { LedgerDeviceSigner } from './ledger_device'; 5 | import { SoloWalletSigner } from './solo_signer'; 6 | import { DcentSigner } from './dcent_signer'; 7 | import { XummWalletSigner } from './xumm_signer'; 8 | 9 | export { 10 | SologenicTxSigner, 11 | OfflineSigner, 12 | XummSigner, 13 | LedgerDeviceSigner, 14 | SoloWalletSigner, 15 | XummWalletSigner, 16 | DcentSigner 17 | }; 18 | -------------------------------------------------------------------------------- /src/types/xrpl.ts: -------------------------------------------------------------------------------- 1 | export interface RippleAPIOptions { 2 | trace?: boolean; 3 | proxy?: string; 4 | proxyAuthorization?: string; 5 | authorization?: string; 6 | trustedCertificates?: string[]; 7 | key?: string; 8 | passphrase?: string; 9 | certificate?: string; 10 | timeout?: number; 11 | server?: string; 12 | feeCushion?: number; 13 | maxFeeXRP?: string; 14 | } 15 | 16 | export interface Ledger { 17 | baseFeeXRP: string; 18 | ledgerHash?: string; 19 | ledgerVersion: number; 20 | ledgerTimestamp: string; 21 | reserveBaseXRP?: string; 22 | reserveIncrementXRP?: string; 23 | transactionCount?: number; 24 | validatedLedgerVersions?: string; 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Sologenic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/tests/signing/offline.spec.ts: -------------------------------------------------------------------------------- 1 | import anyTest, {TestInterface} from 'ava'; 2 | 3 | import { OfflineSigner } from '../../lib/signing/offline'; 4 | import XrplAccount from '../../lib/account'; 5 | import { SignedTx } from '../../types'; 6 | 7 | const binaryCodec = require('ripple-binary-codec'); 8 | 9 | const test = anyTest as TestInterface<{ 10 | session: any, 11 | data: any 12 | }>; 13 | 14 | test.serial("sign a transaction and verify the signature", async t => { 15 | const account = new XrplAccount( 16 | "rE3uRzyUuKCFGuuUSKPPz7MeLcbckeuczu", 17 | "snevTM71p4xj5ZQcnBvpHeb9P3Umn"); 18 | 19 | const txJson = { 20 | Account: account.getAddress(), 21 | TransactionType: 'AccountSet', 22 | SetFlag: 1 23 | }; 24 | 25 | const signer = new OfflineSigner({}); 26 | 27 | // Encode transaction and sign 28 | const signedTx: SignedTx = await signer.sign(txJson, "1234", account, {}); 29 | 30 | // Decode the transaction 31 | const decodedTransaction = binaryCodec.decode(signedTx.signedTransaction); 32 | 33 | t.is(decodedTransaction.TransactionType, txJson.TransactionType); 34 | t.is(decodedTransaction.SetFlag, txJson.SetFlag); 35 | }); 36 | -------------------------------------------------------------------------------- /src/types/api_signer.ts: -------------------------------------------------------------------------------- 1 | import * as TXTypes from './txhandler'; 2 | 3 | export interface SoloWalletSignerSubmitPayload { 4 | tx_json: TXTypes.TX; 5 | meta: { 6 | identifier: string; 7 | expires_at: string; 8 | submit: boolean; 9 | pushed: boolean; 10 | opened: boolean; 11 | resolved: boolean; 12 | signed: boolean; 13 | cancelled: boolean; 14 | expired: boolean; 15 | }; 16 | refs?: { 17 | qr: string; 18 | ws: string; 19 | deeplink: string; 20 | }; 21 | signer?: string; 22 | txid?: string; 23 | tx_hex?: string; 24 | } 25 | 26 | export interface XummWalletSignerSubmitPayload { 27 | tx_json: TXTypes.TX; 28 | meta: { 29 | identifier: string; 30 | expires_at: string; 31 | submit: boolean; 32 | pushed: boolean; 33 | opened: boolean; 34 | resolved: boolean; 35 | signed: boolean; 36 | cancelled: boolean; 37 | expired: boolean; 38 | push_token: string; 39 | }; 40 | refs?: { 41 | qr: string; 42 | ws: string; 43 | deeplink: string; 44 | }; 45 | signer?: string; 46 | txid?: string; 47 | tx_hex?: string; 48 | } 49 | 50 | export interface SocketResponse { 51 | meta: object; 52 | signed: boolean; 53 | opened?: boolean; 54 | } 55 | -------------------------------------------------------------------------------- /src/types/queues.ts: -------------------------------------------------------------------------------- 1 | import { ISologenicTxSigner } from "./txhandler"; 2 | 3 | export interface IQueue { 4 | add(queue: string, data: MQTX, id?: string): Promise; 5 | get(queue: string, id: string): Promise; 6 | getAll(queue?: string): Promise; 7 | pop(queue: string): Promise; 8 | del(queue: string, id: string): Promise; 9 | delAll(queue: string): Promise; 10 | appendEvent(queue: string, id: string, event_name: string): Promise; 11 | queues(): Promise; 12 | deleteQueue(queue: string): Promise; 13 | } 14 | 15 | export interface HashTransactionHandlerOptions {} 16 | 17 | export interface RedisTransactionHandlerOptions { 18 | port?: number; 19 | host?: string; 20 | family?: number; 21 | password?: string; 22 | db?: number; 23 | } 24 | 25 | export interface TransactionHandlerOptions { 26 | queueType?: string; 27 | clearCache?: boolean; 28 | redis?: RedisTransactionHandlerOptions 29 | hash?: HashTransactionHandlerOptions 30 | maximumTimeToLive?: number; 31 | signingMechanism?: ISologenicTxSigner; 32 | } 33 | 34 | export interface MQTX { 35 | id: string; 36 | data: any; 37 | created: number; 38 | } 39 | 40 | export declare const QUEUE_TYPE_STXMQ_REDIS = 'redis'; 41 | export declare const QUEUE_TYPE_STXMQ_HASH = 'hash'; 42 | -------------------------------------------------------------------------------- /src/tests/error.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { SologenicError } from '../lib/error'; 3 | 4 | test.serial('check for duplicate error ids and messages', async function(t) { 5 | let errorCodes = SologenicError.getErrorCodes(); 6 | 7 | for (var i in errorCodes) { 8 | let errorId = SologenicError.getErrorCodeByMessage(errorCodes[i].message); 9 | let message = SologenicError.getErrorCodeById(errorCodes[i].id); 10 | 11 | t.true(typeof(errorId) == 'string'); 12 | t.true(typeof(message) == 'string'); 13 | 14 | await new Promise((resolve) => { 15 | let count = 0; 16 | 17 | for (var index in errorCodes) { 18 | if (errorCodes[index].id == errorId) { 19 | count++; 20 | } 21 | } 22 | 23 | resolve(count); 24 | }).then(function(value) { 25 | t.is(value, 1); 26 | }); 27 | 28 | await new Promise((resolve) => { 29 | let count = 0; 30 | 31 | for (var index in errorCodes) { 32 | if (errorCodes[index].message == message) { 33 | count++; 34 | } 35 | } 36 | 37 | resolve(count); 38 | }).then(function(value) { 39 | t.is(value, 1); 40 | }); 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /src/lib/signing/sologenic_tx_signer.ts: -------------------------------------------------------------------------------- 1 | import { RippleAPI } from 'ripple-lib'; 2 | 3 | import XrplAccount from '../account'; 4 | 5 | import * as SologenicTypes from '../../types/'; 6 | 7 | export default abstract class SologenicTxSigner 8 | implements SologenicTypes.ISologenicTxSigner { 9 | protected rippleApi!: RippleAPI; 10 | protected includeSequence: boolean = false; 11 | signerID: string = 'default'; 12 | cancelled: boolean = false; 13 | 14 | constructor(options: any) { 15 | if (options && options.hasOwnProperty('rippleApi')) { 16 | this.rippleApi = options.rippleApi; 17 | } else { 18 | this.rippleApi = new RippleAPI({}); 19 | } 20 | } 21 | 22 | /** 23 | * Should we include a sequence number (for xumm, let xumm decide) 24 | */ 25 | public getIncludeSequence(): boolean { 26 | return this.includeSequence; 27 | } 28 | 29 | public cancelSigning(cancel: boolean) { 30 | return (this.cancelled = cancel); 31 | } 32 | 33 | public async sign( 34 | txJson: SologenicTypes.TX, 35 | txId: string, 36 | account?: XrplAccount, 37 | signingOptions?: any 38 | ): Promise { 39 | txJson; 40 | txId; 41 | account; 42 | signingOptions; 43 | 44 | throw new Error('Method not implemented.'); 45 | } 46 | 47 | public async requestConnection(): Promise { 48 | throw new Error('Method not implemented.'); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/types/xumm.ts: -------------------------------------------------------------------------------- 1 | export interface IXummSubmitAdditional { 2 | issued_user_token: string; 3 | }; 4 | 5 | export interface IXummSubmitPayload { 6 | uuid: string, 7 | next: { 8 | always: string; 9 | no_push_msg_received: string; 10 | }, 11 | refs: { 12 | qr_png: string, 13 | qr_matrix: string, 14 | qr_uri_quality_opts: [] 15 | }, 16 | websocket_status: string, 17 | pushed: boolean 18 | }; 19 | 20 | export interface IXummQueryPayload { 21 | meta: { 22 | exists: boolean, 23 | uuid: any, 24 | multisign: boolean, 25 | submit: boolean, 26 | destination: any, 27 | resolved_destination: any, 28 | resolved: boolean, 29 | signed: boolean, 30 | cancelled: boolean, 31 | expired: boolean, 32 | pushed: boolean, 33 | app_opened: any, 34 | return_url_app: any 35 | return_url_web?: any 36 | }, 37 | 38 | custom_meta?: { 39 | identifier: any, 40 | blob: any, 41 | instruction: any 42 | }, 43 | 44 | application: { 45 | name: any, 46 | description: any, 47 | disabled: any, 48 | uuidv4: any, 49 | icon_url: any, 50 | issued_user_token: any 51 | } 52 | 53 | payload: { 54 | tx_type: any, 55 | tx_destination: any, 56 | tx_destination_tag: any 57 | request_json?: any 58 | created_at: any, 59 | expires_at: any, 60 | expires_in_seconds: any 61 | }, 62 | 63 | response: { 64 | hex: any, 65 | txid: any, 66 | resolved_at: any, 67 | dispatched_to: any, 68 | dispatched_result: any, 69 | multisign_account: any, 70 | account: any 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/lib/signing/offline.ts: -------------------------------------------------------------------------------- 1 | import { RippleAPI } from 'ripple-lib'; 2 | import { SologenicError } from '../error'; 3 | import * as SologenicTypes from '../../types'; 4 | 5 | import XrplAccount from '../account'; 6 | import { SologenicTxSigner } from './index'; 7 | 8 | export class OfflineSigner extends SologenicTxSigner 9 | implements SologenicTypes.ISologenicTxSigner { 10 | protected rippleApi!: RippleAPI; 11 | signerID: string = 'offline'; 12 | 13 | constructor(options: any) { 14 | super(options); 15 | 16 | this.includeSequence = true; 17 | } 18 | 19 | requestConnection(): any { 20 | return true; 21 | } 22 | 23 | async sign( 24 | txJson: SologenicTypes.TX, 25 | txId: string, 26 | account: XrplAccount, 27 | signingOptions?: any 28 | ): Promise { 29 | try { 30 | // Sign the transaction using the secret provided on init 31 | // console.log(`Signing transaction txJson=${txJson}, secret=${account.secret}, keypair=${account.keypair}`) 32 | 33 | // Delete the transaction metadata if it exists since the signing will fail 34 | // as this TransactionMetadata is not known to the schema. 35 | if (txJson.TransactionMetadata) { 36 | delete txJson.TransactionMetadata; 37 | } 38 | 39 | const signedTx: SologenicTypes.SignedTx = this.rippleApi.sign( 40 | JSON.stringify(txJson), 41 | account.getSecret(), 42 | signingOptions, 43 | account.getKeypair() 44 | ); 45 | 46 | signedTx.id = txId; 47 | 48 | return signedTx; 49 | } catch (error) { 50 | // Re-throw the error (we catch it just for debugging purposes) 51 | throw error; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/lib/error.ts: -------------------------------------------------------------------------------- 1 | export class SologenicError extends Error { 2 | /** 3 | * @param status Status code for error 4 | * @param rippleError Inner error 5 | */ 6 | 7 | constructor(public status: string, public rippleError?: Error) { 8 | super(); 9 | this.status = status; 10 | this.message = this._getError(status); 11 | Object.setPrototypeOf(this, SologenicError.prototype); 12 | } 13 | 14 | /** 15 | * @returns An array of error codes 16 | */ 17 | 18 | public static getErrorCodes(): Array { 19 | return [ 20 | { id: '1000', message: 'unspecified_error' }, 21 | { id: '1001', message: 'sologenic_constructor_error' }, 22 | { id: '1002', message: 'redis_initialization_failed' }, 23 | { id: '1003', message: 'connection_error' }, 24 | { id: '1004', message: 'unspecified_ripple_error' }, 25 | { id: '1005', message: 'ripple_ws_connection_error' }, 26 | { id: '1006', message: 'unable_to_validate_missed_transactions' }, 27 | { id: '2000', message: 'invalid_xrp_address' }, 28 | { id: '2001', message: 'invalid_xrp_secret' }, 29 | { id: '2002', message: 'unable_to_sign_transaction' }, 30 | { id: '2003', message: 'transaction_signing_rejected' }, 31 | { id: '2004', message: 'sign_in_rejected' }, 32 | { id: '2005', message: 'transaction_cancelled' } 33 | ]; 34 | } 35 | 36 | /** 37 | * @param errorId An error 'id' 38 | * @returns An error 'message' or undefined. 39 | */ 40 | 41 | public static getErrorCodeById(errorId: string): string { 42 | return SologenicError.getErrorCodes().find(e => e.id === errorId)!.message; 43 | } 44 | 45 | /** 46 | * @param message An error 'message' 47 | * @returns An error 'id' or undefined. 48 | */ 49 | 50 | public static getErrorCodeByMessage(message: string): string { 51 | return SologenicError.getErrorCodes().find(e => e.message === message)!.id; 52 | } 53 | 54 | /** 55 | * @param errorId An error 'id' 56 | * @returns An error 'message' or undefined. 57 | */ 58 | 59 | public _getError(errorId: string): string { 60 | return SologenicError.getErrorCodeById(errorId); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "outDir": "build/main", 5 | "rootDir": "src", 6 | "moduleResolution": "node", 7 | "module": "commonjs", 8 | "declaration": true, 9 | "inlineSourceMap": true, 10 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 11 | 12 | "strict": true /* Enable all strict type-checking options. */, 13 | 14 | /* Strict Type-Checking Options */ 15 | // "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 16 | // "strictNullChecks": true /* Enable strict null checks. */, 17 | // "strictFunctionTypes": true /* Enable strict checking of function types. */, 18 | // "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, 19 | // "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 20 | // "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 21 | 22 | /* Additional Checks */ 23 | "noUnusedLocals": false /* Report errors on unused locals. */, 24 | "noUnusedParameters": true /* Report errors on unused parameters. */, 25 | "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, 26 | "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, 27 | 28 | /* Debugging Options */ 29 | "traceResolution": false /* Report module resolution log messages. */, 30 | "listEmittedFiles": false /* Print names of generated files part of the compilation. */, 31 | "listFiles": false /* Print names of files part of the compilation. */, 32 | "pretty": true /* Stylize errors and messages using color and context. */, 33 | 34 | /* Experimental Options */ 35 | // "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, 36 | // "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */, 37 | 38 | "lib": ["es2017", "dom"], 39 | "types": ["node"], 40 | "typeRoots": ["node_modules/@types", "src/types"] 41 | }, 42 | "include": ["src/**/*.ts"], 43 | "exclude": ["node_modules/**"], 44 | "compileOnSave": false 45 | } 46 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import axios, { Method } from 'axios'; 2 | import fetch, { RequestInfo } from 'node-fetch'; 3 | 4 | /** 5 | * Perform a asynchronous request and cast the result back to a 6 | * type. In our case, we're using this to cast to [[IFaucet]]. 7 | */ 8 | export async function http( 9 | url: RequestInfo, 10 | method: string = 'POST', 11 | headers?: object, 12 | body?: string 13 | ): Promise { 14 | const response = await fetch(url, { 15 | method: method, 16 | body: body, 17 | headers: { 18 | 'Content-Type': 'application/json', 19 | ...headers 20 | } 21 | }); 22 | 23 | return await response.json(); 24 | } 25 | 26 | export async function httpRequest( 27 | url: string, 28 | method: Method, 29 | headers?: object, 30 | body?: string 31 | ): Promise { 32 | try { 33 | const response = await axios({ 34 | url: url, 35 | method: method, 36 | headers: { 37 | 'Content-type': 'application/json', 38 | ...headers 39 | }, 40 | data: body 41 | }); 42 | 43 | return response.data; 44 | } catch (e) { 45 | throw new Error(e); 46 | } 47 | } 48 | 49 | /** 50 | * Pause execution for X milliseconds 51 | * 52 | * @param milliseconds Number of milliseconds to wait before resolving the promise. 53 | * @returns {Promise} 54 | */ 55 | export const wait = (milliseconds: number) => { 56 | return new Promise(resolve => setTimeout(resolve, milliseconds)); 57 | }; 58 | 59 | /** 60 | * https://italonascimento.github.io/applying-a-timeout-to-your-promises/ 61 | * @param milliseconds 62 | * @param promise 63 | */ 64 | 65 | export const promiseTimeout = function(milliseconds: number, promise: any) { 66 | // Create a promise that rejects in milliseconds 67 | let timeout = new Promise((_, reject) => { 68 | let id = setTimeout(() => { 69 | clearTimeout(id); 70 | reject('Timed out in ' + milliseconds + 'ms.'); 71 | }, milliseconds); 72 | }); 73 | 74 | // Returns a race between our timeout and the passed in promise 75 | return Promise.race([promise, timeout]); 76 | }; 77 | 78 | /** Retrieve return push token if its the correct one */ 79 | export const getToken = (signerAddress: string, wallet: string) => { 80 | const sessionNet = sessionStorage.mode ? sessionStorage.mode : '_mainnet'; 81 | const tokenStorage = 82 | wallet === 'solo' 83 | ? sessionNet === '_mainnet' 84 | ? localStorage.swToken 85 | : localStorage.swToken_testnet 86 | : sessionNet === '_mainnet' 87 | ? localStorage.xummToken 88 | : localStorage.xummToken_testnet; 89 | 90 | if (!tokenStorage) return null; 91 | 92 | const lsSWToken = JSON.parse(tokenStorage); 93 | 94 | if (signerAddress === lsSWToken.signer) return lsSWToken.push_token; 95 | 96 | return null; 97 | }; 98 | -------------------------------------------------------------------------------- /src/lib/signing/dcent_signer.ts: -------------------------------------------------------------------------------- 1 | import XrplAccount from '../account'; 2 | import * as SologenicTypes from '../../types'; 3 | import { SologenicTxSigner } from './index'; 4 | import { SologenicError } from '../error'; 5 | const binaryCodec = require('ripple-binary-codec'); 6 | const DcentWebConnector = require('dcent-web-connector'); 7 | 8 | export class DcentSigner extends SologenicTxSigner { 9 | signerID: string = 'dcent'; 10 | protected bip32Path: string = "m/44'/144'/0'/0/0"; 11 | protected address: string = ''; 12 | 13 | constructor(options: any) { 14 | super(options); 15 | this.includeSequence = true; 16 | } 17 | 18 | async requestConnection(): Promise { 19 | try { 20 | // Request connection to Ledger Device (Speculos uses http, the actual device use webusb) 21 | const dcentInfo = await DcentWebConnector.getAccountInfo(); 22 | 23 | const ripple_account = dcentInfo.body.parameter.account.find( 24 | (acc: { coin_name: string }) => acc.coin_name === 'RIPPLE' 25 | ); 26 | 27 | if (typeof ripple_account === 'undefined') { 28 | throw new Error('Ripple Account Not Found.'); 29 | } 30 | 31 | const ripple_address = await DcentWebConnector.getAddress( 32 | 'ripple', 33 | ripple_account.address_path 34 | ); 35 | 36 | if (ripple_address.body.parameter.address) { 37 | DcentWebConnector.popupWindowClose(); 38 | } 39 | 40 | this.address = ripple_address.body.parameter.address; 41 | return { 42 | address: ripple_address.body.parameter.address 43 | }; 44 | } catch (e) { 45 | throw new Error(e.message); 46 | } 47 | } 48 | 49 | async sign( 50 | txJson: SologenicTypes.TX, 51 | txId: string, 52 | _account?: XrplAccount, 53 | _signingOptions: any = {} 54 | ): Promise { 55 | try { 56 | if (txJson.TransactionMetadata) delete txJson.TransactionMetadata; 57 | if (txJson.LastLedgerSequence) 58 | txJson.LastLedgerSequence = Number(txJson.LastLedgerSequence) + 100; 59 | 60 | const signedTX = await DcentWebConnector.getXrpSignedTransaction( 61 | txJson, 62 | this.bip32Path 63 | ); 64 | 65 | txJson.SigningPubKey = signedTX.body.parameter.pubkey.toUpperCase(); 66 | txJson.TxnSignature = signedTX.body.parameter.sign.toUpperCase(); 67 | 68 | // Return the signed transaction 69 | DcentWebConnector.popupWindowClose(); 70 | return { 71 | id: txId, 72 | signedTransaction: binaryCodec.encode(txJson) 73 | }; 74 | } catch (e) { 75 | // This error is thrown if the user rejects the transaction on the D'Cent 76 | if (e.body.error.code === 'user_cancel') { 77 | DcentWebConnector.popupWindowClose(); 78 | throw new SologenicError('2003'); 79 | } 80 | 81 | throw new SologenicError('1000'); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/tests/account.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import * as SologenicTypes from '../types'; 3 | import { SologenicTxHandler } from '../lib/txhandler'; 4 | import XrplAccount, { XrplAddressException, XrplSecretException, XrplKeypairException, XrplKeypairOrSecretMissingException } from '../lib/account'; 5 | import { generateSeed, deriveAddress, deriveKeypair } from 'ripple-keypairs'; 6 | 7 | test.serial('validate construction of account', async function(t) { 8 | const seed = generateSeed(); 9 | const keypair = deriveKeypair(seed); 10 | const address = deriveAddress(keypair.publicKey); 11 | 12 | t.throws(() => { 13 | return XrplAccount.getAccount("", undefined, undefined, undefined) 14 | }, { 15 | instanceOf: XrplAddressException 16 | }); 17 | 18 | t.throws(() => { 19 | return XrplAccount.getAccount(address, "", undefined, undefined) 20 | }, { 21 | instanceOf: XrplSecretException 22 | }); 23 | 24 | t.throws(() => { 25 | return XrplAccount.getAccount(address, seed, keypair.publicKey, undefined) 26 | }, { 27 | instanceOf: XrplKeypairException 28 | }); 29 | 30 | t.notThrows(() => { 31 | return XrplAccount.getAccount(address, undefined, keypair.publicKey, keypair.privateKey); 32 | }); 33 | }); 34 | 35 | test.serial('validate account with invalid address', async function(t) { 36 | const seed = generateSeed(); 37 | const keypair = deriveKeypair(seed); 38 | const address = deriveAddress(keypair.publicKey); 39 | 40 | t.throws(() => { 41 | new XrplAccount("", undefined, undefined, undefined) 42 | }, { 43 | instanceOf: XrplAddressException 44 | }); 45 | }); 46 | 47 | test.serial('validate account with address, but invalid secret', async function(t) { 48 | const seed = generateSeed(); 49 | const keypair = deriveKeypair(seed); 50 | const address = deriveAddress(keypair.publicKey); 51 | 52 | t.throws(() => { 53 | new XrplAccount(address, "", undefined, undefined) 54 | }, { 55 | instanceOf: XrplSecretException 56 | }); 57 | }); 58 | 59 | test.serial('validate account with address and secret is valid', async function(t) { 60 | const seed = generateSeed(); 61 | const keypair = deriveKeypair(seed); 62 | const address = deriveAddress(keypair.publicKey); 63 | 64 | t.notThrows(() => { 65 | new XrplAccount(address, seed, undefined, undefined) 66 | }); 67 | 68 | t.true(new XrplAccount(address, seed, undefined, undefined).hasSecret()); 69 | t.false(new XrplAccount(address, seed, undefined, undefined).hasKeypair()); 70 | }); 71 | 72 | test.serial('validate account with address but invalid keypair combinations', async function(t) { 73 | const seed = generateSeed(); 74 | const keypair = deriveKeypair(seed); 75 | const address = deriveAddress(keypair.publicKey); 76 | 77 | t.throws(() => { 78 | new XrplAccount(address, undefined, keypair.publicKey, undefined); 79 | }, { 80 | instanceOf: XrplKeypairException 81 | }, "Missing secret or private key"); 82 | 83 | t.throws(() => { 84 | new XrplAccount(address, undefined, undefined, keypair.privateKey); 85 | }, { 86 | instanceOf: XrplKeypairException 87 | }, "Missing secret or public key"); 88 | 89 | t.notThrows(() => { 90 | new XrplAccount(address, undefined, keypair.publicKey, keypair.privateKey); 91 | }, "Missing secret"); 92 | 93 | t.false(new XrplAccount(address, undefined, keypair.publicKey, keypair.privateKey).hasSecret(), "Has secret"); 94 | t.true(new XrplAccount(address, undefined, keypair.publicKey, keypair.privateKey).hasKeypair(), "Has keypair"); 95 | }); 96 | -------------------------------------------------------------------------------- /src/tests/compatibility.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import * as SologenicTypes from '../types'; 3 | import { SologenicTxHandler } from '../lib/txhandler'; 4 | import XrplAccount, { XrplAddressException, XrplSecretException, XrplKeypairException, XrplKeypairOrSecretMissingException } from '../lib/account'; 5 | import { generateSeed, deriveAddress, deriveKeypair } from 'ripple-keypairs'; 6 | 7 | test.serial('validate backwards compatibility, using secret wallets', async function(t) { 8 | const seed = 'ssH5SSKYvHBynnrYoCnmvsbxrNGEv'; 9 | const keypair = deriveKeypair(seed); 10 | const address = deriveAddress(keypair.publicKey); 11 | 12 | const xrplAccount = new XrplAccount(address, seed, keypair.publicKey, keypair.privateKey); 13 | 14 | try { 15 | const sologenic = await new SologenicTxHandler( 16 | // RippleAPI Options 17 | { 18 | server: 'wss://testnet.xrpl-labs.com', // Kudos to Wietse Wind 19 | }, 20 | // Sologenic Options, hash or redis 21 | { 22 | clearCache: true, 23 | queueType: "hash", 24 | hash: {} 25 | } 26 | ).connect(); 27 | 28 | // Events have their own types now. 29 | sologenic.on('queued', (event: SologenicTypes.QueuedEvent) => { 30 | console.log('GLOBAL QUEUED: ', event); 31 | }); 32 | sologenic.on('dispatched', (event: SologenicTypes.DispatchedEvent) => { 33 | console.log('GLOBAL DISPATCHED:', event); 34 | }); 35 | sologenic.on('requeued', (event: SologenicTypes.RequeuedEvent) => { 36 | console.log('GLOBAL REQUEUED:', event); 37 | }); 38 | sologenic.on('warning', (event: SologenicTypes.WarningEvent) => { 39 | console.log('GLOBAL WARNING:', event); 40 | }); 41 | sologenic.on('validated', (event: SologenicTypes.ValidatedEvent) => { 42 | console.log('GLOBAL VALIDATED:', event); 43 | }); 44 | sologenic.on('failed', (event: SologenicTypes.FailedEvent) => { 45 | console.log('GLOBAL FAILED:', event); 46 | }); 47 | 48 | await sologenic.setAccount({ 49 | address: 'rNbe8nh1K6nDC5XNsdAzHMtgYDXHZB486G', 50 | secret: 'ssH5SSKYvHBynnrYoCnmvsbxrNGEv' 51 | }); 52 | 53 | t.is(sologenic.getAccount().getAddress(), xrplAccount.getAddress()); 54 | t.is(sologenic.getAccount().getSecret(), xrplAccount.getSecret()); 55 | 56 | await sologenic.submit({ 57 | TransactionType: 'Payment', 58 | Account: 'rNbe8nh1K6nDC5XNsdAzHMtgYDXHZB486G', 59 | Destination: 'rUwty6Pf4gzUmCLVuKwrRWPYaUiUiku8Rg', 60 | Amount: { 61 | currency: '534F4C4F00000000000000000000000000000000', 62 | issuer: 'rNbe8nh1K6nDC5XNsdAzHMtgYDXHZB486G', 63 | value: '100000000' 64 | } 65 | }).promise; 66 | 67 | } catch (error) { 68 | t.fail(error); 69 | } 70 | }); 71 | 72 | test.serial('validate backwards compatibility, using keypair', async function(t) { 73 | const seed = 'ssH5SSKYvHBynnrYoCnmvsbxrNGEv'; 74 | const keypair = deriveKeypair(seed); 75 | const address = deriveAddress(keypair.publicKey); 76 | 77 | try { 78 | const sologenic = await new SologenicTxHandler( 79 | // RippleAPI Options 80 | { 81 | server: 'wss://testnet.xrpl-labs.com', // Kudos to Wietse Wind 82 | }, 83 | // Sologenic Options, hash or redis 84 | { 85 | clearCache: true, 86 | queueType: "hash", 87 | hash: {} 88 | } 89 | ).connect(); 90 | 91 | await sologenic.setAccount({ 92 | address: 'rNbe8nh1K6nDC5XNsdAzHMtgYDXHZB486G', 93 | keypair: { 94 | publicKey: keypair.publicKey, 95 | privateKey: keypair.privateKey 96 | } 97 | }); 98 | 99 | t.is(sologenic.getAccount().getAddress(), address); 100 | t.is(sologenic.getAccount().getSecret(), undefined); 101 | t.is(sologenic.getAccount().getKeypair().publicKey, keypair.publicKey); 102 | t.is(sologenic.getAccount().getKeypair().privateKey, keypair.privateKey); 103 | 104 | await sologenic.submit({ 105 | TransactionType: 'Payment', 106 | Account: 'rNbe8nh1K6nDC5XNsdAzHMtgYDXHZB486G', 107 | Destination: 'rUwty6Pf4gzUmCLVuKwrRWPYaUiUiku8Rg', 108 | Amount: { 109 | currency: '534F4C4F00000000000000000000000000000000', 110 | issuer: 'rNbe8nh1K6nDC5XNsdAzHMtgYDXHZB486G', 111 | value: '100000000' 112 | } 113 | }).promise; 114 | 115 | } catch (error) { 116 | t.fail(error); 117 | } 118 | }); 119 | 120 | 121 | -------------------------------------------------------------------------------- /src/types/txhandler.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import XrplAccount from '../lib/account'; 3 | import { IXummSubmitAdditional } from './xumm'; 4 | 5 | export interface ISologenicTxHandler extends EventEmitter { 6 | on(event: string, listener: Function): this; 7 | } 8 | 9 | export interface ISologenicTxSigner { 10 | sign( 11 | txJson: TX, 12 | txId: string, 13 | account: XrplAccount, 14 | signingOptions: any 15 | ): Promise; 16 | 17 | getIncludeSequence(): boolean; 18 | requestConnection(): any; 19 | cancelSigning(cancel: boolean): any; 20 | signerID: string; 21 | } 22 | 23 | export interface SignerConnectionRef { 24 | address?: string; 25 | accounts?: object[]; 26 | publicKey?: string; 27 | tx_json?: TX; 28 | meta?: { 29 | identifier: string; 30 | expires_at: string; 31 | submit: boolean; 32 | pushed: boolean; 33 | opened: boolean; 34 | resolved: boolean; 35 | signed: boolean; 36 | cancelled: boolean; 37 | expired: boolean; 38 | }; 39 | refs?: { 40 | qr: string; 41 | ws: string; 42 | deeplink: string; 43 | }; 44 | } 45 | 46 | export interface LedgerSelectedAccount { 47 | address: string; 48 | index: number; 49 | publicKey: string; 50 | info?: object | null; 51 | } 52 | 53 | export interface TX { 54 | Account: string; 55 | TransactionType: string; 56 | Memos?: { Memo: any }[]; 57 | Flags?: any; 58 | TransactionMetadata?: { 59 | offlineMeta?: object; 60 | xummMeta?: IXummSubmitAdditional; 61 | }; 62 | [Field: string]: string | number | object | Array | undefined; 63 | } 64 | 65 | export interface TxJSON { 66 | [Field: string]: any; 67 | } 68 | 69 | export interface SignedTx { 70 | id: string; 71 | signedTransaction: string; 72 | } 73 | 74 | export interface FormattedSubmitResponse { 75 | resultCode: string; 76 | resultMessage: string; 77 | } 78 | 79 | export interface ValidatedEvent { 80 | id: string; 81 | resolvedTx: ResolvedTx; 82 | dispatchedTx: DispatchedTx; 83 | reason: string; 84 | } 85 | 86 | export interface WarningEvent { 87 | id: string; 88 | state: string; 89 | reason: string; 90 | dispatchedTx?: DispatchedTx; 91 | unsignedTx?: UnsignedTx; 92 | } 93 | 94 | export interface RequeuedEvent { 95 | id: string; 96 | reason: string; 97 | dispatchedTx: DispatchedTx; 98 | } 99 | 100 | export interface QueuedEvent { 101 | id: string; 102 | txJson: TxJSON; 103 | } 104 | 105 | export interface SigningEvent { 106 | id: string; 107 | txJson: TxJSON; 108 | } 109 | 110 | export interface FailedEvent { 111 | id: string; 112 | failedTx: FailedTx; 113 | reason: string; 114 | result: any; 115 | } 116 | 117 | export interface DispatchedEvent { 118 | id: string; 119 | unsignedTx?: UnsignedTx; 120 | dispatchedTx: DispatchedTx; 121 | } 122 | 123 | export interface TxResult { 124 | status: any; 125 | hash?: any; 126 | sequence?: any; 127 | firstLedger?: any; 128 | lastLedger?: any; 129 | } 130 | 131 | export interface TxFailedResult { 132 | status: any; 133 | reason: string; 134 | } 135 | 136 | export interface DispatchedTx { 137 | unsignedTx?: UnsignedTx; 138 | result: TxResult; 139 | } 140 | 141 | export interface FailedTx { 142 | unsignedTx?: UnsignedTx; 143 | result: TxFailedResult; 144 | } 145 | 146 | export interface ResolvedTx { 147 | hash: string; 148 | sequence: number; 149 | accountSequence: number; 150 | ledgerVersion: number; 151 | timestamp: string; 152 | fee: string; 153 | } 154 | 155 | export interface TransactionObject { 156 | /** 157 | * @description events: Each instance of the submit() gets an instance of `EventEmitter` these events are emitted when certain actions take place within the transaction submission. 158 | * Events: {queued, dispatched, requeued, warning, validated} 159 | * 160 | * @description id: This is the uuid generated in a non-blocking approach so clients can later use this id for reference. The id is of type of string and are generated using v4 of uuid library. 161 | * e.g: 6316751c-bde4-412b-ac9a-7d05e548171f 162 | * 163 | * @description promise: This property contains a promise and resolves only when a transaction has been validated. 164 | * Contains: hash, dispatchedSequence, accountSequence, ledgerVersion, timestamp, fee 165 | */ 166 | events: EventEmitter; 167 | id: string; 168 | promise: Promise; 169 | } 170 | 171 | export interface UnsignedTx { 172 | id: string; 173 | data: TxJSON; 174 | } 175 | -------------------------------------------------------------------------------- /src/lib/queues/hash.ts: -------------------------------------------------------------------------------- 1 | import { MQTX, IQueue, HashTransactionHandlerOptions } from '../../types'; 2 | 3 | import { v4 as uuid } from 'uuid'; 4 | 5 | export default class HashQueue implements IQueue { 6 | private hash: Map> = new Map>(); 7 | 8 | constructor(options: HashTransactionHandlerOptions) { 9 | options!; 10 | } 11 | 12 | public async deleteQueue(queue: string): Promise { 13 | if (this.hash.hasOwnProperty(queue)) { 14 | return this.hash.delete(queue); 15 | } 16 | 17 | return false; 18 | } 19 | 20 | public async queues(): Promise { 21 | var keys = Array(); 22 | 23 | for (var key in this.hash) { 24 | if (this.hash.hasOwnProperty(key)) { 25 | keys.push(key); 26 | } 27 | } 28 | 29 | return keys; 30 | } 31 | 32 | private _exist(queue: string): boolean { 33 | return this.hash.has(queue); 34 | } 35 | 36 | /** 37 | * 38 | * @param queue 39 | * @param data 40 | * @param id 41 | * @description add an object to the queue 42 | */ 43 | public async add(queue: string, data: MQTX, id?: string): Promise { 44 | const element = { 45 | id: typeof id !== 'undefined' ? id : uuid(), 46 | created: data.hasOwnProperty('created') ? data.created : Math.floor(new Date().getTime() / 1000), 47 | data, 48 | }; 49 | 50 | var _queue = this._exist(queue) ? this.hash.get(queue) : new Array(); 51 | 52 | if (_queue instanceof Array) { 53 | _queue.push(element); 54 | 55 | this.hash.set(queue, _queue); 56 | } 57 | 58 | return element; 59 | } 60 | 61 | /** 62 | * 63 | * @param queue 64 | * @param id 65 | * @description returns a specific object within the queue 66 | */ 67 | public async get(queue: string, id: string): Promise { 68 | var _queue = this._exist(queue) ? this.hash.get(queue) : new Array(); 69 | 70 | var found = undefined; 71 | 72 | if (_queue instanceof Array) { 73 | _queue.forEach(obj => { 74 | if (obj.id === id) found = obj; 75 | }); 76 | } 77 | 78 | return found; 79 | } 80 | 81 | /** 82 | * 83 | * @param queue 84 | * @description returns all elements of the queue 85 | */ 86 | public async getAll(queue: string): Promise> { 87 | return this.hash.get(queue) || Array(); 88 | 89 | /* 90 | this.hash.forEach(function(_, key, data) { 91 | elements.set(key, data.get(key) || []); 92 | }); 93 | 94 | return elements; 95 | */ 96 | } 97 | 98 | /** 99 | * 100 | * @param queue 101 | * @description pop an element off the end of the queue 102 | */ 103 | 104 | public async pop(queue: string): Promise { 105 | try { 106 | if (this._exist(queue)) { 107 | const data = this.hash.get(queue) || []; 108 | 109 | if (data.length > 0) { 110 | const item = data.pop(); 111 | 112 | this.hash.set(queue, data); 113 | 114 | return item; 115 | } 116 | 117 | return undefined; 118 | } 119 | } catch (error) { 120 | return undefined; 121 | } 122 | 123 | return undefined; 124 | } 125 | 126 | /** 127 | * 128 | * @param queue 129 | * @param id 130 | * @description delete an object by id from the queue 131 | */ 132 | public async del(queue: string, id: string): Promise { 133 | if (this._exist(queue)) { 134 | const data = this.hash.get(queue); 135 | 136 | if (typeof data !== 'undefined') { 137 | const filteredObjects = data.filter(e => e.id !== id); 138 | 139 | if (data.length > filteredObjects.length) { 140 | this.hash.set(queue, filteredObjects); 141 | 142 | return true; 143 | } 144 | } 145 | } 146 | 147 | return false; 148 | } 149 | 150 | /** 151 | * @param queue 152 | * @description delete all elements from the queue 153 | */ 154 | public async delAll(queue: string): Promise { 155 | return this.hash.delete(queue); 156 | } 157 | 158 | public async appendEvent( 159 | queue: string, 160 | id: string, 161 | event_name: string 162 | ): Promise { 163 | try { 164 | let result; 165 | 166 | result = await this.get(queue, id); 167 | 168 | if (result && typeof result.data.events === 'undefined') 169 | result.data.events = []; 170 | 171 | result.data.events.push(event_name); 172 | 173 | await this.del(queue, id); 174 | await this.add(queue, result!.data, id); 175 | 176 | return true; 177 | } catch (error) { 178 | return false; 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/lib/signing/xumm.ts: -------------------------------------------------------------------------------- 1 | import { http, promiseTimeout, wait } from '../utils'; 2 | 3 | import XrplAccount from '../account'; 4 | import * as SologenicTypes from '../../types'; 5 | import { SologenicTxSigner } from './index'; 6 | import { SologenicError } from '../error'; 7 | import { IXummQueryPayload, IXummSubmitPayload } from '../../types/xumm'; 8 | 9 | export class XummSigner extends SologenicTxSigner { 10 | protected xummApiKey: string = ''; 11 | protected xummApiSecret: string = ''; 12 | protected xummApiEndpoint: string = 13 | 'https://xumm.app/api/v1/platform/payload'; 14 | protected xummApiUserToken?: string; 15 | signerID: string = 'xumm'; 16 | 17 | // 60 seconds should be enough for a user interaction 18 | protected maximumExecutionTime: number = 60 * 1000; 19 | 20 | constructor(options: any) { 21 | super(options); 22 | 23 | this.includeSequence = false; 24 | 25 | if (options.hasOwnProperty('xummApiEndpoint')) { 26 | this.xummApiEndpoint = options['xummApiEndpoint']; 27 | } 28 | 29 | if (options.hasOwnProperty('xummApiKey')) { 30 | this.xummApiKey = options['xummApiKey']; 31 | } 32 | 33 | if (options.hasOwnProperty('xummApiSecret')) { 34 | this.xummApiSecret = options['xummApiSecret']; 35 | } 36 | 37 | if (options.hasOwnProperty('xummApiUserToken')) { 38 | this.xummApiUserToken = options['xummApiUserToken']; 39 | } 40 | 41 | if (options.hasOwnProperty('maximumExecutionTime')) { 42 | this.maximumExecutionTime = options['maximumExecutionTime']; 43 | } 44 | } 45 | 46 | protected _headers(): object { 47 | return { 48 | 'X-API-Key': this.xummApiKey, 49 | 'X-API-Secret': this.xummApiSecret 50 | }; 51 | } 52 | 53 | async verify(payload: string): Promise { 54 | while (true) { 55 | const result = await http( 56 | `${this.xummApiEndpoint}/${payload}`, 57 | 'GET', 58 | this._headers() 59 | ); 60 | 61 | if ( 62 | result.hasOwnProperty('error') && 63 | result.hasOwnProperty('code') && 64 | result.hasOwnProperty('message') 65 | ) { 66 | return undefined; 67 | } else if (result.meta! && result.meta!.resolved) { 68 | if (result.meta!.signed) { 69 | // If you would like to see the raw signed payload 70 | // console.log("XUMM SIGNED PAYLOAD"); 71 | // console.log(result); 72 | 73 | // The request has been signed, send it back, otherwise return undefined if the 74 | // request has been resolved but not signed/cancelled/expired/etc. 75 | // This is probably quite simple for now and can be extended later. 76 | return result; 77 | } 78 | 79 | return undefined; 80 | } 81 | 82 | await wait(2500); 83 | } 84 | } 85 | 86 | async sign( 87 | txJson: SologenicTypes.TX, 88 | txId: string, 89 | account?: XrplAccount, 90 | signingOptions?: any 91 | ): Promise { 92 | txJson; 93 | account; 94 | signingOptions; 95 | 96 | const txMeta: any = txJson.TransactionMetadata; 97 | 98 | const xummMeta: any = txMeta.xummMeta; 99 | 100 | // Delete the transaction metadata if it exists since the signing will fail 101 | // as this TransactionMetadata is not known to the schema. 102 | if (txJson.TransactionMetadata) { 103 | delete txJson.TransactionMetadata; 104 | } 105 | 106 | const xummOptionsPayload = { 107 | options: { 108 | // If submit is false, xumm returns the signed transaction. 109 | // If submit is true, xumm returns the signed transaction, but also submits to the XRPL for us. 110 | submit: false, 111 | expire: Math.ceil(this.maximumExecutionTime / 1000 / 60).toFixed() 112 | }, 113 | user_token: 114 | typeof xummMeta !== 'undefined' ? xummMeta.issued_user_token : '' 115 | }; 116 | 117 | const result = await http( 118 | this.xummApiEndpoint, 119 | 'POST', 120 | this._headers(), 121 | JSON.stringify({ 122 | txjson: txJson, 123 | ...xummOptionsPayload 124 | }) 125 | ); 126 | 127 | // If you would like to see the raw response (with app_url) payload to the xumm API 128 | // console.log("XUMM PAYLOAD (with app_url)"); 129 | // console.log(result); 130 | 131 | const verification: IXummQueryPayload = await promiseTimeout( 132 | this.maximumExecutionTime, 133 | this.verify(result.uuid) 134 | ); 135 | 136 | if (typeof verification === 'undefined') { 137 | // Unable to sign request (request was rejected or cancelled) 138 | throw new SologenicError('2002'); 139 | } 140 | 141 | // Return the signed transaction 142 | return { 143 | id: txId, 144 | signedTransaction: verification.response.hex 145 | }; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sologenic-xrpl-stream-js", 3 | "version": "1.1.0", 4 | "description": "Persistent transaction handling for the XRP Ledger", 5 | "main": "build/main/index.js", 6 | "typings": "build/main/index.d.ts", 7 | "module": "build/module/index.js", 8 | "repository": "https://github.com/sologenic/sologenic-xrpl-stream-js", 9 | "license": "MIT", 10 | "keywords": [], 11 | "scripts": { 12 | "describe": "npm-scripts-info", 13 | "build": "run-s clean && run-p build:*", 14 | "build:main": "tsc -p tsconfig.json", 15 | "build:module": "tsc -p tsconfig.module.json", 16 | "fix": "run-s fix:*", 17 | "fix:prettier": "prettier \"src/**/*.ts\" --write", 18 | "fix:tslint": "tslint --fix --project .", 19 | "test": "run-s build test:*", 20 | "test:lint": "tslint --project .", 21 | "test:unit": "nyc ava --verbose", 22 | "watch": "run-s clean build:main && run-p \"build:main -- -w\" \"test:unit -- --watch\"", 23 | "cov": "run-s build test:unit cov:html && open-cli coverage/index.html", 24 | "cov:html": "nyc report --reporter=html", 25 | "cov:send": "nyc report --reporter=lcov && codecov", 26 | "cov:check": "nyc report && nyc check-coverage --lines 100 --functions 100 --branches 100", 27 | "doc": "run-s doc:html && run-s doc:markdown && open-cli docs/index.html", 28 | "doc:html": "typedoc src/ --exclude **/*.spec.ts --target ES6 --plugin none --mode file --out docs/html", 29 | "doc:markdown": "typedoc src/ --exclude **/*.spec.ts --target ES6 --plugin typedoc-plugin-markdown --mode file --out docs/markdown", 30 | "doc:json": "typedoc src/ --exclude **/*.spec.ts --exclude node_modules/** --target ES6 --mode file --json docs/typedoc.json", 31 | "doc:publish": "gh-pages -m \"[ci skip] Updates\" -d docs", 32 | "version": "standard-version", 33 | "reset": "git clean -dfx && git reset --hard && npm i", 34 | "clean": "trash build test", 35 | "prepare-release": "run-s reset test cov:check doc:html version doc:publish" 36 | }, 37 | "scripts-info": { 38 | "info": "Display information about the package scripts", 39 | "build": "Clean and rebuild the project", 40 | "fix": "Try to automatically fix any linting problems", 41 | "test": "Lint and unit test the project", 42 | "watch": "Watch and rebuild the project on save, then rerun relevant tests", 43 | "cov": "Rebuild, run tests, then create and open the coverage report", 44 | "doc": "Generate HTML API documentation and open it in a browser", 45 | "doc:json": "Generate API documentation in typedoc JSON format", 46 | "version": "Bump package.json version, update CHANGELOG.md, tag release", 47 | "reset": "Delete all untracked files and reset the repo to the last commit", 48 | "prepare-release": "One-step: clean, build, test, publish docs, and prep a release" 49 | }, 50 | "engines": { 51 | "node": ">=8.9" 52 | }, 53 | "dependencies": { 54 | "@ledgerhq/hw-app-xrp": "^5.45.0", 55 | "@ledgerhq/hw-transport-webusb": "^5.45.0", 56 | "@types/ioredis": "^4.14.7", 57 | "@types/mathjs": "6.0.4", 58 | "@types/node": "12.11.5", 59 | "@types/node-fetch": "^2.5.4", 60 | "@types/request": "^2.48.4", 61 | "@types/underscore": "^1.9.4", 62 | "@types/uuid": "^3.4.7", 63 | "axios": "^0.21.1", 64 | "crypto": "^1.0.1", 65 | "dcent-web-connector": "^0.10.1", 66 | "dns": "^0.2.2", 67 | "ioredis": "^4.14.1", 68 | "mathjs": "6.6.0", 69 | "moment": "^2.29.1", 70 | "net": "^1.0.2", 71 | "node-fetch": "^2.6.1", 72 | "request": "^2.88.2", 73 | "ripple-binary-codec": "^0.2.7", 74 | "ripple-lib": "1.6.5", 75 | "stream": "0.0.2", 76 | "tls": "0.0.1", 77 | "ts-node": "^8.8.1", 78 | "typedoc-plugin-markdown": "^2.2.16", 79 | "underscore": "^1.9.2", 80 | "uuid": "^3.4.0" 81 | }, 82 | "devDependencies": { 83 | "@bitjson/npm-scripts-info": "^1.0.0", 84 | "@bitjson/typedoc": "^0.15.0-0", 85 | "@istanbuljs/nyc-config-typescript": "^0.1.3", 86 | "ava": "2.2.0", 87 | "codecov": "^3.6.5", 88 | "cz-conventional-changelog": "^2.1.0", 89 | "gh-pages": "^2.2.0", 90 | "npm-run-all": "^4.1.5", 91 | "nyc": "^14.1.1", 92 | "open-cli": "^5.0.0", 93 | "prettier": "^1.18.2", 94 | "standard-version": "^6.0.1", 95 | "trash-cli": "^3.0.0", 96 | "tslint": "^5.18.0", 97 | "tslint-config-prettier": "^1.18.0", 98 | "tslint-immutable": "^6.0.1", 99 | "typedoc": "^0.16.9", 100 | "typescript": "3.7.5" 101 | }, 102 | "ava": { 103 | "failFast": true, 104 | "files": [ 105 | "build/main/**/*.spec.js" 106 | ], 107 | "sources": [ 108 | "build/main/**/*.js" 109 | ] 110 | }, 111 | "config": { 112 | "commitizen": { 113 | "path": "cz-conventional-changelog" 114 | } 115 | }, 116 | "prettier": { 117 | "singleQuote": true 118 | }, 119 | "nyc": { 120 | "extends": "@istanbuljs/nyc-config-typescript", 121 | "exclude": [ 122 | "**/*.spec.js" 123 | ] 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/lib/queues/redis.ts: -------------------------------------------------------------------------------- 1 | import Redis from 'ioredis'; 2 | import { 3 | MQTX, 4 | IQueue, 5 | RedisTransactionHandlerOptions 6 | } from '../../types'; 7 | 8 | import { v4 as uuid } from 'uuid'; 9 | 10 | export default class RedisQueue implements IQueue { 11 | private redis: any; 12 | 13 | constructor(options: RedisTransactionHandlerOptions) { 14 | try { 15 | this.redis = new Redis(options); 16 | } catch (error) { 17 | throw new Error('Unable to initialize TXMQ'); 18 | } 19 | } 20 | 21 | public async deleteQueue(queue: string): Promise { 22 | if (await this.redis.exists(queue)) { 23 | await this.redis.del(queue); 24 | 25 | return true; 26 | } 27 | 28 | return false; 29 | } 30 | 31 | public async queues(): Promise { 32 | return this.redis.keys('*'); 33 | } 34 | 35 | /** 36 | * 37 | * @param queue 38 | * @param data 39 | * @param id 40 | */ 41 | public async add(queue: string, data: MQTX, id?: string): Promise { 42 | try { 43 | const element = { 44 | id: typeof id !== 'undefined' ? id : uuid(), 45 | created: data.hasOwnProperty('created') ? data.created : Math.floor(new Date().getTime() / 1000), 46 | data 47 | }; 48 | const result = await this.redis.rpush(queue, JSON.stringify(element)); 49 | if (result > 0) { 50 | return element; 51 | } else { 52 | throw new Error("Can't get TX from Redis"); 53 | } 54 | } catch (error) { 55 | throw new Error("Can't get TX from Redis"); 56 | } 57 | } 58 | /** 59 | * 60 | * @param queue 61 | * @param id 62 | */ 63 | public async get(queue: string, id: string): Promise { 64 | try { 65 | const elements = await this.redis.lrange(queue, 0, -1); 66 | const element = elements.find((el: string) => { 67 | const parsed = JSON.parse(el); 68 | if (parsed.id === id) { 69 | return parsed; 70 | } 71 | }); 72 | if (element) { 73 | return JSON.parse(element); 74 | } else { 75 | return undefined; 76 | } 77 | } catch (error) { 78 | throw new Error("Can't get TX from Redis"); 79 | } 80 | } 81 | /** 82 | * 83 | * @param queue 84 | */ 85 | public async getAll(queue: string): Promise { 86 | const elements = await this.redis.lrange(queue, 0, -1); 87 | 88 | if (elements.length > 0) { 89 | return elements.map((el: string) => { 90 | return JSON.parse(el); 91 | }); 92 | } 93 | 94 | return []; 95 | } 96 | 97 | /** 98 | * 99 | * @param queue 100 | */ 101 | public async pop(queue: string): Promise { 102 | try { 103 | const element = await this.redis.blpop(queue, 1); 104 | 105 | /* 106 | If the returned element not undefined 107 | and its length is greater than 0, return the object 108 | */ 109 | 110 | return (element && element.length > 0) ? JSON.parse(element[1]) : undefined; 111 | 112 | } catch (error) { 113 | throw new Error("Can't get TX from Redis"); 114 | } 115 | } 116 | 117 | public async del(queue: string, id: string): Promise { 118 | try { 119 | const elements = await this.redis.lrange(queue, 0, -1); 120 | 121 | const element = elements.find((el: string) => { 122 | const parsed = JSON.parse(el); 123 | 124 | if (parsed.id === id) { 125 | return parsed; 126 | } 127 | }); 128 | 129 | const result = await this.redis.lrem(queue, 1, element); 130 | 131 | if (result) { 132 | return true; 133 | } else { 134 | return false; 135 | } 136 | } catch (error) { 137 | throw new Error("Can't get TX from Redis"); 138 | } 139 | } 140 | /** 141 | * 142 | * @param queue 143 | */ 144 | public async delAll(queue: string): Promise { 145 | try { 146 | const elements = await this.redis.del(queue); 147 | 148 | if (elements > 0) { 149 | return true; 150 | } else { 151 | return false; 152 | } 153 | } catch (error) { 154 | throw new Error("Can't get TX from Redis"); 155 | } 156 | } 157 | 158 | public async appendEvent(queue: string, id: string, event_name: string): Promise { 159 | try { 160 | let result; 161 | 162 | result = await this.get(queue, id); 163 | 164 | if (result && typeof result.data.events === 'undefined') 165 | result.data.events = []; 166 | 167 | result.data.events.push(event_name); 168 | 169 | await this.del(queue, id); 170 | await this.add(queue, result!.data, id); 171 | 172 | return true; 173 | } catch (error) { 174 | return false; 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/lib/queues/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MQTX, 3 | IQueue, 4 | QUEUE_TYPE_STXMQ_REDIS, 5 | QUEUE_TYPE_STXMQ_HASH, 6 | TransactionHandlerOptions 7 | } from '../../types/queues'; 8 | 9 | /** 10 | * Import redis queue implementation 11 | */ 12 | import RedisQueue from './redis'; 13 | 14 | /** 15 | * Import hash queue implementation 16 | */ 17 | import HashQueue from './hash'; 18 | 19 | /** 20 | * The TXMQƨ class is an implementation that calls the methods against the queue 21 | * instances. When constructing the [[SologenicTxHandler]] the `sologenicOptions` 22 | * parameter is passed to this classes constructor. 23 | */ 24 | export default class TXMQƨ implements IQueue { 25 | private queue: IQueue; 26 | 27 | constructor(sologenicOptions: TransactionHandlerOptions) { 28 | try { 29 | switch (sologenicOptions!.queueType) { 30 | case QUEUE_TYPE_STXMQ_REDIS: 31 | this.queue = new RedisQueue(sologenicOptions.redis!); 32 | break; 33 | 34 | case QUEUE_TYPE_STXMQ_HASH: 35 | this.queue = new HashQueue(sologenicOptions.hash!); 36 | break; 37 | 38 | default: 39 | this.queue = new HashQueue(sologenicOptions.hash!); 40 | break; 41 | } 42 | } catch (error) { 43 | throw new Error('Unable to initialize TXMQ'); 44 | } 45 | } 46 | 47 | public deleteQueue(queue: string): Promise { 48 | return this.queue.deleteQueue(queue); 49 | } 50 | 51 | public queues(): Promise { 52 | return this.queue.queues(); 53 | } 54 | 55 | /** 56 | * Add a new data object to a queue with an optional `id`. If the `id` is not provided 57 | * an unique UUID will generated and assigned. This method will return the created 58 | * object rather than just the `id`. 59 | * 60 | * @todo Only return `id` from this method 61 | * 62 | * @param queue Queue name 63 | * @param data Object of keys and values 64 | * @param id Optional `id` key to store the data against 65 | */ 66 | public async add(queue: string, data: MQTX, id?: string): Promise { 67 | return this.queue.add(queue, data, id); 68 | } 69 | 70 | /** 71 | * Returns a single object from the queue or undefined 72 | * 73 | * @param queue Queue name 74 | * @param id Key used to retrieve the data 75 | */ 76 | public async get(queue: string, id: string): Promise { 77 | return this.queue.get(queue, id); 78 | } 79 | 80 | /** 81 | * Returns all objects from within the queue 82 | * 83 | * @param queue Queue name 84 | */ 85 | public async getAll(queue?: string): Promise { 86 | return this.queue.getAll(queue); 87 | } 88 | /** 89 | * Pop and element from the end of the queue 90 | * 91 | * @param queue Queue name 92 | */ 93 | public async pop(queue: string): Promise { 94 | const result = await this.queue.pop(queue); 95 | const items = await this.queue.getAll(queue); 96 | 97 | if (items.length < 1) { 98 | await this.queue.deleteQueue(queue); 99 | } 100 | 101 | return result; 102 | } 103 | 104 | /** 105 | * Delete an element from the queue 106 | * 107 | * @param queue Queue name 108 | * @param id Key used to retrieve the data 109 | */ 110 | public async del(queue: string, id: string): Promise { 111 | const result = await this.queue.del(queue, id); 112 | const items = await this.queue.getAll(queue); 113 | 114 | if (items.length < 1) { 115 | await this.queue.deleteQueue(queue); 116 | } 117 | 118 | return result; 119 | } 120 | 121 | /** 122 | * Delete all items from the queue 123 | * 124 | * @param queue Queue name 125 | */ 126 | public async delAll(queue: string): Promise { 127 | const result = await this.queue.delAll(queue); 128 | 129 | await this.queue.deleteQueue(queue); 130 | 131 | return result; 132 | } 133 | 134 | /** 135 | * @ignore Not used anymore, to be removed. 136 | */ 137 | public async appendEvent( 138 | queue: string, 139 | id: string, 140 | event_name: string 141 | ): Promise { 142 | return this.queue.appendEvent(queue, id, event_name); 143 | } 144 | 145 | /** 146 | * Delete entries older than maximumTimeToLive (seconds) 147 | */ 148 | public async deleteOlderThan(maximumTimeToLive: number, queue?: string): Promise { 149 | let counter = 0; 150 | 151 | const currentTime = Math.floor(new Date().getTime() / 1000); 152 | const queueNames = await this.queue.queues(); 153 | 154 | if (maximumTimeToLive < 0) { 155 | return counter; 156 | } 157 | 158 | for (var queueName in queueNames) { 159 | if (queueNames[queueName] !== queue) 160 | // Skip the queue if it does not match what we're looking for (if specified) 161 | continue; 162 | 163 | const items = await this.queue.getAll(queueNames[queueName]); 164 | 165 | for (var key in items) { 166 | if (items[key].created + maximumTimeToLive <= currentTime) { 167 | // console.log(`Item (${items[key].created} is older than ${currentTime}), so ${JSON.stringify(items[key])} will be deleted`); 168 | await this.queue.del(queueNames[queueName], items[key].id); 169 | 170 | counter++; 171 | } 172 | } 173 | } 174 | 175 | return counter; 176 | } 177 | } 178 | 179 | export { 180 | HashQueue, 181 | RedisQueue 182 | }; 183 | -------------------------------------------------------------------------------- /src/lib/signing/ledger_device.ts: -------------------------------------------------------------------------------- 1 | import XrplAccount from '../account'; 2 | import * as SologenicTypes from '../../types'; 3 | import { SologenicTxSigner } from './index'; 4 | import { SologenicError } from '../error'; 5 | import { wait } from '../utils'; 6 | import { RippleAPI } from 'ripple-lib'; 7 | const TransportWebUSB = require('@ledgerhq/hw-transport-webusb').default; 8 | const Xrp = require('@ledgerhq/hw-app-xrp').default; 9 | 10 | const binaryCodec = require('ripple-binary-codec'); 11 | export class LedgerDeviceSigner extends SologenicTxSigner { 12 | protected getAddress: any = ''; 13 | protected getAppConfiguration: any = ''; 14 | protected signTransaction: any = ''; 15 | protected transport: any = ''; 16 | protected bip32Path: string = ''; // "44'/144'/i'/0/0"; 17 | protected address: string = ''; 18 | protected publicKey: string = ''; 19 | protected api: RippleAPI; 20 | private _signAttempts: number = 0; 21 | signerID: string = 'ledger'; 22 | 23 | constructor(options: any) { 24 | super(options); 25 | 26 | if (options.hasOwnProperty('ripple_server')) { 27 | // this.ripple_server = options['ripple_server']; 28 | console.log(options['ripple_server']); 29 | 30 | this.api = new RippleAPI({ 31 | server: options['ripple_server'], 32 | feeCushion: 1, 33 | timeout: 1000000 34 | }); 35 | 36 | this.api.connect(); 37 | } else { 38 | throw new Error('Ripple Server url is missing'); 39 | } 40 | 41 | this.includeSequence = true; 42 | } 43 | 44 | public async getWalletAddress() { 45 | if (this.address && this.publicKey) { 46 | return { 47 | address: this.address, 48 | publicKey: this.publicKey 49 | }; 50 | } else { 51 | // const { address, publicKey } = await this.getAddress(this.bip32Path); 52 | 53 | // this.address = address; 54 | // this.publicKey = publicKey; 55 | 56 | // return { 57 | // address, 58 | // publicKey 59 | // }; 60 | 61 | let accounts: Object[] = []; 62 | let bipIndex = 0; 63 | 64 | while (true) { 65 | const { address, publicKey } = await this.getAddress( 66 | `44'/144'/${bipIndex}'/0/0` 67 | ); 68 | 69 | const addressInfo = await this.api 70 | .getAccountInfo(address) 71 | .then(r => r) 72 | .catch(() => null); 73 | 74 | const account = { 75 | address: address, 76 | publicKey: publicKey, 77 | info: addressInfo, 78 | index: bipIndex 79 | }; 80 | 81 | accounts = [...accounts, account]; 82 | 83 | if (addressInfo === null) { 84 | break; 85 | } 86 | 87 | bipIndex++; 88 | } 89 | 90 | return { accounts }; 91 | } 92 | } 93 | 94 | public async setSelectedAccount( 95 | account: SologenicTypes.LedgerSelectedAccount 96 | ): Promise { 97 | const bip32 = `44'/144'/${account.index}'/0/0`; 98 | 99 | this.bip32Path = bip32; 100 | this.address = account.address; 101 | this.publicKey = account.publicKey; 102 | 103 | return true; 104 | } 105 | 106 | public async requestConnection(): Promise< 107 | SologenicTypes.SignerConnectionRef 108 | > { 109 | try { 110 | // Request connection to Ledger Device (Speculos uses http, the actual device use webusb) 111 | const trans = await TransportWebUSB.create(); 112 | // Create the communication object between the Ledger and the WebApp 113 | const xrpApp = new Xrp(trans); 114 | 115 | this.getAddress = xrpApp.getAddress; 116 | this.getAppConfiguration = xrpApp.getAppConfiguration; 117 | this.signTransaction = xrpApp.signTransaction; 118 | this.transport = trans; 119 | 120 | return await this.getWalletAddress(); 121 | } catch (e) { 122 | console.log('E_CONNECTING_SIGNER ->', e); 123 | throw new Error(e.message); 124 | } 125 | } 126 | 127 | public async sign( 128 | txJson: SologenicTypes.TX, 129 | txId: string, 130 | _account?: XrplAccount, 131 | _signingOptions: any = {} 132 | ): Promise { 133 | try { 134 | if (this._signAttempts > 10) { 135 | throw new SologenicError('1003'); 136 | } 137 | 138 | this._signAttempts += 1; 139 | // Delete the transaction metadata if it exists since the signing will fail 140 | // as this TransactionMetadata is not known to the schema. 141 | if (txJson.TransactionMetadata) delete txJson.TransactionMetadata; 142 | 143 | if (txJson.LastLedgerSequence) 144 | txJson.LastLedgerSequence = Number(txJson.LastLedgerSequence) + 100; 145 | 146 | // Add Public Key to the txJson to encode. 147 | txJson.SigningPubKey = this.publicKey.toUpperCase(); 148 | 149 | // Encode the Transaction 150 | const txBlob: string = await binaryCodec.encode(txJson); 151 | 152 | // Pass the txBlob to be signed by the LedgerDevice, this will return ONLY the signature for the transaction. It will not return the signed transaction. 153 | const signature: string = await this.signTransaction( 154 | this.bip32Path, 155 | txBlob 156 | ); 157 | 158 | // Add the signature to the transaction 159 | txJson.TxnSignature = signature.toUpperCase(); 160 | 161 | // Return the signed transaction 162 | return { 163 | id: txId, 164 | signedTransaction: binaryCodec.encode(txJson) 165 | }; 166 | } catch (e) { 167 | // This error is thrown if the user rejects the transaction on the LedgerDevice 168 | if (e.statusText === 'CONDITIONS_OF_USE_NOT_SATISFIED') { 169 | throw new SologenicError('2003'); 170 | } 171 | 172 | if (e.id === 'TransportLocked') { 173 | if (e.message === 'Ledger Device is busy (lock getAddress)') { 174 | await wait(500); 175 | await this.sign(txJson, txId, _account, _signingOptions); 176 | } else { 177 | throw new Error('Device Busy'); 178 | } 179 | } 180 | 181 | throw new SologenicError('1000'); 182 | } 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/lib/account.ts: -------------------------------------------------------------------------------- 1 | import * as SologenicTypes from '../types/'; 2 | import { RippleAPI } from 'ripple-lib'; 3 | import * as RippleError from 'ripple-lib/dist/npm/common/errors'; 4 | 5 | export class XrplException extends Error { 6 | /** 7 | * @param message Message for error 8 | * @param error Inner error 9 | */ 10 | 11 | public error?: Error; 12 | 13 | constructor(message: string, error?: Error|undefined) { 14 | super(message); 15 | 16 | this.error = error; 17 | } 18 | } 19 | 20 | export class XrplAddressException extends XrplException { 21 | constructor(message: string, error?: Error|undefined) { 22 | super(message, error); 23 | } 24 | } 25 | 26 | export class XrplSecretException extends XrplException { 27 | constructor(message: string, error?: Error|undefined) { 28 | super(message, error); 29 | } 30 | } 31 | 32 | export class XrplKeypairException extends XrplException { 33 | constructor(message: string, error?: Error|undefined) { 34 | super(message, error); 35 | } 36 | } 37 | 38 | export class XrplKeypairOrSecretMissingException extends XrplException { 39 | constructor(message: string, error?: Error|undefined) { 40 | super(message, error); 41 | } 42 | } 43 | 44 | export default class XrplAccount { 45 | constructor(address: string, secret?: string, publicKey?: string, privateKey?: string) { 46 | this.address = address; 47 | this.secret = secret; 48 | this.keypair = undefined; 49 | 50 | if (typeof(publicKey) !== 'undefined' || typeof(privateKey) !== 'undefined') { 51 | this.keypair = { 52 | publicKey: publicKey!, 53 | privateKey: privateKey! 54 | }; 55 | } 56 | 57 | // Peform a validation 58 | this.validate(); 59 | } 60 | 61 | /** 62 | * Ripple API 63 | */ 64 | 65 | protected rippleApi: RippleAPI = new RippleAPI(); 66 | 67 | /** 68 | * XRPL Account 69 | */ 70 | protected address: string; 71 | 72 | /** 73 | * XRPL Account Secret 74 | */ 75 | protected secret?: string; 76 | 77 | /** 78 | * XRPL Keypair. Reference https://xrpl.org/cryptographic-keys.html#master-key-pair 79 | * and https://xrpl.org/cryptographic-keys.html#regular-key-pair for more details on 80 | * on the difference between the account seed and key pairs and how to disable the 81 | * master key on your account 82 | */ 83 | protected keypair?: SologenicTypes.KeyPair; 84 | 85 | /** 86 | * Current sequence number of the XRPL 87 | */ 88 | 89 | protected currentSequence: number = 0; 90 | protected previousSequence?: number; 91 | protected previousTxId?: any; 92 | 93 | /** 94 | * Initialize an xrpl account 95 | */ 96 | public static getAccount(address: string, secret?: string, publicKey?: string, privateKey?: string): XrplAccount { 97 | return new XrplAccount(address, secret, publicKey, privateKey); 98 | } 99 | 100 | /** 101 | * Validate an account 102 | */ 103 | public validate(): void { 104 | if (!this.rippleApi.isValidAddress(this.address)) { 105 | throw new XrplAddressException('Address is not valid', new RippleError.ValidationError()); 106 | } 107 | 108 | if (typeof(this.secret) !== 'undefined' && !this.rippleApi.isValidSecret(this.secret)) { 109 | throw new XrplSecretException('Secret is not valid', new RippleError.ValidationError()); 110 | } 111 | 112 | if (typeof(this.keypair) === 'object' 113 | && (typeof(this.keypair.publicKey) === 'undefined' || typeof(this.keypair.privateKey) === 'undefined')) { 114 | throw new XrplKeypairException('Keypair is not valid', new RippleError.ValidationError()); 115 | } 116 | 117 | // Don't check if we have a keypair or secret because 118 | // we may only pass in an address because we are are only 119 | // wanting to monitor an address 120 | 121 | /* 122 | if (!this.hasKeypair() && !this.hasSecret()) { 123 | throw new XrplKeypairOrSecretMissingException('Missing keypair or secret'); 124 | } 125 | */ 126 | } 127 | 128 | /** 129 | * Set XRPL account sequence 130 | * @param sequence XRPL account sequence 131 | * @returns {Promise.} 132 | */ 133 | public setAccountSequence(currentSequence: number, previousSequence?: number, previousTxId?: string) { 134 | // Prevent race since the current sequence is sometimes calculate at the end of the 135 | // ledger, therefore once a transaction is created, it fails because the sequence 136 | // is already past. 137 | 138 | this.currentSequence = currentSequence; 139 | this.previousSequence = previousSequence; 140 | this.previousTxId = previousTxId; 141 | 142 | // console.log(`Assigning account sequence (currentSequence=${this.currentSequence}, previousSequence=${this.previousSequence})`); 143 | 144 | return this; 145 | } 146 | 147 | /** 148 | * Increment XRPL account sequence by 149 | * @param steps Increment by number of steps (signed; -1 to decrement by 1) 150 | * @returns {Promise.} 151 | */ 152 | public incrementAccountSequenceBy(steps: number) { 153 | this.currentSequence = this.currentSequence + steps; 154 | 155 | return this; 156 | } 157 | 158 | /** 159 | * Get the current XRPL account sequence 160 | * @returns {Promise.} 161 | */ 162 | public getCurrentAccountSequence(): number { 163 | return this.currentSequence; 164 | } 165 | 166 | public getAddress(): any { 167 | return this.address; 168 | } 169 | 170 | public getSecret(): any { 171 | return this.hasSecret() ? this.secret : undefined; 172 | } 173 | 174 | public getKeypair(): any { 175 | return this.hasKeypair() ? this.keypair : undefined; 176 | } 177 | 178 | public setAddress(address: string) { 179 | this.address = address; 180 | 181 | return this; 182 | } 183 | 184 | public setSecret(secret: string) { 185 | this.secret = secret; 186 | 187 | return this; 188 | } 189 | 190 | public setKeypair(keypair: SologenicTypes.KeyPair) { 191 | this.keypair = keypair; 192 | 193 | return this; 194 | } 195 | 196 | public getAccount(): SologenicTypes.Account { 197 | let account: SologenicTypes.Account = { 198 | address: this.getAddress(), 199 | secret: this.getSecret(), 200 | keypair: this.getKeypair() 201 | }; 202 | 203 | return account; 204 | } 205 | 206 | /** 207 | * Helper method to verify that the account has a keypair 208 | * @returns {boolean} 209 | */ 210 | public hasKeypair(): boolean { 211 | if (typeof (this.keypair) === 'undefined') 212 | return false; 213 | 214 | if ((typeof this.keypair.publicKey !== 'undefined') && (typeof this.keypair.privateKey !== 'undefined')) { 215 | return true; 216 | } 217 | 218 | return false; 219 | } 220 | 221 | /** 222 | * Helper method to verify the account has a secret 223 | * @returns {boolean} 224 | */ 225 | public hasSecret(): boolean { 226 | if (typeof this.secret !== 'undefined' && this.secret !== '') { 227 | return true; 228 | } 229 | 230 | return false; 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [1.0.62](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.61...v1.0.62) (2021-08-13) 6 | 7 | 8 | 9 | ### [1.0.61](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.60...v1.0.61) (2021-08-13) 10 | 11 | 12 | 13 | ### [1.0.60](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.59...v1.0.60) (2021-08-12) 14 | 15 | 16 | 17 | ### [1.0.59](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.58...v1.0.59) (2021-05-18) 18 | 19 | 20 | 21 | ### [1.0.58](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.57...v1.0.58) (2021-05-17) 22 | 23 | 24 | 25 | ### [1.0.57](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.56...v1.0.57) (2021-05-17) 26 | 27 | 28 | 29 | ### [1.0.56](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.55...v1.0.56) (2021-05-14) 30 | 31 | 32 | 33 | ### [1.0.55](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.54...v1.0.55) (2021-05-14) 34 | 35 | 36 | 37 | ### [1.0.54](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.53...v1.0.54) (2021-04-16) 38 | 39 | 40 | 41 | ### [1.0.53](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.52...v1.0.53) (2021-03-25) 42 | 43 | 44 | 45 | ### [1.0.52](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.51...v1.0.52) (2021-03-24) 46 | 47 | 48 | 49 | ### [1.0.51](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.50...v1.0.51) (2021-03-24) 50 | 51 | 52 | 53 | ### [1.0.50](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.49...v1.0.50) (2021-03-24) 54 | 55 | 56 | 57 | ### [1.0.49](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.48...v1.0.49) (2021-03-22) 58 | 59 | 60 | 61 | ### [1.0.48](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.47...v1.0.48) (2021-03-22) 62 | 63 | 64 | 65 | ### [1.0.47](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.46...v1.0.47) (2021-03-22) 66 | 67 | 68 | 69 | ### [1.0.46](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.45...v1.0.46) (2021-03-20) 70 | 71 | 72 | 73 | ### [1.0.45](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.44...v1.0.45) (2021-03-19) 74 | 75 | 76 | 77 | ### [1.0.44](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.43...v1.0.44) (2021-03-17) 78 | 79 | 80 | 81 | ### [1.0.43](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.42...v1.0.43) (2021-03-17) 82 | 83 | 84 | 85 | ### [1.0.42](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.41...v1.0.42) (2021-03-17) 86 | 87 | 88 | 89 | ### [1.0.41](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.40...v1.0.41) (2021-03-17) 90 | 91 | 92 | 93 | ### [1.0.40](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.39...v1.0.40) (2021-03-17) 94 | 95 | 96 | 97 | ### [1.0.39](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.38...v1.0.39) (2021-03-17) 98 | 99 | 100 | 101 | ### [1.0.38](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.36...v1.0.38) (2021-03-17) 102 | 103 | 104 | 105 | ### [1.0.36](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.35...v1.0.36) (2020-06-01) 106 | 107 | 108 | 109 | ### [1.0.35](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.34...v1.0.35) (2020-05-22) 110 | 111 | 112 | 113 | ### [1.0.34](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.33...v1.0.34) (2020-05-22) 114 | 115 | 116 | 117 | ### [1.0.33](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.32...v1.0.33) (2020-05-21) 118 | 119 | 120 | 121 | ### [1.0.32](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.31...v1.0.32) (2020-05-20) 122 | 123 | 124 | 125 | ### [1.0.31](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.30...v1.0.31) (2020-03-04) 126 | 127 | 128 | 129 | ### [1.0.30](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.29...v1.0.30) (2020-02-14) 130 | 131 | 132 | 133 | ### [1.0.29](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.28...v1.0.29) (2020-02-06) 134 | 135 | 136 | 137 | ### [1.0.28](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.27...v1.0.28) (2020-01-31) 138 | 139 | 140 | 141 | ### [1.0.27](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.26...v1.0.27) (2020-01-31) 142 | 143 | 144 | 145 | ### [1.0.26](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.25...v1.0.26) (2020-01-31) 146 | 147 | 148 | 149 | ### [1.0.25](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.24...v1.0.25) (2020-01-31) 150 | 151 | 152 | 153 | ### [1.0.24](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.23...v1.0.24) (2020-01-31) 154 | 155 | 156 | 157 | ### [1.0.23](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.22...v1.0.23) (2020-01-30) 158 | 159 | 160 | 161 | ### [1.0.22](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.21...v1.0.22) (2020-01-30) 162 | 163 | 164 | 165 | ### [1.0.21](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.20...v1.0.21) (2020-01-29) 166 | 167 | 168 | 169 | ### [1.0.20](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.19...v1.0.20) (2020-01-29) 170 | 171 | 172 | 173 | ### [1.0.19](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.18...v1.0.19) (2020-01-29) 174 | 175 | 176 | 177 | ### [1.0.18](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.17...v1.0.18) (2020-01-29) 178 | 179 | 180 | 181 | ### [1.0.17](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.16...v1.0.17) (2020-01-29) 182 | 183 | 184 | 185 | ### [1.0.16](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.15...v1.0.16) (2020-01-29) 186 | 187 | 188 | 189 | ### [1.0.15](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.14...v1.0.15) (2020-01-28) 190 | 191 | 192 | 193 | ### [1.0.14](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.13...v1.0.14) (2020-01-28) 194 | 195 | 196 | 197 | ### [1.0.13](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.12...v1.0.13) (2020-01-15) 198 | 199 | 200 | 201 | ### [1.0.12](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.11...v1.0.12) (2020-01-15) 202 | 203 | 204 | 205 | ### [1.0.11](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.10...v1.0.11) (2020-01-15) 206 | 207 | 208 | 209 | ### [1.0.10](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.9...v1.0.10) (2020-01-15) 210 | 211 | 212 | 213 | ### [1.0.9](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.8...v1.0.9) (2020-01-15) 214 | 215 | 216 | 217 | ### [1.0.8](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.7...v1.0.8) (2019-12-17) 218 | 219 | 220 | 221 | ### [1.0.7](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.6...v1.0.7) (2019-12-06) 222 | 223 | 224 | 225 | ### [1.0.6](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.5...v1.0.6) (2019-12-06) 226 | 227 | 228 | 229 | ### [1.0.5](https://github.com/sologenic/sologenic-xrpl-stream-js/compare/v1.0.4...v1.0.5) (2019-12-04) 230 | 231 | 232 | 233 | ### 1.0.4 (2019-12-03) 234 | 235 | 236 | 237 | # Changelog 238 | 239 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 240 | -------------------------------------------------------------------------------- /src/tests/signing/xumm.spec.ts: -------------------------------------------------------------------------------- 1 | import anyTest, {TestInterface} from 'ava'; 2 | import { SologenicError } from '../../lib/error'; 3 | import { XummSigner } from '../../lib/signing/xumm'; 4 | import XrplAccount from '../../lib/account'; 5 | import * as SologenicTypes from '../../types'; 6 | 7 | const test = anyTest as TestInterface<{ 8 | session: any, 9 | data: any 10 | }>; 11 | 12 | const MOCK_XUMM_SIGN_REQUEST_RESPONSE = ` 13 | { 14 | uuid: '0ea14bb7-6054-44a9-bcfc-5bff6b132261', 15 | next: { 16 | always: 'https://xumm.app/sign/0ea14bb7-6054-44a9-bcfc-5bff6b132261' 17 | }, 18 | refs: { 19 | qr_png: 'https://xumm.app/sign/0ea14bb7-6054-44a9-bcfc-5bff6b132261_q.png', 20 | qr_matrix: 'https://xumm.app/sign/0ea14bb7-6054-44a9-bcfc-5bff6b132261_q.json', 21 | qr_uri_quality_opts: [ 'm', 'q', 'h' ], 22 | websocket_status: 'wss://xumm.app/sign/0ea14bb7-6054-44a9-bcfc-5bff6b132261' 23 | }, 24 | pushed: false 25 | } 26 | `; 27 | 28 | const MOCK_XUMM_VERIFY_REQUEST_RESPONSE = ` 29 | { 30 | meta: { 31 | exists: true, 32 | uuid: '0ea14bb7-6054-44a9-bcfc-5bff6b132261', 33 | multisign: false, 34 | submit: false, 35 | destination: '', 36 | resolved_destination: '', 37 | resolved: true, 38 | signed: true, 39 | cancelled: false, 40 | expired: false, 41 | pushed: false, 42 | app_opened: true, 43 | return_url_app: null, 44 | return_url_web: null 45 | }, 46 | application: { 47 | name: 'sologenic-xrpl-stream-js', 48 | description: 'Sologenic XRPL Stream', 49 | disabled: 0, 50 | uuidv4: 'be29f4b8-7d12-4646-8145-584954fd7b7e', 51 | icon_url: 'https://xumm-cdn.imgix.net/app-logo/e41dc88d-0426-44c3-99a6-a075329e8550.png', 52 | issued_user_token: 'ee9d788d-2de7-4d27-8afd-7829490f21bf' 53 | }, 54 | payload: { 55 | tx_type: 'AccountSet', 56 | tx_destination: '', 57 | tx_destination_tag: null, 58 | request_json: { 59 | Account: 'rEj9X3pz7JCTsAmEqLh4NC6ahk2JbCbVmt', 60 | TransactionType: 'AccountSet', 61 | SetFlag: 5 62 | }, 63 | created_at: '2020-04-25T17:55:07Z', 64 | expires_at: '2020-04-25T17:57:07Z', 65 | expires_in_seconds: 77 66 | }, 67 | response: { 68 | hex: '1200032280000000240000008320210000000568400000000000000C7321028F2278D083264AF4A9098966DD868809908DE98778EA1F8F0620CD9C6DF3C94974473045022100B345FD048E9CC994375B9240A5CD468F0A384C3AD589732431F6B8BA8BDF61E602203CD6F714C8F02F30BAA20525CDF80BBDE6BBE244985CD4DB685D3C1507E9B4918114A18A57CB7B1112537A2D39777590EAE34911222B', 69 | txid: '2F42AB1673FD749099801478137A4D3F1BAE184A64A30219CCB116C051BE7544', 70 | resolved_at: '2020-04-25T17:55:49.000Z', 71 | dispatched_to: 'wss://s2.ripple.com', 72 | dispatched_result: '', 73 | dispatched_nodetype: '', 74 | multisign_account: '', 75 | account: 'rEj9X3pz7JCTsAmEqLh4NC6ahk2JbCbVmt' 76 | }, 77 | custom_meta: { identifier: null, blob: null, instruction: null } 78 | } 79 | `; 80 | 81 | test.skip("signing of a request, with an issued user token (push notification should be received)", async t => { 82 | // This is a visual test, just make sure you receive the push notification on 83 | // your mobile test. Again, we'll eventually use some mocks here. 84 | 85 | /* 86 | XUMM PAYLOAD (with app_url) 87 | { 88 | uuid: '7ab07133-d1f5-49c7-968f-14d03e889bb8', 89 | next: { 90 | always: 'https://xumm.app/sign/7ab07133-d1f5-49c7-968f-14d03e889bb8', 91 | no_push_msg_received: 'https://xumm.app/sign/7ab07133-d1f5-49c7-968f-14d03e889bb8/qr' 92 | }, 93 | refs: { 94 | qr_png: 'https://xumm.app/sign/7ab07133-d1f5-49c7-968f-14d03e889bb8_q.png', 95 | qr_matrix: 'https://xumm.app/sign/7ab07133-d1f5-49c7-968f-14d03e889bb8_q.json', 96 | qr_uri_quality_opts: [ 'm', 'q', 'h' ], 97 | websocket_status: 'wss://xumm.app/sign/7ab07133-d1f5-49c7-968f-14d03e889bb8' 98 | }, 99 | pushed: true 100 | } 101 | */ 102 | 103 | const xs = new XummSigner({ 104 | xummApiKey: process.env.XUMM_API_KEY, 105 | xummApiSecret: process.env.XUMM_API_SECRET, 106 | // Gives us 10 seconds to react as this is a manual test, just so we can verify 107 | // the push notification was received. 108 | maximumExecutionTime: 10000 109 | }); 110 | 111 | const account = new XrplAccount( 112 | "rEj9X3pz7JCTsAmEqLh4NC6ahk2JbCbVmt", 113 | undefined, 114 | "", 115 | ""); 116 | 117 | const tx: SologenicTypes.TX = { 118 | Account: 'rEj9X3pz7JCTsAmEqLh4NC6ahk2JbCbVmt', 119 | TransactionType: 'AccountSet', 120 | SetFlag: 5, 121 | TransactionMetadata: { 122 | xummMeta: { 123 | // This is a test account so I am not worried about the push 124 | // notifications on this device. 125 | issued_user_token: 'ee9d788d-2de7-4d27-8afd-7829490f21bf' 126 | } 127 | } 128 | }; 129 | 130 | try { 131 | // Should immmediately throw an error because we have set our maximum execution time to 1ms 132 | const error = await xs.sign(tx, "1234", account, undefined); 133 | 134 | // Should not get here 135 | t.fail(); 136 | } catch (error) { 137 | // Should throw an exception because our promise times out before we can complete the 138 | // request for testing. We should probably use some mocks to actually test this 139 | // eventually. 140 | t.pass(); 141 | } 142 | }); 143 | 144 | test.skip("signing of a request, without an issued user token (app_url should be manually accessed on mobile device)", async t => { 145 | /* 146 | XUMM PAYLOAD (with app_url) 147 | { 148 | uuid: 'd1a3fb57-96b1-4f8b-8314-411d9fb652aa', 149 | next: { 150 | always: 'https://xumm.app/sign/d1a3fb57-96b1-4f8b-8314-411d9fb652aa' 151 | }, 152 | refs: { 153 | qr_png: 'https://xumm.app/sign/d1a3fb57-96b1-4f8b-8314-411d9fb652aa_q.png', 154 | qr_matrix: 'https://xumm.app/sign/d1a3fb57-96b1-4f8b-8314-411d9fb652aa_q.json', 155 | qr_uri_quality_opts: [ 'm', 'q', 'h' ], 156 | websocket_status: 'wss://xumm.app/sign/d1a3fb57-96b1-4f8b-8314-411d9fb652aa' 157 | }, 158 | pushed: false 159 | } 160 | */ 161 | 162 | const xs = new XummSigner({ 163 | xummApiKey: process.env.XUMM_API_KEY, 164 | xummApiSecret: process.env.XUMM_API_SECRET, 165 | // Gives us 5 seconds to react as this is a manual test 166 | maximumExecutionTime: 5000 167 | }); 168 | 169 | const account = new XrplAccount( 170 | "rEj9X3pz7JCTsAmEqLh4NC6ahk2JbCbVmt", 171 | undefined, 172 | "", 173 | ""); 174 | 175 | const tx: SologenicTypes.TX = { 176 | Account: 'rEj9X3pz7JCTsAmEqLh4NC6ahk2JbCbVmt', 177 | TransactionType: 'AccountSet', 178 | SetFlag: 5 179 | }; 180 | 181 | try { 182 | /* 183 | tx.TransactionMetadata = { 184 | xummMeta: issued_user_token 185 | }; 186 | */ 187 | 188 | // Should immmediately throw an error because we have set our maximum execution time to 1ms 189 | const error = await xs.sign(tx, "1234", account, undefined); 190 | 191 | t.fail(); 192 | } catch (error) { 193 | t.pass(); 194 | } 195 | }); 196 | 197 | test.skip("time out a signing a request with xumm", async t => { 198 | const xs = new XummSigner({ 199 | xummApiKey: process.env.XUMM_API_KEY, 200 | xummApiSecret: process.env.XUMM_API_SECRET, 201 | maximumExecutionTime: 1000 202 | }); 203 | 204 | const account = new XrplAccount( 205 | "rEj9X3pz7JCTsAmEqLh4NC6ahk2JbCbVmt", 206 | undefined, 207 | "", 208 | ""); 209 | 210 | const tx = { 211 | Account: 'rEj9X3pz7JCTsAmEqLh4NC6ahk2JbCbVmt', 212 | TransactionType: 'AccountSet', 213 | SetFlag: 5 214 | }; 215 | 216 | try { 217 | // Should immmediately throw an error because we have set our maximum execution time to 1ms 218 | const error = await xs.sign(tx, "1234", account, undefined); 219 | 220 | // Should not get here 221 | t.fail(); 222 | } catch (error) { 223 | // Should throw an exception 224 | t.pass(); 225 | } 226 | }); 227 | 228 | test.skip("valid signing a request with xumm", async t => { 229 | const xs = new XummSigner({ 230 | xummApiKey: process.env.XUMM_API_KEY, 231 | xummApiSecret: process.env.XUMM_API_SECRET 232 | }); 233 | 234 | const account = new XrplAccount( 235 | "rEj9X3pz7JCTsAmEqLh4NC6ahk2JbCbVmt", 236 | undefined, 237 | "", 238 | ""); 239 | 240 | const tx = { 241 | Account: 'rEj9X3pz7JCTsAmEqLh4NC6ahk2JbCbVmt', 242 | TransactionType: 'AccountSet', 243 | SetFlag: 5 244 | }; 245 | 246 | // Should immmediately throw an error because we have set our maximum execution time to 1ms 247 | await xs.sign(tx, "1234", account, undefined); 248 | 249 | t.pass(); 250 | }); 251 | -------------------------------------------------------------------------------- /src/tests/queues/hash.spec.ts: -------------------------------------------------------------------------------- 1 | import anyTest, {TestInterface} from 'ava'; 2 | import { MQTX, QUEUE_TYPE_STXMQ_HASH } from '../../types/queues'; 3 | import { v4 as uuid } from 'uuid'; 4 | 5 | import TXMQƨ from '../../lib/queues'; 6 | 7 | const test = anyTest as TestInterface<{ 8 | session: TXMQƨ, 9 | data: MQTX, 10 | queue: string 11 | }>; 12 | 13 | test.beforeEach(async t => { 14 | // Before each test create a unique queue 15 | t.context.queue = `queue_${uuid()}`; 16 | t.context.data = { id: uuid(), data: { 17 | message: `Hello, World: ${uuid()}` 18 | }}; 19 | 20 | t.context.session = new TXMQƨ({ 21 | queueType: QUEUE_TYPE_STXMQ_HASH 22 | }); 23 | }); 24 | 25 | test.afterEach(async t => { 26 | // Cleanup after each test 27 | await t.context.session.delAll(t.context.queue); 28 | }); 29 | 30 | test.serial('get all queues (should only be one at a time)', async t => { 31 | var session = t.context.session; 32 | var result = await session.add(t.context.queue, t.context.data); 33 | 34 | t.is(typeof result.id, 'string'); 35 | t.is(typeof result.data, 'object'); 36 | 37 | var response = (await session.get(t.context.queue, result.id)) || { 38 | id: undefined 39 | }; 40 | 41 | t.is(response.id, result.id); 42 | 43 | const queues = await t.context.session.queues(); 44 | t.true(queues.length > 0); 45 | }); 46 | 47 | test.serial('add item to the queue', async t => { 48 | var session = t.context.session; 49 | var result = await session.add(t.context.queue, t.context.data); 50 | 51 | t.is(typeof result.id, 'string'); 52 | t.is(typeof result.data, 'object'); 53 | 54 | var response = (await session.get(t.context.queue, result.id)) || { 55 | id: undefined 56 | }; 57 | 58 | t.is(response.id, result.id); 59 | }); 60 | 61 | test.serial("add item to the queue, with a custom id for", async t => { 62 | var session = t.context.session; 63 | var custom_id = 'foobar'; 64 | 65 | // Expect the the object does not already exist 66 | t.false(await session.del(t.context.queue, custom_id)); 67 | 68 | var result = await session.add(t.context.queue, t.context.data, custom_id); 69 | 70 | t.is(typeof result.id, 'string'); 71 | t.is(typeof result.data, 'object'); 72 | 73 | var response = await session.get(t.context.queue, custom_id); 74 | 75 | if (typeof response === 'undefined') { 76 | t.fail(); 77 | } 78 | 79 | t.is(response!.id, custom_id); 80 | }); 81 | 82 | test.serial('validate attempt to get an invalid object id is undefined', async t => { 83 | var session = t.context.session; 84 | 85 | t.true((await session.get(t.context.queue, 'barfoo')) === undefined); 86 | }); 87 | 88 | test.serial('validate pop an item off the queue', async t => { 89 | var session = t.context.session; 90 | 91 | const result = await session.add(t.context.queue, t.context.data); 92 | 93 | let items = await session.getAll(t.context.queue); 94 | t.is(items.length, 1); 95 | 96 | await session.pop(t.context.queue); 97 | 98 | items = await session.getAll(t.context.queue); 99 | t.is(items.length, 0); 100 | }); 101 | 102 | test.serial('store and retrieve multiple objects', async t => { 103 | try { 104 | var session = t.context.session; 105 | await t.context.session.delAll(t.context.queue); 106 | 107 | var objects: Array = [ 108 | { id: '1', data: [ 'Message 1' ], created: Math.floor(new Date().getTime() / 1000) - 10 }, 109 | { id: '2', data: [ 'Message 2' ], created: Math.floor(new Date().getTime() / 1000) - 0 }, 110 | { id: '3', data: [ 'Message 3' ], created: Math.floor(new Date().getTime() / 1000) - 10 }, 111 | { id: '4', data: [ 'Message 4' ], created: Math.floor(new Date().getTime() / 1000) - 0 }, 112 | ]; 113 | 114 | for (var index in objects) { 115 | const add_result = await session.add(t.context.queue, objects[index]); 116 | t.is(typeof add_result.id, 'string'); 117 | t.is(typeof add_result.data, 'object'); 118 | 119 | const get_result = await session.get(t.context.queue, add_result.id); 120 | 121 | t.is(add_result.id, get_result!.id); 122 | t.deepEqual(add_result.data, get_result!.data); 123 | } 124 | 125 | let items = await session.getAll(t.context.queue); 126 | 127 | t.is(items.length, objects.length); 128 | 129 | } catch (error) { 130 | t.fail(); 131 | } 132 | }); 133 | 134 | test.serial('add and remove (delete) multiple objects from queue', async t => { 135 | try { 136 | var session = t.context.session; 137 | await t.context.session.delAll(t.context.queue); 138 | 139 | var objects: Array = [ 140 | { id: '1', data: [ 'Message 1' ], created: Math.floor(new Date().getTime() / 1000) - 901 }, 141 | { id: '2', data: [ 'Message 2' ], created: Math.floor(new Date().getTime() / 1000) - 0 }, 142 | { id: '3', data: [ 'Message 3' ], created: Math.floor(new Date().getTime() / 1000) - 901 }, 143 | { id: '4', data: [ 'Message 4' ], created: Math.floor(new Date().getTime() / 1000) - 0 }, 144 | { id: '5', data: [ 'Message 5' ], created: Math.floor(new Date().getTime() / 1000) - 0 }, 145 | { id: '6', data: [ 'Message 6' ], created: Math.floor(new Date().getTime() / 1000) - 0 }, 146 | ]; 147 | 148 | // Verify our queue is empty 149 | let items = await session.getAll(t.context.queue); 150 | 151 | t.true(items.length === 0); 152 | 153 | // Add each element to the queue 154 | for (var index in objects) { 155 | let object_ = await session.add(t.context.queue, objects[index]); 156 | t.is(typeof object_, 'object'); 157 | 158 | // Verify the object is returned from the queue 159 | var result_ = await session.get(t.context.queue, object_.id); 160 | t.is(typeof result_, 'object'); 161 | 162 | t.deepEqual(object_, result_); 163 | } 164 | 165 | // Delete all items older than 900 seconds (we expect that we'll delete two) 166 | let deleted_ = await session.deleteOlderThan(900, t.context.queue); 167 | let items_ = await session.getAll(t.context.queue); 168 | 169 | // Expect that the number of items in the queue is now equal to the initial object 170 | // count minus the deleted_ items count 171 | t.is(items_.length, objects.length - deleted_); 172 | 173 | // Pop an item from the queue (since we should have only deleted two items) 174 | // we should have another 4 items on the queue still 175 | t.is(typeof(await session.pop(t.context.queue)), 'object'); 176 | 177 | items_ = await session.getAll(t.context.queue); 178 | t.true(await session.del(t.context.queue, items_[0].id)); 179 | 180 | // Delete the remaining items, since all items should be gone now 181 | items_ = await session.getAll(t.context.queue); 182 | t.true(items_.length > 0); 183 | t.true(await session.delAll(t.context.queue)); 184 | 185 | // Delete an item 186 | t.false(await session.del(t.context.queue, items_[0].id)); 187 | 188 | // Pop an item off the queue 189 | t.is(await session.pop(t.context.queue), undefined); 190 | 191 | } catch (error) { 192 | t.fail(); 193 | } 194 | }); 195 | 196 | test.serial('delete items older than 900 seconds', async t => { 197 | var session = t.context.session; 198 | await t.context.session.delAll(t.context.queue); 199 | 200 | var objects: Array = [ 201 | { id: '1', data: [ 'Message 1' ], created: Math.floor(new Date().getTime() / 1000) - 901 }, 202 | { id: '2', data: [ 'Message 2' ], created: Math.floor(new Date().getTime() / 1000) - 0 }, 203 | { id: '3', data: [ 'Message 3' ], created: Math.floor(new Date().getTime() / 1000) - 901 }, 204 | { id: '4', data: [ 'Message 4' ], created: Math.floor(new Date().getTime() / 1000) - 0 }, 205 | { id: '5', data: [ 'Message 5' ], created: Math.floor(new Date().getTime() / 1000) - 0 }, 206 | { id: '6', data: [ 'Message 6' ], created: Math.floor(new Date().getTime() / 1000) - 0 }, 207 | ]; 208 | 209 | // Add each element to the queue 210 | for (var index in objects) { 211 | let object_ = await session.add(t.context.queue, objects[index]); 212 | t.is(typeof object_, 'object'); 213 | 214 | // Verify the object is returned from the queue 215 | var result_ = await session.get(t.context.queue, object_.id); 216 | t.is(typeof result_, 'object'); 217 | 218 | t.deepEqual(object_, result_); 219 | } 220 | 221 | // Verify our queue is equal to the same number of objects we have 222 | let items = await session.getAll(t.context.queue); 223 | 224 | t.true(items.length === objects.length); 225 | 226 | // Delete entries older than 900 seconds 227 | const deleted_ = await t.context.session.deleteOlderThan(900, t.context.queue); 228 | 229 | // Get all items in our queue 230 | items = await session.getAll(t.context.queue); 231 | 232 | t.is(deleted_, 2); 233 | t.true(items.length === objects.length - deleted_); 234 | }); 235 | -------------------------------------------------------------------------------- /src/tests/queues/redis.spec.ts: -------------------------------------------------------------------------------- 1 | import anyTest, {TestInterface} from 'ava'; 2 | import { RippleAPIOptions } from '../../types/xrpl'; 3 | import { MQTX, TransactionHandlerOptions, QUEUE_TYPE_STXMQ_REDIS } from '../../types/queues'; 4 | import { v4 as uuid } from 'uuid'; 5 | 6 | import TXMQƨ from '../../lib/queues'; 7 | 8 | const test = anyTest as TestInterface<{ 9 | session: TXMQƨ, 10 | data: MQTX, 11 | queue: string 12 | }>; 13 | 14 | test.beforeEach(async t => { 15 | // Before each test create a unique queue 16 | t.context.queue = `queue_${uuid()}`; 17 | t.context.data = { id: uuid(), data: { 18 | message: `Hello, World: ${uuid()}` 19 | }}; 20 | 21 | t.context.session = new TXMQƨ({ 22 | queueType: QUEUE_TYPE_STXMQ_REDIS 23 | }); 24 | }); 25 | 26 | test.afterEach(async t => { 27 | // Cleanup after each test 28 | await t.context.session.delAll(t.context.queue); 29 | }); 30 | 31 | test.serial('get all queues (should only be one at a time)', async t => { 32 | var session = t.context.session; 33 | var result = await session.add(t.context.queue, t.context.data); 34 | 35 | t.is(typeof result.id, 'string'); 36 | t.is(typeof result.data, 'object'); 37 | 38 | var response = (await session.get(t.context.queue, result.id)) || { 39 | id: undefined 40 | }; 41 | 42 | t.is(response.id, result.id); 43 | 44 | const queues = await t.context.session.queues(); 45 | t.true(queues.length > 0); 46 | }); 47 | 48 | test.serial('add item to the queue', async t => { 49 | var session = t.context.session; 50 | var result = await session.add(t.context.queue, t.context.data); 51 | 52 | t.is(typeof result.id, 'string'); 53 | t.is(typeof result.data, 'object'); 54 | 55 | var response = (await session.get(t.context.queue, result.id)) || { 56 | id: undefined 57 | }; 58 | 59 | t.is(response.id, result.id); 60 | }); 61 | 62 | test.serial("add item to the queue, with a custom id for", async t => { 63 | var session = t.context.session; 64 | var custom_id = 'foobar'; 65 | 66 | // Expect the the object does not already exist 67 | t.false(await session.del(t.context.queue, custom_id)); 68 | 69 | var result = await session.add(t.context.queue, t.context.data, custom_id); 70 | 71 | t.is(typeof result.id, 'string'); 72 | t.is(typeof result.data, 'object'); 73 | 74 | var response = await session.get(t.context.queue, custom_id); 75 | 76 | if (typeof response === 'undefined') { 77 | t.fail(); 78 | } 79 | 80 | t.is(response!.id, custom_id); 81 | }); 82 | 83 | test.serial('validate attempt to get an invalid object id is undefined', async t => { 84 | var session = t.context.session; 85 | 86 | t.true((await session.get(t.context.queue, 'barfoo')) === undefined); 87 | }); 88 | 89 | test.serial('validate pop an item off the queue', async t => { 90 | var session = t.context.session; 91 | 92 | const result = await session.add(t.context.queue, t.context.data); 93 | 94 | let items = await session.getAll(t.context.queue); 95 | t.is(items.length, 1); 96 | 97 | await session.pop(t.context.queue); 98 | 99 | items = await session.getAll(t.context.queue); 100 | t.is(items.length, 0); 101 | }); 102 | 103 | test.serial('store and retrieve multiple objects', async t => { 104 | try { 105 | var session = t.context.session; 106 | await t.context.session.delAll(t.context.queue); 107 | 108 | var objects: Array = [ 109 | { id: '1', data: [ 'Message 1' ], created: Math.floor(new Date().getTime() / 1000) - 10 }, 110 | { id: '2', data: [ 'Message 2' ], created: Math.floor(new Date().getTime() / 1000) - 0 }, 111 | { id: '3', data: [ 'Message 3' ], created: Math.floor(new Date().getTime() / 1000) - 10 }, 112 | { id: '4', data: [ 'Message 4' ], created: Math.floor(new Date().getTime() / 1000) - 0 }, 113 | ]; 114 | 115 | for (var index in objects) { 116 | const add_result = await session.add(t.context.queue, objects[index]); 117 | t.is(typeof add_result.id, 'string'); 118 | t.is(typeof add_result.data, 'object'); 119 | 120 | const get_result = await session.get(t.context.queue, add_result.id); 121 | 122 | t.is(add_result.id, get_result!.id); 123 | t.deepEqual(add_result.data, get_result!.data); 124 | } 125 | 126 | let items = await session.getAll(t.context.queue); 127 | 128 | t.is(items.length, objects.length); 129 | 130 | } catch (error) { 131 | t.fail(); 132 | } 133 | }); 134 | 135 | test.serial('add and remove (delete) multiple objects from queue', async t => { 136 | try { 137 | var session = t.context.session; 138 | await t.context.session.delAll(t.context.queue); 139 | 140 | var objects: Array = [ 141 | { id: '1', data: [ 'Message 1' ], created: Math.floor(new Date().getTime() / 1000) - 901 }, 142 | { id: '2', data: [ 'Message 2' ], created: Math.floor(new Date().getTime() / 1000) - 0 }, 143 | { id: '3', data: [ 'Message 3' ], created: Math.floor(new Date().getTime() / 1000) - 901 }, 144 | { id: '4', data: [ 'Message 4' ], created: Math.floor(new Date().getTime() / 1000) - 0 }, 145 | { id: '5', data: [ 'Message 5' ], created: Math.floor(new Date().getTime() / 1000) - 0 }, 146 | { id: '6', data: [ 'Message 6' ], created: Math.floor(new Date().getTime() / 1000) - 0 }, 147 | ]; 148 | 149 | // Verify our queue is empty 150 | let items = await session.getAll(t.context.queue); 151 | 152 | t.true(items.length === 0); 153 | 154 | // Add each element to the queue 155 | for (var index in objects) { 156 | let object_ = await session.add(t.context.queue, objects[index]); 157 | t.is(typeof object_, 'object'); 158 | 159 | // Verify the object is returned from the queue 160 | var result_ = await session.get(t.context.queue, object_.id); 161 | t.is(typeof result_, 'object'); 162 | 163 | t.deepEqual(object_, result_); 164 | } 165 | 166 | // Delete all items older than 900 seconds (we expect that we'll delete two) 167 | let deleted_ = await session.deleteOlderThan(900, t.context.queue); 168 | let items_ = await session.getAll(t.context.queue); 169 | 170 | // Expect that the number of items in the queue is now equal to the initial object 171 | // count minus the deleted_ items count 172 | t.is(items_.length, objects.length - deleted_); 173 | 174 | // Pop an item from the queue (since we should have only deleted two items) 175 | // we should have another 4 items on the queue still 176 | t.is(typeof(await session.pop(t.context.queue)), 'object'); 177 | 178 | items_ = await session.getAll(t.context.queue); 179 | t.true(await session.del(t.context.queue, items_[0].id)); 180 | 181 | // Delete the remaining items, since all items should be gone now 182 | items_ = await session.getAll(t.context.queue); 183 | t.true(items_.length > 0); 184 | t.true(await session.delAll(t.context.queue)); 185 | 186 | // Delete an item 187 | t.false(await session.del(t.context.queue, items_[0].id)); 188 | 189 | // Pop an item off the queue 190 | t.is(await session.pop(t.context.queue), undefined); 191 | 192 | } catch (error) { 193 | t.fail(); 194 | } 195 | }); 196 | 197 | test.serial('delete items older than 900 seconds', async t => { 198 | var session = t.context.session; 199 | await t.context.session.delAll(t.context.queue); 200 | 201 | var objects: Array = [ 202 | { id: '1', data: [ 'Message 1' ], created: Math.floor(new Date().getTime() / 1000) - 901 }, 203 | { id: '2', data: [ 'Message 2' ], created: Math.floor(new Date().getTime() / 1000) - 0 }, 204 | { id: '3', data: [ 'Message 3' ], created: Math.floor(new Date().getTime() / 1000) - 901 }, 205 | { id: '4', data: [ 'Message 4' ], created: Math.floor(new Date().getTime() / 1000) - 0 }, 206 | { id: '5', data: [ 'Message 5' ], created: Math.floor(new Date().getTime() / 1000) - 0 }, 207 | { id: '6', data: [ 'Message 6' ], created: Math.floor(new Date().getTime() / 1000) - 0 }, 208 | ]; 209 | 210 | // Add each element to the queue 211 | for (var index in objects) { 212 | let object_ = await session.add(t.context.queue, objects[index]); 213 | t.is(typeof object_, 'object'); 214 | 215 | // Verify the object is returned from the queue 216 | var result_ = await session.get(t.context.queue, object_.id); 217 | t.is(typeof result_, 'object'); 218 | 219 | t.deepEqual(object_, result_); 220 | } 221 | 222 | // Verify our queue is equal to the same number of objects we have 223 | let items = await session.getAll(t.context.queue); 224 | 225 | t.true(items.length === objects.length); 226 | 227 | // Delete entries older than 900 seconds 228 | const deleted_ = await t.context.session.deleteOlderThan(900, t.context.queue); 229 | 230 | // Get all items in our queue 231 | items = await session.getAll(t.context.queue); 232 | 233 | t.is(deleted_, 2); 234 | t.true(items.length === objects.length - deleted_); 235 | }); 236 | -------------------------------------------------------------------------------- /src/lib/signing/xumm_signer.ts: -------------------------------------------------------------------------------- 1 | import XrplAccount from '../account'; 2 | import * as SologenicTypes from '../../types'; 3 | import { SologenicTxSigner } from './index'; 4 | import { SologenicError } from '../error'; 5 | import { getToken, httpRequest, wait } from '../utils'; 6 | import { XummWalletSignerSubmitPayload } from '../../types/api_signer'; 7 | import moment from 'moment'; 8 | 9 | export class XummWalletSigner extends SologenicTxSigner { 10 | protected server_url: string = ''; 11 | protected container_id: string = ''; 12 | protected address: string = ''; 13 | protected signing_refs: any; 14 | protected fallback_container_id: string = ''; 15 | protected is_mobile: boolean = false; 16 | signerID: string = 'xumm'; 17 | 18 | constructor(options: any) { 19 | super(options); 20 | 21 | if (options.hasOwnProperty('server')) { 22 | this.server_url = options['server']; 23 | } else { 24 | throw new Error('Server url missing.'); 25 | } 26 | 27 | if (options.hasOwnProperty('container_id')) { 28 | this.container_id = options['container_id']; 29 | } else { 30 | throw new Error('Container ID missing.'); 31 | } 32 | 33 | if (options.hasOwnProperty('fallback_container_id')) { 34 | this.fallback_container_id = options['fallback_container_id']; 35 | } else { 36 | throw new Error('Fallback container ID missing.'); 37 | } 38 | 39 | if (options.hasOwnProperty('is_mobile')) { 40 | this.is_mobile = options['is_mobile']; 41 | } 42 | 43 | this.includeSequence = true; 44 | } 45 | 46 | async requestConnection(): Promise { 47 | const tx_json = { 48 | TransactionType: 'SignIn' 49 | // TransactionKind: 'SignIn' 50 | }; 51 | 52 | const connectionRefs = await httpRequest( 53 | this.server_url + 'xumm/payload', 54 | 'post', 55 | {}, 56 | JSON.stringify({ 57 | tx_json: tx_json, 58 | options: { 59 | expires_at: moment() 60 | .add(10, 'm') 61 | .toISOString() 62 | } 63 | }) 64 | ); 65 | 66 | var signed_tx: any; 67 | 68 | if (connectionRefs.refs) { 69 | let qrCode = document.createElement('img'); 70 | qrCode.setAttribute('src', connectionRefs.refs.qr); 71 | qrCode.setAttribute('alt', 'QR Code'); 72 | 73 | let container: any = document.getElementById(this.container_id); 74 | container.appendChild(qrCode); 75 | 76 | if (this.is_mobile) { 77 | let deepLink = document.createElement('a'); 78 | deepLink.setAttribute('href', connectionRefs.refs.deeplink); 79 | deepLink.setAttribute('target', '_blank'); 80 | deepLink.setAttribute('rel', 'noopener noreferrer'); 81 | deepLink.innerText = 'XUMM Wallet >'; 82 | 83 | container.appendChild(deepLink); 84 | } 85 | 86 | const socket: WebSocket = new WebSocket(connectionRefs.refs.ws); 87 | var isSocketOpen = false; 88 | const expires_at = moment() 89 | .add(10, 'm') 90 | .valueOf(); 91 | 92 | var socket_interval: any = null; 93 | 94 | var socketResponse = { 95 | updated: false, 96 | data: null 97 | }; 98 | 99 | socket.addEventListener('open', () => { 100 | isSocketOpen = true; 101 | socket_interval = setInterval(() => { 102 | socket.send('ping'); 103 | }, 5000); 104 | }); 105 | 106 | socket.addEventListener('message', message => { 107 | if (message.data !== 'pong') { 108 | const { data } = message; 109 | 110 | if (JSON.parse(data).hasOwnProperty('signed')) { 111 | socketResponse = { 112 | updated: true, 113 | data: JSON.parse(message.data) 114 | }; 115 | } 116 | } 117 | }); 118 | 119 | socket.onerror = event => { 120 | throw new Error('WebSocket error: ' + event); 121 | }; 122 | 123 | while (true) { 124 | if (moment().valueOf() > expires_at) { 125 | if (isSocketOpen) { 126 | socket.close(); 127 | clearInterval(socket_interval); 128 | throw new Error('Timed out. No response from server.'); 129 | } else { 130 | throw new Error('Unable to establish connection with the server.'); 131 | } 132 | } 133 | 134 | if (socketResponse.updated) { 135 | socket.close(); 136 | clearInterval(socket_interval); 137 | break; 138 | } 139 | 140 | await wait(1000); 141 | } 142 | 143 | if (socketResponse.updated) { 144 | let data: any = socketResponse.data; 145 | 146 | if (data.hasOwnProperty('signed') && !data.signed) { 147 | throw new SologenicError('2004'); 148 | } 149 | 150 | signed_tx = await this.checkForSigned( 151 | this.server_url, 152 | connectionRefs.meta.identifier 153 | ); 154 | } 155 | } 156 | 157 | this.address = signed_tx.signer; 158 | 159 | return { 160 | address: signed_tx.signer 161 | }; 162 | } 163 | 164 | async sign( 165 | txJson: SologenicTypes.TX, 166 | txId: string, 167 | _account?: XrplAccount, 168 | _signingOptions: any = {} 169 | ): Promise { 170 | try { 171 | // Delete the transaction metadata if it exists since the signing will fail 172 | // as this TransactionMetadata is not known to the schema. 173 | if (txJson.TransactionMetadata) delete txJson.TransactionMetadata; 174 | 175 | if (txJson.LastLedgerSequence) 176 | txJson.LastLedgerSequence = Number(txJson.LastLedgerSequence) + 100; 177 | 178 | var pushToken = getToken(txJson.Account, 'xumm'); 179 | 180 | const tx_init = await httpRequest( 181 | this.server_url + 'xumm/payload', 182 | 'post', 183 | {}, 184 | JSON.stringify({ 185 | tx_json: txJson, 186 | options: { 187 | submit: false, 188 | ...(pushToken ? { push_token: pushToken } : {}), 189 | expires_at: moment() 190 | .add(10, 'm') 191 | .toISOString() 192 | } 193 | }) 194 | ); 195 | 196 | var signed_tx: any; 197 | 198 | if (tx_init.refs) { 199 | this.signing_refs = tx_init.refs; 200 | 201 | const socket: WebSocket = new WebSocket(tx_init.refs.ws); 202 | var isSocketOpen = false; 203 | const expires_at = moment() 204 | .add(10, 'm') 205 | .valueOf(); 206 | 207 | var socket_interval: any = null; 208 | 209 | var socketResponse = { 210 | updated: false, 211 | data: null 212 | }; 213 | 214 | socket.addEventListener('open', () => { 215 | isSocketOpen = true; 216 | socket_interval = setInterval(() => { 217 | socket.send('ping'); 218 | }, 5000); 219 | }); 220 | 221 | socket.addEventListener('message', message => { 222 | if (message.data !== 'pong') { 223 | const { data } = message; 224 | 225 | if (JSON.parse(data).hasOwnProperty('signed')) { 226 | socketResponse = { 227 | updated: true, 228 | data: JSON.parse(message.data) 229 | }; 230 | } 231 | } 232 | }); 233 | 234 | socket.onerror = event => { 235 | throw new Error('WebSocket error: ' + event); 236 | }; 237 | 238 | while (true) { 239 | if (moment().valueOf() > expires_at) { 240 | if (isSocketOpen) { 241 | socket.close(); 242 | clearInterval(socket_interval); 243 | throw new Error('Timed out. No response from server.'); 244 | } else { 245 | throw new Error( 246 | 'Unable to establish connection with the server.' 247 | ); 248 | } 249 | } 250 | 251 | if (this.cancelled) { 252 | if (isSocketOpen) { 253 | socket.close(); 254 | clearInterval(socket_interval); 255 | } 256 | 257 | this.cancelled = false; 258 | throw new SologenicError('2005'); 259 | } 260 | 261 | if (socketResponse.updated) { 262 | socket.close(); 263 | clearInterval(socket_interval); 264 | break; 265 | } 266 | 267 | await wait(1000); 268 | } 269 | 270 | if (socketResponse.updated) { 271 | let data: any = socketResponse.data; 272 | 273 | if (data.hasOwnProperty('signed') && !data.signed) { 274 | throw new SologenicError('2003'); 275 | } 276 | 277 | signed_tx = await this.checkForSigned( 278 | this.server_url, 279 | data.payload_uuidv4 280 | ); 281 | } 282 | } 283 | 284 | this.signing_refs = ''; 285 | 286 | if (signed_tx.tx_hex) { 287 | return { 288 | id: txId, 289 | signedTransaction: signed_tx.tx_hex 290 | }; 291 | } else { 292 | throw new SologenicError('1000'); 293 | } 294 | } catch (e) { 295 | if (e.message === 'connection_error') { 296 | throw new SologenicError('1003'); 297 | } 298 | 299 | throw new Error(e.message); 300 | } 301 | } 302 | 303 | showSigningQRcode() { 304 | let qrCode = document.createElement('img'); 305 | qrCode.setAttribute('src', this.signing_refs.qr); 306 | qrCode.setAttribute('alt', 'QR Code'); 307 | 308 | let container: any = document.getElementById(this.fallback_container_id); 309 | container.appendChild(qrCode); 310 | 311 | if (this.is_mobile) { 312 | let deepLink = document.createElement('a'); 313 | deepLink.setAttribute('href', this.signing_refs.deeplink); 314 | deepLink.setAttribute('target', '_blank'); 315 | deepLink.setAttribute('rel', 'noopener noreferrer'); 316 | deepLink.innerText = 'XUMM Wallet >'; 317 | 318 | container.appendChild(deepLink); 319 | } 320 | } 321 | 322 | async checkForSigned(url: string, id: string) { 323 | let waitTime = 100; 324 | 325 | while (true) { 326 | try { 327 | const signedTx = await httpRequest( 328 | url + 'xumm/payload/' + id, 329 | 'get', 330 | {}, 331 | '' 332 | ); 333 | 334 | if (signedTx.hasOwnProperty('signer')) { 335 | if (sessionStorage.mode && sessionStorage.mode === '_testnet') { 336 | localStorage.xummToken_testnet = JSON.stringify({ 337 | push_token: signedTx.meta.push_token, 338 | signer: signedTx.signer 339 | }); 340 | } else { 341 | localStorage.xummToken = JSON.stringify({ 342 | push_token: signedTx.meta.push_token, 343 | signer: signedTx.signer 344 | }); 345 | } 346 | 347 | return signedTx; 348 | } 349 | 350 | if ( 351 | signedTx.hasOwnProperty('meta') && 352 | !signedTx.meta.signed && 353 | waitTime < 1000 354 | ) { 355 | waitTime *= 2; 356 | throw new Error('try again'); 357 | } else { 358 | throw new Error('throw unspecified error'); 359 | } 360 | } catch (e) { 361 | if (e === 'throw error') { 362 | throw new SologenicError('1001'); 363 | } 364 | 365 | if (e.message === 'Error: Request failed with status code 500') { 366 | throw new SologenicError('1003'); 367 | } 368 | 369 | if (e === 'try again') { 370 | await wait(waitTime); 371 | } 372 | } 373 | } 374 | } 375 | } 376 | -------------------------------------------------------------------------------- /src/lib/signing/solo_signer.ts: -------------------------------------------------------------------------------- 1 | import XrplAccount from '../account'; 2 | import * as SologenicTypes from '../../types'; 3 | import { SologenicTxSigner } from './index'; 4 | import { SologenicError } from '../error'; 5 | import { httpRequest, wait, getToken } from '../utils'; 6 | import { SoloWalletSignerSubmitPayload } from '../../types/api_signer'; 7 | import moment from 'moment'; 8 | 9 | export class SoloWalletSigner extends SologenicTxSigner { 10 | protected server_url: string = ''; 11 | protected container_id: string = ''; 12 | protected address: string = ''; 13 | protected signing_refs: any; 14 | protected fallback_container_id: string = ''; 15 | protected is_mobile: boolean = false; 16 | protected deeplink_url: string = ''; 17 | 18 | signerID: string = 'solo_wallet'; 19 | 20 | constructor(options: any) { 21 | super(options); 22 | 23 | if (options.hasOwnProperty('server')) { 24 | this.server_url = options['server']; 25 | } else { 26 | throw new Error('Server url missing.'); 27 | } 28 | 29 | if (options.hasOwnProperty('container_id')) { 30 | this.container_id = options['container_id']; 31 | } else { 32 | throw new Error('Container ID missing.'); 33 | } 34 | 35 | if (options.hasOwnProperty('fallback_container_id')) { 36 | this.fallback_container_id = options['fallback_container_id']; 37 | } else { 38 | throw new Error('Fallback container ID missing.'); 39 | } 40 | 41 | if (options.hasOwnProperty('is_mobile')) { 42 | this.is_mobile = options['is_mobile']; 43 | } 44 | 45 | if (options.hasOwnProperty('deeplink_url')) { 46 | this.deeplink_url = options['deeplink_url']; 47 | } 48 | 49 | this.includeSequence = true; 50 | } 51 | 52 | async requestConnection(): Promise { 53 | const tx_json = { 54 | TransactionType: 'NickNameSet', 55 | TransactionKind: 'SignIn' 56 | }; 57 | 58 | const connectionRefs = await httpRequest( 59 | this.server_url + 'issuer/transactions', 60 | 'post', 61 | {}, 62 | JSON.stringify({ 63 | tx_json: tx_json, 64 | options: { 65 | expires_at: moment() 66 | .add(10, 'm') 67 | .toISOString() 68 | } 69 | }) 70 | ); 71 | 72 | var signed_tx: any; 73 | 74 | if (connectionRefs.refs) { 75 | let qrCode = document.createElement('img'); 76 | qrCode.setAttribute('src', connectionRefs.refs.qr); 77 | qrCode.setAttribute('alt', 'QR Code'); 78 | 79 | let container: any = document.getElementById(this.container_id); 80 | 81 | container.appendChild(qrCode); 82 | 83 | if (this.is_mobile && this.deeplink_url) { 84 | let deepLink = document.createElement('a'); 85 | 86 | deepLink.setAttribute('href', connectionRefs.refs.deeplink); 87 | deepLink.setAttribute('target', '_blank'); 88 | deepLink.setAttribute('rel', 'noopener noreferrer'); 89 | deepLink.innerText = 'SOLO Wallet >'; 90 | 91 | container.appendChild(deepLink); 92 | } 93 | 94 | const socket: WebSocket = new WebSocket(connectionRefs.refs.ws); 95 | var isSocketOpen = false; 96 | const expires_at = moment() 97 | .add(10, 'm') 98 | .valueOf(); 99 | 100 | var socket_interval: any = null; 101 | 102 | var socketResponse = { 103 | updated: false, 104 | data: null 105 | }; 106 | 107 | socket.addEventListener('open', () => { 108 | isSocketOpen = true; 109 | socket_interval = setInterval(() => { 110 | socket.send('ping'); 111 | }, 5000); 112 | }); 113 | 114 | socket.addEventListener('message', message => { 115 | if (message.data !== 'pong') { 116 | const { data } = message; 117 | 118 | if ( 119 | JSON.parse(data).hasOwnProperty('meta') && 120 | (JSON.parse(data).meta.hasOwnProperty('signed') || 121 | JSON.parse(data).meta.hasOwnProperty('cancelled')) 122 | ) { 123 | socketResponse = { 124 | updated: true, 125 | data: JSON.parse(message.data) 126 | }; 127 | } 128 | } 129 | }); 130 | 131 | socket.onerror = event => { 132 | throw new Error('WebSocket error: ' + event); 133 | }; 134 | 135 | while (true) { 136 | if (moment().valueOf() > expires_at) { 137 | if (isSocketOpen) { 138 | socket.close(); 139 | clearInterval(socket_interval); 140 | throw new Error('Timed out. No response from server.'); 141 | } else { 142 | throw new Error('Unable to establish connection with the server.'); 143 | } 144 | } 145 | 146 | if (socketResponse.updated) { 147 | socket.close(); 148 | clearInterval(socket_interval); 149 | break; 150 | } 151 | 152 | await wait(1000); 153 | } 154 | 155 | if (socketResponse.updated) { 156 | let data: any = socketResponse.data; 157 | let meta: any = data.meta; 158 | 159 | if (meta.hasOwnProperty('cancelled') && meta.cancelled) { 160 | throw new SologenicError('2004'); 161 | } 162 | 163 | signed_tx = await httpRequest( 164 | this.server_url + 165 | 'issuer/transactions/' + 166 | connectionRefs.meta.identifier, 167 | 'get', 168 | {}, 169 | '' 170 | ); 171 | 172 | if (sessionStorage.mode && sessionStorage.mode === '_testnet') { 173 | localStorage.swToken_testnet = JSON.stringify({ 174 | push_token: meta.push_token, 175 | signer: signed_tx.signer 176 | }); 177 | } else { 178 | localStorage.swToken = JSON.stringify({ 179 | push_token: meta.push_token, 180 | signer: signed_tx.signer 181 | }); 182 | } 183 | } 184 | } 185 | 186 | this.address = signed_tx.signer ? signed_tx.signer : ''; 187 | 188 | return { 189 | address: signed_tx.signer ? signed_tx.signer : null 190 | }; 191 | } 192 | 193 | async sign( 194 | txJson: SologenicTypes.TX, 195 | txId: string, 196 | _account?: XrplAccount, 197 | _signingOptions: any = {} 198 | ): Promise { 199 | try { 200 | // Delete the transaction metadata if it exists since the signing will fail 201 | // as this TransactionMetadata is not known to the schema. 202 | if (txJson.TransactionMetadata) delete txJson.TransactionMetadata; 203 | 204 | if (txJson.LastLedgerSequence) 205 | txJson.LastLedgerSequence = Number(txJson.LastLedgerSequence) + 100; 206 | 207 | var pushToken = getToken(txJson.Account, 'solo'); 208 | 209 | const tx_init = await httpRequest( 210 | this.server_url + 'issuer/transactions', 211 | 'post', 212 | {}, 213 | JSON.stringify({ 214 | tx_json: txJson, 215 | options: { 216 | ...(pushToken ? { push_token: pushToken } : {}), 217 | expires_at: moment() 218 | .add(10, 'm') 219 | .toISOString() 220 | } 221 | }) 222 | ); 223 | 224 | var signed_tx: any; 225 | 226 | if (tx_init.refs) { 227 | this.signing_refs = tx_init; 228 | 229 | const socket: WebSocket = new WebSocket(tx_init.refs.ws); 230 | var isSocketOpen = false; 231 | const expires_at = moment() 232 | .add(10, 'm') 233 | .valueOf(); 234 | 235 | var socket_interval: any = null; 236 | 237 | var socketResponse = { 238 | updated: false, 239 | data: null 240 | }; 241 | 242 | socket.addEventListener('open', () => { 243 | isSocketOpen = true; 244 | socket_interval = setInterval(() => { 245 | socket.send('ping'); 246 | }, 5000); 247 | }); 248 | 249 | socket.addEventListener('message', message => { 250 | if (message.data !== 'pong') { 251 | const { data } = message; 252 | 253 | if ( 254 | JSON.parse(data).hasOwnProperty('meta') && 255 | (JSON.parse(data).meta.hasOwnProperty('signed') || 256 | JSON.parse(data).meta.hasOwnProperty('cancelled')) 257 | ) { 258 | socketResponse = { 259 | updated: true, 260 | data: JSON.parse(message.data) 261 | }; 262 | } 263 | } 264 | }); 265 | 266 | socket.onerror = event => { 267 | throw new Error('WebSocket error: ' + event); 268 | }; 269 | 270 | while (true) { 271 | if (moment().valueOf() > expires_at) { 272 | if (isSocketOpen) { 273 | socket.close(); 274 | clearInterval(socket_interval); 275 | throw new Error('Timed out. No response from server.'); 276 | } else { 277 | throw new Error( 278 | 'Unable to establish connection with the server.' 279 | ); 280 | } 281 | } 282 | 283 | if (this.cancelled) { 284 | if (isSocketOpen) { 285 | socket.close(); 286 | clearInterval(socket_interval); 287 | } 288 | 289 | this.cancelled = false; 290 | throw new SologenicError('2005'); 291 | } 292 | 293 | if (socketResponse.updated) { 294 | socket.close(); 295 | clearInterval(socket_interval); 296 | break; 297 | } 298 | 299 | await wait(1000); 300 | } 301 | 302 | if (socketResponse.updated) { 303 | let data: any = socketResponse.data; 304 | let meta: any = data.meta; 305 | 306 | if (meta.hasOwnProperty('cancelled') && meta.cancelled === true) { 307 | throw new SologenicError('2003'); 308 | } 309 | 310 | signed_tx = await httpRequest( 311 | this.server_url + 'issuer/transactions/' + tx_init.meta.identifier, 312 | 'get', 313 | {}, 314 | '' 315 | ); 316 | 317 | if (sessionStorage.mode && sessionStorage.mode === '_testnet') { 318 | localStorage.swToken_testnet = JSON.stringify({ 319 | push_token: meta.push_token, 320 | signer: signed_tx.signer 321 | }); 322 | } else { 323 | localStorage.swToken = JSON.stringify({ 324 | push_token: meta.push_token, 325 | signer: signed_tx.signer 326 | }); 327 | } 328 | } 329 | } 330 | 331 | this.signing_refs = ''; 332 | 333 | if (signed_tx.tx_hex) { 334 | return { 335 | id: txId, 336 | signedTransaction: signed_tx.tx_hex 337 | }; 338 | } else { 339 | throw new SologenicError('1000'); 340 | } 341 | } catch (e) { 342 | throw new Error(e.message); 343 | } 344 | } 345 | 346 | showSigningQRcode() { 347 | let qrCode = document.createElement('img'); 348 | 349 | qrCode.setAttribute('src', this.signing_refs.refs.qr); 350 | qrCode.setAttribute('alt', 'QR Code'); 351 | 352 | let container: any = document.getElementById(this.fallback_container_id); 353 | container.appendChild(qrCode); 354 | 355 | if (this.is_mobile && this.deeplink_url) { 356 | let deepLink = document.createElement('a'); 357 | 358 | deepLink.setAttribute('href', this.signing_refs.refs.deeplink); 359 | deepLink.setAttribute('target', '_blank'); 360 | deepLink.setAttribute('rel', 'noopener noreferrer'); 361 | deepLink.innerText = 'SOLO Wallet >'; 362 | 363 | container.appendChild(deepLink); 364 | } 365 | } 366 | } 367 | -------------------------------------------------------------------------------- /src/tests/txhandler/redis.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Currently there is an open ripple-lib issue which requires that tests 3 | * be run with the environment variable NODE_TLS_REJECT_UNAUTHORIZED set 4 | * to 0 when connecting to the s.devnet.rippletest.net or 5 | * s.altnet.rippletest.net networks. 6 | * 7 | * The open issue can be found at: 8 | * https://github.com/ripple/ripple-lib/issues/1191 9 | */ 10 | 11 | import anyTest, {TestInterface} from 'ava'; 12 | import { RippleAPIOptions } from '../../types/xrpl'; 13 | import { TransactionHandlerOptions, QUEUE_TYPE_STXMQ_REDIS } from '../../types/queues'; 14 | 15 | import { http } from '../../lib/utils'; 16 | 17 | const test = anyTest as TestInterface<{ 18 | handler: any, 19 | server: string, 20 | faucet: string, 21 | validAccount: any, 22 | invalidAccount: any, 23 | emptyAccount: any 24 | }>; 25 | 26 | import { IFaucet } from '../../types/utils'; 27 | import * as SologenicTypes from '../../types/txhandler'; 28 | import { SologenicTxHandler } from '../../lib/txhandler'; 29 | import XrplAccount from '../../lib/account'; 30 | 31 | const NETWORK_LIST = { 32 | dev: { 33 | wss: 'wss://s.devnet.rippletest.net:51233', 34 | faucet: 'https://faucet.devnet.rippletest.net/accounts', 35 | certificates: [ 36 | `-----BEGIN CERTIFICATE----- 37 | MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU 38 | MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs 39 | IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290 40 | MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux 41 | FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h 42 | bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v 43 | dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt 44 | H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9 45 | uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX 46 | mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX 47 | a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN 48 | E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0 49 | WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD 50 | VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0 51 | Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU 52 | cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx 53 | IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN 54 | AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH 55 | YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5 56 | 6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC 57 | Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX 58 | c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a 59 | mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ= 60 | -----END CERTIFICATE----- 61 | `, 62 | `-----BEGIN CERTIFICATE----- 63 | MIIFdzCCBF+gAwIBAgIQE+oocFv07O0MNmMJgGFDNjANBgkqhkiG9w0BAQwFADBv 64 | MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFk 65 | ZFRydXN0IEV4dGVybmFsIFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBF 66 | eHRlcm5hbCBDQSBSb290MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFow 67 | gYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5MRQwEgYDVQQHEwtK 68 | ZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMS4wLAYD 69 | VQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjAN 70 | BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAgBJlFzYOw9sIs9CsVw127c0n00yt 71 | UINh4qogTQktZAnczomfzD2p7PbPwdzx07HWezcoEStH2jnGvDoZtF+mvX2do2NC 72 | tnbyqTsrkfjib9DsFiCQCT7i6HTJGLSR1GJk23+jBvGIGGqQIjy8/hPwhxR79uQf 73 | jtTkUcYRZ0YIUcuGFFQ/vDP+fmyc/xadGL1RjjWmp2bIcmfbIWax1Jt4A8BQOujM 74 | 8Ny8nkz+rwWWNR9XWrf/zvk9tyy29lTdyOcSOk2uTIq3XJq0tyA9yn8iNK5+O2hm 75 | AUTnAU5GU5szYPeUvlM3kHND8zLDU+/bqv50TmnHa4xgk97Exwzf4TKuzJM7UXiV 76 | Z4vuPVb+DNBpDxsP8yUmazNt925H+nND5X4OpWaxKXwyhGNVicQNwZNUMBkTrNN9 77 | N6frXTpsNVzbQdcS2qlJC9/YgIoJk2KOtWbPJYjNhLixP6Q5D9kCnusSTJV882sF 78 | qV4Wg8y4Z+LoE53MW4LTTLPtW//e5XOsIzstAL81VXQJSdhJWBp/kjbmUZIO8yZ9 79 | HE0XvMnsQybQv0FfQKlERPSZ51eHnlAfV1SoPv10Yy+xUGUJ5lhCLkMaTLTwJUdZ 80 | +gQek9QmRkpQgbLevni3/GcV4clXhB4PY9bpYrrWX1Uu6lzGKAgEJTm4Diup8kyX 81 | HAc/DVL17e8vgg8CAwEAAaOB9DCB8TAfBgNVHSMEGDAWgBStvZh6NLQm9/rEJlTv 82 | A73gJMtUGjAdBgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/ 83 | BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0gBAowCDAGBgRVHSAAMEQGA1Ud 84 | HwQ9MDswOaA3oDWGM2h0dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9BZGRUcnVzdEV4 85 | dGVybmFsQ0FSb290LmNybDA1BggrBgEFBQcBAQQpMCcwJQYIKwYBBQUHMAGGGWh0 86 | dHA6Ly9vY3NwLnVzZXJ0cnVzdC5jb20wDQYJKoZIhvcNAQEMBQADggEBAJNl9jeD 87 | lQ9ew4IcH9Z35zyKwKoJ8OkLJvHgwmp1ocd5yblSYMgpEg7wrQPWCcR23+WmgZWn 88 | RtqCV6mVksW2jwMibDN3wXsyF24HzloUQToFJBv2FAY7qCUkDrvMKnXduXBBP3zQ 89 | YzYhBx9G/2CkkeFnvN4ffhkUyWNnkepnB2u0j4vAbkN9w6GAbLIevFOFfdyQoaS8 90 | Le9Gclc1Bb+7RrtubTeZtv8jkpHGbkD4jylW6l/VXxRTrPBPYer3IsynVgviuDQf 91 | Jtl7GQVoP7o81DgGotPmjw7jtHFtQELFhLRAlSv0ZaBIefYdgWOWnU914Ph85I6p 92 | 0fKtirOMxyHNwu8= 93 | -----END CERTIFICATE----- 94 | `, 95 | `-----BEGIN CERTIFICATE----- 96 | MIIF6TCCA9GgAwIBAgIQBeTcO5Q4qzuFl8umoZhQ4zANBgkqhkiG9w0BAQwFADCB 97 | iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl 98 | cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV 99 | BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTQw 100 | OTEyMDAwMDAwWhcNMjQwOTExMjM1OTU5WjBfMQswCQYDVQQGEwJGUjEOMAwGA1UE 101 | CBMFUGFyaXMxDjAMBgNVBAcTBVBhcmlzMQ4wDAYDVQQKEwVHYW5kaTEgMB4GA1UE 102 | AxMXR2FuZGkgU3RhbmRhcmQgU1NMIENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IB 103 | DwAwggEKAoIBAQCUBC2meZV0/9UAPPWu2JSxKXzAjwsLibmCg5duNyj1ohrP0pIL 104 | m6jTh5RzhBCf3DXLwi2SrCG5yzv8QMHBgyHwv/j2nPqcghDA0I5O5Q1MsJFckLSk 105 | QFEW2uSEEi0FXKEfFxkkUap66uEHG4aNAXLy59SDIzme4OFMH2sio7QQZrDtgpbX 106 | bmq08j+1QvzdirWrui0dOnWbMdw+naxb00ENbLAb9Tr1eeohovj0M1JLJC0epJmx 107 | bUi8uBL+cnB89/sCdfSN3tbawKAyGlLfOGsuRTg/PwSWAP2h9KK71RfWJ3wbWFmV 108 | XooS/ZyrgT5SKEhRhWvzkbKGPym1bgNi7tYFAgMBAAGjggF1MIIBcTAfBgNVHSME 109 | GDAWgBRTeb9aqitKz1SA4dibwJ3ysgNmyzAdBgNVHQ4EFgQUs5Cn2MmvTs1hPJ98 110 | rV1/Qf1pMOowDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYD 111 | VR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMCIGA1UdIAQbMBkwDQYLKwYBBAGy 112 | MQECAhowCAYGZ4EMAQIBMFAGA1UdHwRJMEcwRaBDoEGGP2h0dHA6Ly9jcmwudXNl 113 | cnRydXN0LmNvbS9VU0VSVHJ1c3RSU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNy 114 | bDB2BggrBgEFBQcBAQRqMGgwPwYIKwYBBQUHMAKGM2h0dHA6Ly9jcnQudXNlcnRy 115 | dXN0LmNvbS9VU0VSVHJ1c3RSU0FBZGRUcnVzdENBLmNydDAlBggrBgEFBQcwAYYZ 116 | aHR0cDovL29jc3AudXNlcnRydXN0LmNvbTANBgkqhkiG9w0BAQwFAAOCAgEAWGf9 117 | crJq13xhlhl+2UNG0SZ9yFP6ZrBrLafTqlb3OojQO3LJUP33WbKqaPWMcwO7lWUX 118 | zi8c3ZgTopHJ7qFAbjyY1lzzsiI8Le4bpOHeICQW8owRc5E69vrOJAKHypPstLbI 119 | FhfFcvwnQPYT/pOmnVHvPCvYd1ebjGU6NSU2t7WKY28HJ5OxYI2A25bUeo8tqxyI 120 | yW5+1mUfr13KFj8oRtygNeX56eXVlogMT8a3d2dIhCe2H7Bo26y/d7CQuKLJHDJd 121 | ArolQ4FCR7vY4Y8MDEZf7kYzawMUgtN+zY+vkNaOJH1AQrRqahfGlZfh8jjNp+20 122 | J0CT33KpuMZmYzc4ZCIwojvxuch7yPspOqsactIGEk72gtQjbz7Dk+XYtsDe3CMW 123 | 1hMwt6CaDixVBgBwAc/qOR2A24j3pSC4W/0xJmmPLQphgzpHphNULB7j7UTKvGof 124 | KA5R2d4On3XNDgOVyvnFqSot/kGkoUeuDcL5OWYzSlvhhChZbH2UF3bkRYKtcCD9 125 | 0m9jqNf6oDP6N8v3smWe2lBvP+Sn845dWDKXcCMu5/3EFZucJ48y7RetWIExKREa 126 | m9T8bJUox04FB6b9HbwZ4ui3uRGKLXASUoWNjDNKD/yZkuBjcNqllEdjB+dYxzFf 127 | BT02Vf6Dsuimrdfp5gJ0iHRc2jTbkNJtUQoj1iM= 128 | -----END CERTIFICATE----- 129 | `], 130 | }, 131 | 132 | test: { 133 | wss: 'wss://s.altnet.rippletest.net:51233', 134 | faucet: 'https://faucet.altnet.rippletest.net/accounts', 135 | certificates: [`-----BEGIN CERTIFICATE----- 136 | MIIGHzCCBQegAwIBAgIQZVrM7fIUFRxOEkmjVlxrYDANBgkqhkiG9w0BAQsFADBf 137 | MQswCQYDVQQGEwJGUjEOMAwGA1UECBMFUGFyaXMxDjAMBgNVBAcTBVBhcmlzMQ4w 138 | DAYDVQQKEwVHYW5kaTEgMB4GA1UEAxMXR2FuZGkgU3RhbmRhcmQgU1NMIENBIDIw 139 | HhcNMTkwNDI5MDAwMDAwWhcNMjAwNDI5MjM1OTU5WjBrMSEwHwYDVQQLExhEb21h 140 | aW4gQ29udHJvbCBWYWxpZGF0ZWQxJDAiBgNVBAsTG0dhbmRpIFN0YW5kYXJkIFdp 141 | bGRjYXJkIFNTTDEgMB4GA1UEAwwXKi5hbHRuZXQucmlwcGxldGVzdC5uZXQwggEi 142 | MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDQEAw6I2062RdTvb20fl5lESHZ 143 | iJ0+OM1Oih3r4kt1iqKjCu4o9lITCM/5XFLn7nKBakLUtd5g2zsHMYCNXPkJsqr5 144 | xlHgrlTqxAEOe0/QMyICStzC/mgMeZQhMow8sfN43ClsCEV4RZqP+F6BNav9C4BN 145 | FhSeI9pzy2v4jFJTmvGSUm3rRaHGsSXSY1wwenQeQvXTXTULngfK2wd0rjR7HFsY 146 | OZC6kDLW5Flj6jVX19l5qAYJhyWueoHSkODtOVUZeNHW4ip+T0w1uh8Sdz7RHBNN 147 | SP+acPyVz+jSTXDmlT5NnSaSmr54ORjxCLF+/MuRXKcHXcCkc65l3MMhIXsLAgMB 148 | AAGjggLJMIICxTAfBgNVHSMEGDAWgBSzkKfYya9OzWE8n3ytXX9B/Wkw6jAdBgNV 149 | HQ4EFgQUgPI58+IFqXqwJO3/Ayv3RtEhypAwDgYDVR0PAQH/BAQDAgWgMAwGA1Ud 150 | EwEB/wQCMAAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMEsGA1UdIARE 151 | MEIwNgYLKwYBBAGyMQECAhowJzAlBggrBgEFBQcCARYZaHR0cHM6Ly9jcHMudXNl 152 | cnRydXN0LmNvbTAIBgZngQwBAgEwQQYDVR0fBDowODA2oDSgMoYwaHR0cDovL2Ny 153 | bC51c2VydHJ1c3QuY29tL0dhbmRpU3RhbmRhcmRTU0xDQTIuY3JsMHMGCCsGAQUF 154 | BwEBBGcwZTA8BggrBgEFBQcwAoYwaHR0cDovL2NydC51c2VydHJ1c3QuY29tL0dh 155 | bmRpU3RhbmRhcmRTU0xDQTIuY3J0MCUGCCsGAQUFBzABhhlodHRwOi8vb2NzcC51 156 | c2VydHJ1c3QuY29tMDkGA1UdEQQyMDCCFyouYWx0bmV0LnJpcHBsZXRlc3QubmV0 157 | ghVhbHRuZXQucmlwcGxldGVzdC5uZXQwggEEBgorBgEEAdZ5AgQCBIH1BIHyAPAA 158 | dgC72d+8H4pxtZOUI5eqkntHOFeVCqtS6BqQlmQ2jh7RhQAAAWpqdpMTAAAEAwBH 159 | MEUCIFvNilgFe/ZcrLQsoxgC7LqB1JNlp5iGi+rolgLv3oxgAiEAtovapui1y/rv 160 | TQDJVIfbJNCohOJiilBYWmp3LxHqkcEAdgBep3P531bA57U2SH3QSeAyepGaDISh 161 | EhKEGHWWgXFFWAAAAWpqdpNIAAAEAwBHMEUCIQC/iVRdRxtHIZVVEl+ERPw0QD3t 162 | AYboOHAqKmTHQA62EwIgTkis/eCm0jqfMLcK81B8oEgDd1N9VZOG+pKfflgg1+8w 163 | DQYJKoZIhvcNAQELBQADggEBAApOtMkSb/Rf05YAHpd78tiRxdUivDPVphkUeLKE 164 | OMvcowZ7xzZFz72uKx7txxhu/RpJY0g7W6kPAc1E8+MuQC6OU4JJiCc1s2yPNkor 165 | eyeAH2AVqsj7GfOWPRIQp234HW73+PGEoeq/uK0sMf8BywDkOhhAtLtUrW69w0k6 166 | obg/VSM4w3wR7oQpb1q7yDNbj0sCM/gVd23lgDl2mi6M+N/sINo7JtoxTv8/F9/R 167 | YxBvre4eabGxv2BSuEhFUdA30lDiwYOj4UnLVoCKMjsZZoY4WnkKrm4q6hITkV2c 168 | uiKk3SZRjGo9kHr8kBmWVY0Icm3LRF6bNMEcve6un3v7D/c= 169 | -----END CERTIFICATE-----`,] 170 | } 171 | } 172 | 173 | const NETWORK = NETWORK_LIST.dev; 174 | 175 | /* Use before each so we use a different address for each request */ 176 | test.before(async t => { 177 | t.context.server = NETWORK.wss; 178 | t.context.faucet = NETWORK.faucet; 179 | 180 | const accounts = await Promise.all([ 181 | http(NETWORK.faucet), 182 | http(NETWORK.faucet), 183 | http(NETWORK.faucet) 184 | ]); 185 | 186 | t.context.invalidAccount = new XrplAccount(accounts[0].account.address, accounts[0].account.secret, undefined, undefined); 187 | t.context.validAccount = new XrplAccount(accounts[1].account.address, accounts[1].account.secret, undefined, undefined); 188 | t.context.emptyAccount = new XrplAccount(accounts[2].account.address, accounts[2].account.secret, undefined, undefined); 189 | 190 | const rippleOptions: RippleAPIOptions = { 191 | server: t.context.server, 192 | trustedCertificates: NETWORK.certificates, 193 | trace: false 194 | }; 195 | 196 | const thOptions: TransactionHandlerOptions = { 197 | queueType: QUEUE_TYPE_STXMQ_REDIS, 198 | redis: { 199 | port: 6379, 200 | host: 'localhost', 201 | password: '', 202 | db: 1, 203 | } 204 | }; 205 | 206 | t.context.handler = new SologenicTxHandler(rippleOptions, thOptions); 207 | }); 208 | 209 | test('sologenic tx redis initialization', async t => { 210 | t.pass(); 211 | }); 212 | 213 | test('transaction to sologenic xrpl stream', async t => { 214 | try { 215 | const handler: SologenicTxHandler = t.context!.handler; 216 | const eventsReceived: Array = []; 217 | 218 | await handler.setXrplAccount(t.context.validAccount); 219 | 220 | // Make sure we're actually performing an operation (setflags: 5) 221 | const tx: SologenicTypes.TX = { 222 | Account: t.context.validAccount.address, 223 | TransactionType: 'AccountSet', 224 | SetFlag: 5 225 | }; 226 | 227 | const transaction: SologenicTypes.TransactionObject = handler.submit(tx); 228 | 229 | // noUnusedLocals is enabled in the tsconfig, so we access the object at least once 230 | transaction.events 231 | .on('queued', (event: SologenicTypes.QueuedEvent) => { 232 | event; 233 | 234 | // t.log("Event (queued): ", JSON.stringify(event)); 235 | 236 | eventsReceived.push('queued'); 237 | }) 238 | .on('dispatched', (event: SologenicTypes.DispatchedEvent) => { 239 | event; 240 | 241 | // t.log("Event (dispatched): ", JSON.stringify(event)); 242 | 243 | eventsReceived.push('dispatched'); 244 | }) 245 | .on('requeued', (event: SologenicTypes.RequeuedEvent) => { 246 | event; 247 | 248 | // t.log("Event (requeued): ", JSON.stringify(event)); 249 | 250 | eventsReceived.push('requeued'); 251 | }) 252 | .on('warning', (event: SologenicTypes.WarningEvent) => { 253 | event; 254 | 255 | // t.log("Event (warning): ", JSON.stringify(event)); 256 | 257 | eventsReceived.push('warning'); 258 | }) 259 | .on('validated', (event: SologenicTypes.ValidatedEvent) => { 260 | event; 261 | 262 | // t.log("Event (validated): ", JSON.stringify(event)); 263 | 264 | eventsReceived.push('validated'); 265 | 266 | t.is(event.reason, 'tesSUCCESS'); 267 | }) 268 | .on('failed', (event: SologenicTypes.FailedEvent) => { 269 | event; 270 | 271 | // t.log("Event (failed): ", JSON.stringify(event)); 272 | eventsReceived.push('failed'); 273 | 274 | t.fail(event.reason); 275 | }); 276 | 277 | await transaction.promise; 278 | 279 | transaction.events.removeAllListeners('queued'); 280 | transaction.events.removeAllListeners('dispatched'); 281 | transaction.events.removeAllListeners('requeued'); 282 | transaction.events.removeAllListeners('warning'); 283 | transaction.events.removeAllListeners('validated'); 284 | transaction.events.removeAllListeners('failed'); 285 | 286 | t.false(eventsReceived.includes('failed')) 287 | t.true(eventsReceived.includes('queued')); 288 | t.true(eventsReceived.includes('dispatched')); 289 | t.true(eventsReceived.includes('validated')); 290 | 291 | } catch (error) { 292 | t.log(error); 293 | t.fail(); 294 | } 295 | }); 296 | -------------------------------------------------------------------------------- /src/tests/txhandler/hash.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Currently there is an open ripple-lib issue which requires that tests 3 | * be run with the environment variable NODE_TLS_REJECT_UNAUTHORIZED set 4 | * to 0 when connecting to the s.devnet.rippletest.net or 5 | * s.altnet.rippletest.net networks. 6 | * 7 | * The open issue can be found at: 8 | * https://github.com/ripple/ripple-lib/issues/1191 9 | */ 10 | 11 | import anyTest, {TestInterface} from 'ava'; 12 | 13 | import { http } from '../../lib/utils'; 14 | 15 | const test = anyTest as TestInterface<{ 16 | handler: any, 17 | server: string, 18 | faucet: string, 19 | validAccount: any, 20 | invalidAccount: any, 21 | emptyAccount: any 22 | }>; 23 | 24 | import { RippleAPIOptions } from '../../types/xrpl'; 25 | import { TransactionHandlerOptions, QUEUE_TYPE_STXMQ_HASH } from '../../types/queues'; 26 | import { IFaucet } from '../../types/utils'; 27 | import * as SologenicTypes from '../../types/txhandler'; 28 | import { SologenicTxHandler } from '../../lib/txhandler'; 29 | import XrplAccount from '../../lib/account'; 30 | import { XummSigner } from '../../lib/signing'; 31 | 32 | const NETWORK_LIST = { 33 | dev: { 34 | wss: 'wss://s.devnet.rippletest.net:51233', 35 | faucet: 'https://faucet.devnet.rippletest.net/accounts', 36 | certificates: [ 37 | `-----BEGIN CERTIFICATE----- 38 | MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU 39 | MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs 40 | IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290 41 | MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux 42 | FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h 43 | bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v 44 | dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt 45 | H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9 46 | uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX 47 | mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX 48 | a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN 49 | E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0 50 | WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD 51 | VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0 52 | Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU 53 | cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx 54 | IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN 55 | AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH 56 | YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5 57 | 6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC 58 | Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX 59 | c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a 60 | mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ= 61 | -----END CERTIFICATE----- 62 | `, 63 | `-----BEGIN CERTIFICATE----- 64 | MIIFdzCCBF+gAwIBAgIQE+oocFv07O0MNmMJgGFDNjANBgkqhkiG9w0BAQwFADBv 65 | MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFk 66 | ZFRydXN0IEV4dGVybmFsIFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBF 67 | eHRlcm5hbCBDQSBSb290MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFow 68 | gYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5MRQwEgYDVQQHEwtK 69 | ZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMS4wLAYD 70 | VQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjAN 71 | BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAgBJlFzYOw9sIs9CsVw127c0n00yt 72 | UINh4qogTQktZAnczomfzD2p7PbPwdzx07HWezcoEStH2jnGvDoZtF+mvX2do2NC 73 | tnbyqTsrkfjib9DsFiCQCT7i6HTJGLSR1GJk23+jBvGIGGqQIjy8/hPwhxR79uQf 74 | jtTkUcYRZ0YIUcuGFFQ/vDP+fmyc/xadGL1RjjWmp2bIcmfbIWax1Jt4A8BQOujM 75 | 8Ny8nkz+rwWWNR9XWrf/zvk9tyy29lTdyOcSOk2uTIq3XJq0tyA9yn8iNK5+O2hm 76 | AUTnAU5GU5szYPeUvlM3kHND8zLDU+/bqv50TmnHa4xgk97Exwzf4TKuzJM7UXiV 77 | Z4vuPVb+DNBpDxsP8yUmazNt925H+nND5X4OpWaxKXwyhGNVicQNwZNUMBkTrNN9 78 | N6frXTpsNVzbQdcS2qlJC9/YgIoJk2KOtWbPJYjNhLixP6Q5D9kCnusSTJV882sF 79 | qV4Wg8y4Z+LoE53MW4LTTLPtW//e5XOsIzstAL81VXQJSdhJWBp/kjbmUZIO8yZ9 80 | HE0XvMnsQybQv0FfQKlERPSZ51eHnlAfV1SoPv10Yy+xUGUJ5lhCLkMaTLTwJUdZ 81 | +gQek9QmRkpQgbLevni3/GcV4clXhB4PY9bpYrrWX1Uu6lzGKAgEJTm4Diup8kyX 82 | HAc/DVL17e8vgg8CAwEAAaOB9DCB8TAfBgNVHSMEGDAWgBStvZh6NLQm9/rEJlTv 83 | A73gJMtUGjAdBgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/ 84 | BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0gBAowCDAGBgRVHSAAMEQGA1Ud 85 | HwQ9MDswOaA3oDWGM2h0dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9BZGRUcnVzdEV4 86 | dGVybmFsQ0FSb290LmNybDA1BggrBgEFBQcBAQQpMCcwJQYIKwYBBQUHMAGGGWh0 87 | dHA6Ly9vY3NwLnVzZXJ0cnVzdC5jb20wDQYJKoZIhvcNAQEMBQADggEBAJNl9jeD 88 | lQ9ew4IcH9Z35zyKwKoJ8OkLJvHgwmp1ocd5yblSYMgpEg7wrQPWCcR23+WmgZWn 89 | RtqCV6mVksW2jwMibDN3wXsyF24HzloUQToFJBv2FAY7qCUkDrvMKnXduXBBP3zQ 90 | YzYhBx9G/2CkkeFnvN4ffhkUyWNnkepnB2u0j4vAbkN9w6GAbLIevFOFfdyQoaS8 91 | Le9Gclc1Bb+7RrtubTeZtv8jkpHGbkD4jylW6l/VXxRTrPBPYer3IsynVgviuDQf 92 | Jtl7GQVoP7o81DgGotPmjw7jtHFtQELFhLRAlSv0ZaBIefYdgWOWnU914Ph85I6p 93 | 0fKtirOMxyHNwu8= 94 | -----END CERTIFICATE----- 95 | `, 96 | `-----BEGIN CERTIFICATE----- 97 | MIIF6TCCA9GgAwIBAgIQBeTcO5Q4qzuFl8umoZhQ4zANBgkqhkiG9w0BAQwFADCB 98 | iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl 99 | cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV 100 | BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTQw 101 | OTEyMDAwMDAwWhcNMjQwOTExMjM1OTU5WjBfMQswCQYDVQQGEwJGUjEOMAwGA1UE 102 | CBMFUGFyaXMxDjAMBgNVBAcTBVBhcmlzMQ4wDAYDVQQKEwVHYW5kaTEgMB4GA1UE 103 | AxMXR2FuZGkgU3RhbmRhcmQgU1NMIENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IB 104 | DwAwggEKAoIBAQCUBC2meZV0/9UAPPWu2JSxKXzAjwsLibmCg5duNyj1ohrP0pIL 105 | m6jTh5RzhBCf3DXLwi2SrCG5yzv8QMHBgyHwv/j2nPqcghDA0I5O5Q1MsJFckLSk 106 | QFEW2uSEEi0FXKEfFxkkUap66uEHG4aNAXLy59SDIzme4OFMH2sio7QQZrDtgpbX 107 | bmq08j+1QvzdirWrui0dOnWbMdw+naxb00ENbLAb9Tr1eeohovj0M1JLJC0epJmx 108 | bUi8uBL+cnB89/sCdfSN3tbawKAyGlLfOGsuRTg/PwSWAP2h9KK71RfWJ3wbWFmV 109 | XooS/ZyrgT5SKEhRhWvzkbKGPym1bgNi7tYFAgMBAAGjggF1MIIBcTAfBgNVHSME 110 | GDAWgBRTeb9aqitKz1SA4dibwJ3ysgNmyzAdBgNVHQ4EFgQUs5Cn2MmvTs1hPJ98 111 | rV1/Qf1pMOowDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYD 112 | VR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMCIGA1UdIAQbMBkwDQYLKwYBBAGy 113 | MQECAhowCAYGZ4EMAQIBMFAGA1UdHwRJMEcwRaBDoEGGP2h0dHA6Ly9jcmwudXNl 114 | cnRydXN0LmNvbS9VU0VSVHJ1c3RSU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNy 115 | bDB2BggrBgEFBQcBAQRqMGgwPwYIKwYBBQUHMAKGM2h0dHA6Ly9jcnQudXNlcnRy 116 | dXN0LmNvbS9VU0VSVHJ1c3RSU0FBZGRUcnVzdENBLmNydDAlBggrBgEFBQcwAYYZ 117 | aHR0cDovL29jc3AudXNlcnRydXN0LmNvbTANBgkqhkiG9w0BAQwFAAOCAgEAWGf9 118 | crJq13xhlhl+2UNG0SZ9yFP6ZrBrLafTqlb3OojQO3LJUP33WbKqaPWMcwO7lWUX 119 | zi8c3ZgTopHJ7qFAbjyY1lzzsiI8Le4bpOHeICQW8owRc5E69vrOJAKHypPstLbI 120 | FhfFcvwnQPYT/pOmnVHvPCvYd1ebjGU6NSU2t7WKY28HJ5OxYI2A25bUeo8tqxyI 121 | yW5+1mUfr13KFj8oRtygNeX56eXVlogMT8a3d2dIhCe2H7Bo26y/d7CQuKLJHDJd 122 | ArolQ4FCR7vY4Y8MDEZf7kYzawMUgtN+zY+vkNaOJH1AQrRqahfGlZfh8jjNp+20 123 | J0CT33KpuMZmYzc4ZCIwojvxuch7yPspOqsactIGEk72gtQjbz7Dk+XYtsDe3CMW 124 | 1hMwt6CaDixVBgBwAc/qOR2A24j3pSC4W/0xJmmPLQphgzpHphNULB7j7UTKvGof 125 | KA5R2d4On3XNDgOVyvnFqSot/kGkoUeuDcL5OWYzSlvhhChZbH2UF3bkRYKtcCD9 126 | 0m9jqNf6oDP6N8v3smWe2lBvP+Sn845dWDKXcCMu5/3EFZucJ48y7RetWIExKREa 127 | m9T8bJUox04FB6b9HbwZ4ui3uRGKLXASUoWNjDNKD/yZkuBjcNqllEdjB+dYxzFf 128 | BT02Vf6Dsuimrdfp5gJ0iHRc2jTbkNJtUQoj1iM= 129 | -----END CERTIFICATE----- 130 | `], 131 | }, 132 | 133 | test: { 134 | wss: 'wss://s.altnet.rippletest.net:51233', 135 | faucet: 'https://faucet.altnet.rippletest.net/accounts', 136 | certificates: [`-----BEGIN CERTIFICATE----- 137 | MIIGHzCCBQegAwIBAgIQZVrM7fIUFRxOEkmjVlxrYDANBgkqhkiG9w0BAQsFADBf 138 | MQswCQYDVQQGEwJGUjEOMAwGA1UECBMFUGFyaXMxDjAMBgNVBAcTBVBhcmlzMQ4w 139 | DAYDVQQKEwVHYW5kaTEgMB4GA1UEAxMXR2FuZGkgU3RhbmRhcmQgU1NMIENBIDIw 140 | HhcNMTkwNDI5MDAwMDAwWhcNMjAwNDI5MjM1OTU5WjBrMSEwHwYDVQQLExhEb21h 141 | aW4gQ29udHJvbCBWYWxpZGF0ZWQxJDAiBgNVBAsTG0dhbmRpIFN0YW5kYXJkIFdp 142 | bGRjYXJkIFNTTDEgMB4GA1UEAwwXKi5hbHRuZXQucmlwcGxldGVzdC5uZXQwggEi 143 | MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDQEAw6I2062RdTvb20fl5lESHZ 144 | iJ0+OM1Oih3r4kt1iqKjCu4o9lITCM/5XFLn7nKBakLUtd5g2zsHMYCNXPkJsqr5 145 | xlHgrlTqxAEOe0/QMyICStzC/mgMeZQhMow8sfN43ClsCEV4RZqP+F6BNav9C4BN 146 | FhSeI9pzy2v4jFJTmvGSUm3rRaHGsSXSY1wwenQeQvXTXTULngfK2wd0rjR7HFsY 147 | OZC6kDLW5Flj6jVX19l5qAYJhyWueoHSkODtOVUZeNHW4ip+T0w1uh8Sdz7RHBNN 148 | SP+acPyVz+jSTXDmlT5NnSaSmr54ORjxCLF+/MuRXKcHXcCkc65l3MMhIXsLAgMB 149 | AAGjggLJMIICxTAfBgNVHSMEGDAWgBSzkKfYya9OzWE8n3ytXX9B/Wkw6jAdBgNV 150 | HQ4EFgQUgPI58+IFqXqwJO3/Ayv3RtEhypAwDgYDVR0PAQH/BAQDAgWgMAwGA1Ud 151 | EwEB/wQCMAAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMEsGA1UdIARE 152 | MEIwNgYLKwYBBAGyMQECAhowJzAlBggrBgEFBQcCARYZaHR0cHM6Ly9jcHMudXNl 153 | cnRydXN0LmNvbTAIBgZngQwBAgEwQQYDVR0fBDowODA2oDSgMoYwaHR0cDovL2Ny 154 | bC51c2VydHJ1c3QuY29tL0dhbmRpU3RhbmRhcmRTU0xDQTIuY3JsMHMGCCsGAQUF 155 | BwEBBGcwZTA8BggrBgEFBQcwAoYwaHR0cDovL2NydC51c2VydHJ1c3QuY29tL0dh 156 | bmRpU3RhbmRhcmRTU0xDQTIuY3J0MCUGCCsGAQUFBzABhhlodHRwOi8vb2NzcC51 157 | c2VydHJ1c3QuY29tMDkGA1UdEQQyMDCCFyouYWx0bmV0LnJpcHBsZXRlc3QubmV0 158 | ghVhbHRuZXQucmlwcGxldGVzdC5uZXQwggEEBgorBgEEAdZ5AgQCBIH1BIHyAPAA 159 | dgC72d+8H4pxtZOUI5eqkntHOFeVCqtS6BqQlmQ2jh7RhQAAAWpqdpMTAAAEAwBH 160 | MEUCIFvNilgFe/ZcrLQsoxgC7LqB1JNlp5iGi+rolgLv3oxgAiEAtovapui1y/rv 161 | TQDJVIfbJNCohOJiilBYWmp3LxHqkcEAdgBep3P531bA57U2SH3QSeAyepGaDISh 162 | EhKEGHWWgXFFWAAAAWpqdpNIAAAEAwBHMEUCIQC/iVRdRxtHIZVVEl+ERPw0QD3t 163 | AYboOHAqKmTHQA62EwIgTkis/eCm0jqfMLcK81B8oEgDd1N9VZOG+pKfflgg1+8w 164 | DQYJKoZIhvcNAQELBQADggEBAApOtMkSb/Rf05YAHpd78tiRxdUivDPVphkUeLKE 165 | OMvcowZ7xzZFz72uKx7txxhu/RpJY0g7W6kPAc1E8+MuQC6OU4JJiCc1s2yPNkor 166 | eyeAH2AVqsj7GfOWPRIQp234HW73+PGEoeq/uK0sMf8BywDkOhhAtLtUrW69w0k6 167 | obg/VSM4w3wR7oQpb1q7yDNbj0sCM/gVd23lgDl2mi6M+N/sINo7JtoxTv8/F9/R 168 | YxBvre4eabGxv2BSuEhFUdA30lDiwYOj4UnLVoCKMjsZZoY4WnkKrm4q6hITkV2c 169 | uiKk3SZRjGo9kHr8kBmWVY0Icm3LRF6bNMEcve6un3v7D/c= 170 | -----END CERTIFICATE-----`,] 171 | } 172 | } 173 | 174 | const NETWORK = NETWORK_LIST.dev; 175 | 176 | /* Use before each so we use a different address for each request */ 177 | test.before(async t => { 178 | t.context.server = NETWORK.wss; 179 | t.context.faucet = NETWORK.faucet; 180 | 181 | const accounts = await Promise.all([ 182 | http(NETWORK.faucet), 183 | http(NETWORK.faucet), 184 | http(NETWORK.faucet) 185 | ]); 186 | 187 | t.context.invalidAccount = new XrplAccount(accounts[0].account.address, accounts[0].account.secret, undefined, undefined); 188 | t.context.validAccount = new XrplAccount(accounts[1].account.address, accounts[1].account.secret, undefined, undefined); 189 | t.context.emptyAccount = new XrplAccount(accounts[2].account.address, accounts[2].account.secret, undefined, undefined); 190 | 191 | const rippleOptions: RippleAPIOptions = { 192 | server: t.context.server, 193 | trustedCertificates: NETWORK.certificates, 194 | trace: false 195 | }; 196 | 197 | const thOptions: TransactionHandlerOptions = { 198 | queueType: QUEUE_TYPE_STXMQ_HASH, 199 | hash: {} 200 | }; 201 | 202 | t.context.handler = new SologenicTxHandler(rippleOptions, thOptions); 203 | }); 204 | 205 | test.serial('sologenic tx hash initialization', async t => { 206 | await t.notThrowsAsync(t.context.handler.setXrplAccount(t.context.validAccount)); 207 | }); 208 | 209 | test.serial('transaction to sologenic xrpl stream', async t => { 210 | try { 211 | const handler: SologenicTxHandler = t.context!.handler; 212 | const eventsReceived: Array = []; 213 | 214 | await handler.setXrplAccount(t.context.validAccount); 215 | 216 | // Make sure we're actually performing an operation (setflags: 5) 217 | const tx: SologenicTypes.TX = { 218 | Account: t.context.validAccount.getAddress(), 219 | TransactionType: 'AccountSet', 220 | SetFlag: 5 221 | }; 222 | 223 | const transaction: SologenicTypes.TransactionObject = handler.submit(tx); 224 | 225 | // noUnusedLocals is enabled in the tsconfig, so we access the object at least once 226 | transaction.events 227 | .on('queued', () => { 228 | eventsReceived.push('queued'); 229 | }) 230 | .on('dispatched', () => { 231 | eventsReceived.push('dispatched'); 232 | }) 233 | .on('requeued', () => { 234 | eventsReceived.push('requeued'); 235 | }) 236 | .on('warning', () => { 237 | eventsReceived.push('warning'); 238 | }) 239 | .on('validated', (event: SologenicTypes.ValidatedEvent) => { 240 | eventsReceived.push('validated'); 241 | 242 | t.is(event.reason, 'tesSUCCESS'); 243 | }) 244 | .on('failed', (event: SologenicTypes.FailedEvent) => { 245 | eventsReceived.push('failed'); 246 | 247 | t.fail(event.reason); 248 | }); 249 | 250 | await transaction.promise; 251 | 252 | transaction.events.removeAllListeners('queued'); 253 | transaction.events.removeAllListeners('dispatched'); 254 | transaction.events.removeAllListeners('requeued'); 255 | transaction.events.removeAllListeners('warning'); 256 | transaction.events.removeAllListeners('validated'); 257 | transaction.events.removeAllListeners('failed'); 258 | 259 | t.false(eventsReceived.includes('failed')) 260 | t.true(eventsReceived.includes('queued')); 261 | t.true(eventsReceived.includes('dispatched')); 262 | t.true(eventsReceived.includes('validated')); 263 | } catch (error) { 264 | t.fail(error); 265 | } 266 | }); 267 | 268 | test.serial('transaction should fail immediately (invalid flags)', async t => { 269 | try { 270 | const handler: SologenicTxHandler = t.context!.handler; 271 | await handler.setXrplAccount(t.context.validAccount); 272 | 273 | // See flags at https://xrpl.org/accountset.html 274 | const tx: SologenicTypes.TX = { 275 | Account: t.context.validAccount.getAddress(), 276 | TransactionType: 'AccountSet', 277 | SetFlag: -1 278 | }; 279 | 280 | const transaction: SologenicTypes.TransactionObject = handler.submit(tx); 281 | 282 | let txFailed = false; 283 | 284 | transaction.events.on('failed', (failedTx: SologenicTypes.FailedEvent) => { 285 | txFailed = true; 286 | 287 | // Should fail signing since -1 is not a valid flag 288 | t.is(failedTx.reason, "unable_to_sign_transaction"); 289 | }); 290 | 291 | await transaction.promise; 292 | 293 | } catch (error) { 294 | t.fail(); 295 | } 296 | }); 297 | 298 | test.serial('transaction should be successful', async t => { 299 | try { 300 | const handler: SologenicTxHandler = t.context!.handler; 301 | 302 | await handler.setXrplAccount(t.context.validAccount); 303 | 304 | // See flags at https://xrpl.org/accountset.html 305 | const tx: SologenicTypes.TX = { 306 | Account: t.context.validAccount.getAddress(), 307 | TransactionType: 'AccountSet', 308 | SetFlag: 5 309 | }; 310 | 311 | const transaction: SologenicTypes.TransactionObject = handler.submit(tx); 312 | 313 | transaction.events 314 | .on('validated', (event: SologenicTypes.ValidatedEvent) => { 315 | t.true(typeof event !== 'undefined'); 316 | t.is(event.reason, 'tesSUCCESS'); 317 | }) 318 | .on('failed', (event: SologenicTypes.FailedEvent) => { 319 | t.true(typeof event !== 'undefined'); 320 | t.fail(event.reason); 321 | }); 322 | 323 | await transaction.promise; 324 | } catch (error) { 325 | t.fail(error); 326 | } 327 | }); 328 | 329 | test.serial('transaction send multiple transactions', async t => { 330 | try { 331 | const handler: SologenicTxHandler = t.context!.handler; 332 | 333 | await handler.setXrplAccount(t.context.validAccount); 334 | 335 | // See flags at https://xrpl.org/accountset.html 336 | const tx1: SologenicTypes.TX = { 337 | Account: t.context.validAccount.getAddress(), 338 | TransactionType: 'AccountSet', 339 | SetFlag: 5 340 | }; 341 | 342 | // See flags at https://xrpl.org/accountset.html 343 | const tx2: SologenicTypes.TX = { 344 | Account: t.context.validAccount.getAddress(), 345 | TransactionType: 'AccountSet', 346 | SetFlag: 5 347 | }; 348 | 349 | // See flags at https://xrpl.org/accountset.html 350 | const tx3: SologenicTypes.TX = { 351 | Account: t.context.validAccount.getAddress(), 352 | TransactionType: 'AccountSet', 353 | SetFlag: 6 354 | }; 355 | 356 | const promises = await Promise.all([ 357 | handler.submit(tx1).promise, 358 | handler.submit(tx2).promise, 359 | handler.submit(tx3).promise 360 | ]); 361 | 362 | // console.log(promises); 363 | 364 | for (const transaction in promises) { 365 | if (promises[transaction].hasOwnProperty("hash")) { 366 | t.true(typeof promises[transaction] === 'object'); 367 | t.true(typeof promises[transaction].hash === 'string'); 368 | } else { 369 | t.fail(); 370 | } 371 | } 372 | } catch (error) { 373 | t.fail(error); 374 | } 375 | }); 376 | 377 | test.serial('transaction should fail with insufficient fee', async t => { 378 | try { 379 | const handler: SologenicTxHandler = t.context!.handler; 380 | 381 | await handler.setXrplAccount(t.context.validAccount); 382 | await handler.setLedgerBaseFeeXRP('0'); 383 | 384 | // See flags at https://xrpl.org/accountset.html 385 | const tx: SologenicTypes.TX = { 386 | Account: t.context.validAccount.getAddress(), 387 | TransactionType: 'AccountSet', 388 | SetFlag: 5 389 | }; 390 | 391 | let txQueued = false; 392 | let txDispatched = false; 393 | let txRequeued = false; 394 | let txWarning: boolean = false; 395 | let txValidated: boolean = false; 396 | let txFailed = false; 397 | 398 | const transaction: SologenicTypes.TransactionObject = handler.submit(tx); 399 | 400 | transaction.events 401 | .on('queued', () => { 402 | txQueued = true; 403 | }) 404 | .on('dispatched', () => { 405 | txDispatched = true; 406 | }) 407 | .on('requeued', () => { 408 | txRequeued = true; 409 | }) 410 | .on('warning', (event: SologenicTypes.WarningEvent) => { 411 | if (!txWarning) { 412 | // TODO: Clean up the reason to only contain telINSUF_FEE_P 413 | t.is(event.reason, 'telINSUF_FEE_P: Fee insufficient.'); 414 | txWarning = true; 415 | } 416 | }) 417 | .on('validated', () => { 418 | // Our transaction has been validated 419 | txValidated = true; 420 | }) 421 | .on('failed', () => { 422 | txFailed = true; 423 | }); 424 | 425 | await transaction.promise; 426 | 427 | t.true(txQueued); 428 | t.true(txDispatched); 429 | t.true(txRequeued); 430 | t.true(txValidated); 431 | t.true(txWarning); 432 | t.false(txFailed); 433 | 434 | } catch (error) { 435 | t.fail(error); 436 | } 437 | }); 438 | 439 | test.serial('transaction should return next sequence', async t => { 440 | try { 441 | const handler: SologenicTxHandler = t.context.handler; 442 | const currentSequence: number = t.context.validAccount.getCurrentAccountSequence(); 443 | await handler.setXrplAccount(t.context.validAccount); 444 | 445 | const sequence = handler.getAccount().getCurrentAccountSequence(); 446 | 447 | t.not(currentSequence, sequence); 448 | t.true(sequence > 0); 449 | 450 | } catch (error) { 451 | t.fail(error); 452 | } 453 | }); 454 | 455 | test.serial('transaction will fail with tefBAD_AUTH (invalid account cannot send on behalf of valid account)', async t => { 456 | try { 457 | const handler: SologenicTxHandler = t.context!.handler; 458 | await handler.setXrplAccount(t.context.validAccount); 459 | 460 | // The current sequence of the valid account needs to be used, otherwise we'll 461 | // fail with tefPAST_SEQ because the transaction sequence is too old. 462 | const currentSequence: number = t.context.validAccount.getCurrentAccountSequence(); 463 | 464 | await handler.setXrplAccount(t.context.invalidAccount); 465 | 466 | // See flags at https://xrpl.org/accountset.html 467 | const tx: SologenicTypes.TX = { 468 | Account: t.context.validAccount.getAddress(), 469 | TransactionType: 'AccountSet', 470 | SetFlag: 5, 471 | Sequence: currentSequence 472 | }; 473 | 474 | const transaction: SologenicTypes.TransactionObject = handler.submit(tx); 475 | 476 | let txFailed = false; 477 | 478 | transaction.events.on('failed', (failedTx: SologenicTypes.FailedEvent) => { 479 | t.is(failedTx.reason, "tefBAD_AUTH"); 480 | txFailed = true; 481 | }); 482 | 483 | await transaction.promise; 484 | 485 | t.true(txFailed); 486 | } catch (error) { 487 | t.fail(error); 488 | } 489 | }); 490 | 491 | test.serial('transaction will fail with tefPAST_SEQ (invalid account sequence is less than valid account)', async t => { 492 | try { 493 | const handler: SologenicTxHandler = t.context!.handler; 494 | 495 | // Set an invalid account and set the sequence number to the valid accounts sequence. 496 | await handler.setXrplAccount(t.context.validAccount); 497 | 498 | // See flags at https://xrpl.org/accountset.html 499 | const tx: SologenicTypes.TX = { 500 | Account: t.context.validAccount.getAddress(), 501 | TransactionType: 'AccountSet', 502 | SetFlag: 5, 503 | Sequence: 1 504 | }; 505 | 506 | const transaction: SologenicTypes.TransactionObject = handler.submit(tx); 507 | 508 | let txFailed = false; 509 | 510 | transaction.events.on('failed', (failedTx: SologenicTypes.FailedEvent) => { 511 | t.is(failedTx.reason, "tefPAST_SEQ"); 512 | txFailed = true; 513 | }); 514 | 515 | await transaction.promise; 516 | 517 | t.true(txFailed); 518 | } catch (error) { 519 | t.fail(error); 520 | } 521 | }); 522 | 523 | test.serial('transaction should fail because not enough funds are available', async t => { 524 | try { 525 | const handler: SologenicTxHandler = t.context!.handler; 526 | 527 | await handler.setXrplAccount(t.context.emptyAccount); 528 | 529 | // See flags at https://xrpl.org/accountset.html 530 | const tx1: SologenicTypes.TX = { 531 | Account: t.context.emptyAccount.getAddress(), 532 | TransactionType: 'Payment', 533 | Amount: handler.getRippleApi().xrpToDrops('99999'), 534 | Destination: t.context.validAccount.getAddress() 535 | }; 536 | 537 | // Send all funds out of this account to our validAccount, then 538 | // we'll send another transaction which will not be successful 539 | // because we'll be out of funds. 540 | 541 | const transaction: SologenicTypes.TransactionObject = handler.submit(tx1); 542 | 543 | transaction.events.on('failed', (failedTx: SologenicTypes.FailedEvent) => { 544 | t.true(typeof failedTx !== 'undefined'); 545 | t.is(failedTx.reason, 'tecUNFUNDED_PAYMENT'); 546 | }); 547 | 548 | await transaction.promise; 549 | 550 | } catch (error) { 551 | t.fail(error); 552 | } 553 | }); 554 | 555 | test.serial('transaction should fail, after sending request via xumm because no user input', async t => { 556 | try { 557 | const handler: SologenicTxHandler = t.context!.handler; 558 | 559 | handler.setSigningMechanism(new XummSigner({ 560 | xummApiKey: process.env.XUMM_API_KEY, 561 | xummApiSecret: process.env.XUMM_API_SECRET, 562 | // Gives us 10 seconds to react as this is a manual test, just so we can verify 563 | // the push notification was received. 564 | maximumExecutionTime: 5000 565 | })); 566 | 567 | await handler.setXrplAccount(t.context.emptyAccount); 568 | 569 | // See flags at https://xrpl.org/accountset.html 570 | const tx1: SologenicTypes.TX = { 571 | Account: t.context.emptyAccount.getAddress(), 572 | TransactionType: 'Payment', 573 | Amount: handler.getRippleApi().xrpToDrops('99999'), 574 | Destination: t.context.validAccount.getAddress() 575 | }; 576 | 577 | // Send all funds out of this account to our validAccount, then 578 | // we'll send another transaction which will not be successful 579 | // because we'll be out of funds. 580 | 581 | const transaction: SologenicTypes.TransactionObject = handler.submit(tx1); 582 | 583 | transaction.events.on('failed', (failedTx: SologenicTypes.FailedEvent) => { 584 | t.true(typeof failedTx !== 'undefined'); 585 | t.is(failedTx.reason, 'unable_to_sign_transaction'); 586 | }); 587 | 588 | await transaction.promise; 589 | 590 | } catch (error) { 591 | t.fail(error); 592 | } 593 | }); 594 | 595 | test.serial('transaction should fail, after sending push notification via xumm because no user input', async t => { 596 | try { 597 | const handler: SologenicTxHandler = t.context!.handler; 598 | 599 | handler.setSigningMechanism(new XummSigner({ 600 | xummApiKey: process.env.XUMM_API_KEY, 601 | xummApiSecret: process.env.XUMM_API_SECRET, 602 | // Gives us 10 seconds to react as this is a manual test, just so we can verify 603 | // the push notification was received. 604 | maximumExecutionTime: 10000 605 | })); 606 | 607 | await handler.setXrplAccount(t.context.emptyAccount); 608 | 609 | // See flags at https://xrpl.org/accountset.html 610 | const tx1: SologenicTypes.TX = { 611 | Account: t.context.emptyAccount.getAddress(), 612 | TransactionType: 'Payment', 613 | Amount: handler.getRippleApi().xrpToDrops('99999'), 614 | Destination: t.context.validAccount.getAddress(), 615 | TransactionMetadata: { 616 | xummMeta: { 617 | issued_user_token: 'ee9d788d-2de7-4d27-8afd-7829490f21bf' 618 | } 619 | } 620 | }; 621 | 622 | // Send all funds out of this account to our validAccount, then 623 | // we'll send another transaction which will not be successful 624 | // because we'll be out of funds. 625 | 626 | const transaction: SologenicTypes.TransactionObject = handler.submit(tx1); 627 | 628 | transaction.events.on('failed', (failedTx: SologenicTypes.FailedEvent) => { 629 | t.true(typeof failedTx !== 'undefined'); 630 | t.is(failedTx.reason, 'unable_to_sign_transaction'); 631 | }); 632 | 633 | await transaction.promise; 634 | 635 | } catch (error) { 636 | t.fail(error); 637 | } 638 | }); 639 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![sologenic-xrpl-stream-js](https://www.sologenic.com/images/solo-plain.png) 2 | 3 | 4 | 5 | - [ƨ Sologenic XRPL Stream](#ƨ-sologenic-xrpl-stream) 6 | 7 | - [Purpose](#purpose) 8 | 9 | - [How to Participate](#how-to-participate) 10 | 11 | - [Install](#install) 12 | 13 | - [Node Package Manager Scripts](#node-package-manager-scripts) 14 | 15 | - [Creating a packagable distribution](#creating-a-packagable-distribution) 16 | 17 | - [Testing](#testing) 18 | 19 | - [One-time execution of the unit tests](#one-time-execution-of-the-unit-tests) 20 | 21 | - [Persistent execution of unit tests (watching for changes)](#persistent-execution-of-unit-tests-watching-for-changes) 22 | 23 | - [Generating documentation](#generating-documentation) 24 | 25 | - [Generating HTML documentation only](#generating-html-documentation-only) 26 | 27 | - [Generating markdown documentation only](#generating-markdown-documentation-only) 28 | 29 | - [Typescript Example](#typescript-example) 30 | 31 | - [Intializing the Sologenic XRPL stream with a hash-based queue](#intializing-the-sologenic-xrpl-stream-with-a-hash-based-queue) 32 | 33 | - [Intializing the Sologenic XRPL stream with a redis-based queue](#intializing-the-sologenic-xrpl-stream-with-a-redis-based-queue) 34 | 35 | - [Using LedgerDevice as SigningMechanism](#using-ledgerdevice-as-signingmechanism) 36 | 37 | - [Using D'CENT Wallet as SigningMechanism](#using-dcent-wallet-as-signingmechanism) 38 | 39 | - [Using SOLO Wallet as SigningMechanism](#using-solo-wallet-as-signingmechanism) 40 | 41 | - [Using XUMM Wallet as SigningMechanism](#using-xumm-wallet-as-signingmechanism) 42 | 43 | - [Sending a Payment with the LedgerDeviceSigner as SigningMechanism](#sending-a-payment-with-the-ledgerdevicesigner-as-signingmechanism) 44 | 45 | - [Sending a Payment with the DcentSigner as SigningMechanism](#sending-a-payment-with-the-dcentsigner-as-signingmechanism) 46 | 47 | - [Sending a Payment with the SoloSigner as SigningMechanism](#sending-a-payment-with-the-solosigner-as-signingmechanism) 48 | 49 | - [Sending a Payment with the XummSigner as SigningMechanism](#sending-a-payment-with-the-xummsigner-as-signingmechanism) 50 | 51 | - [Sending a Payment with XRPL account and secret](#sending-a-payment-with-xrpl-account-and-secret) 52 | 53 | - [Sending a Payment with XRPL account and keypair](#sending-a-payment-with-xrpl-account-and-keypair) 54 | 55 | - [Event Emitter and Listeners](#event-emitter-and-listeners) 56 | 57 | - [Transactions](#transactions) 58 | 59 | 60 | 61 | 62 | 63 | ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/sologenic/sologenic-xrpl-stream-js) 64 | 65 | ![GitHub contributors](https://img.shields.io/github/contributors/sologenic/sologenic-xrpl-stream-js) 66 | 67 | ![GitHub commit activity](https://img.shields.io/github/commit-activity/w/sologenic/sologenic-xrpl-stream-js) 68 | 69 | # [ƨ Sologenic XRPL Stream](https://github.com/sologenic/sologenic-xrpl-stream-js) 70 | 71 | ## Purpose 72 | 73 | The sologenic-xrpl-stream-js enables clients to communicate and submit transactions to the XRP Ledger seamlessly and reliably. 74 | 75 | This library provides reliable transaction handling following the guide provided by XRPL.org [reliable transaction submissions](https://xrpl.org/reliable-transaction-submission.html). 76 | 77 | ![sologenic-xrpl-stream-js.png](assets/img/sologenic-xrpl-stream-js.png) 78 | 79 | Once a transaction is submitted, it is queued either using a Hash-based in-memory queue (non-persistent, ideal for front-end) or a Redis (persistent, ideal for backend). Transactions are queued and dispatched in sequence. Account sequence numbers, ledgerVersions and fees are also handled for each transaction that is being dispatched. 80 | 81 | Events are reported back to the client using a global EventEmitter and transaction-specific EventEmitter. This allows clients to track the statuses of their transactions and take actions based on the results. 82 | 83 | **Production uses** 84 | 85 | - SOLO ReactNative Wallet 86 | 87 | - SOLO React Electron Wallet 88 | 89 | In general, there are two types of users who would benefit from using this library: 90 | 91 | 1. Exchanges or users with large volumes of transactions who want to ensure they receive reliable delivery and can receive event notifications throughout the transactions validation process. 92 | 93 | 2) Users who do not want to deal with transaction dispatching, validations and errors on the XRPL. 94 | 95 | ## How to Participate 96 | 97 | We have a community for questions and support at [sologenic-dev.slack.com](https://sologenic-dev.slack.com). To receive an invite for the community please fill out the [form](https://docs.google.com/forms/d/e/1FAIpQLSdcpIL-u2FsqBZj0ikG7UyJe3l9If7sVr7MdTpVnINQJJbsQg/viewform) and we'll send you your invite link. 98 | 99 | ## Install 100 | 101 | ```bash 102 | 103 | $ npm install sologenic-xrpl-stream-js 104 | 105 | ``` 106 | 107 | ## Node Package Manager Scripts 108 | 109 | Each `npm` script defined in `package.json` can be run by simply running the command `npm run-script