fix: resolves TODOs in ./src (#1623)

fix: resolves TODOs in ./src
This commit is contained in:
Nathan Nichols
2021-10-15 17:18:22 -05:00
committed by GitHub
parent b71147416c
commit 6e57b4e2ee
32 changed files with 136 additions and 610 deletions

View File

@@ -1,51 +0,0 @@
'use strict';
const fs = require('fs');
const path = require('path');
const execSync = require('child_process').execSync;
const ejs = require('ejs');
const renderFromPaths =
require('json-schema-to-markdown-table').renderFromPaths;
const ROOT = path.dirname(path.normalize(__dirname));
function strip(string) {
return string.replace(/^\s+|\s+$/g, '');
}
function importFile(relativePath) {
const absolutePath = path.join(ROOT, relativePath);
return strip(fs.readFileSync(absolutePath).toString('utf-8'));
}
function renderFixture(fixtureRelativePath) {
const json = importFile(path.join('test', 'fixtures', fixtureRelativePath));
return '\n```json\n' + json + '\n```\n';
}
function renderSchema(schemaRelativePath) {
const schemasPath = path.join(ROOT, 'src', 'common', 'schemas');
const schemaPath = path.join(schemasPath, schemaRelativePath);
return renderFromPaths(schemaPath, schemasPath);
}
function main() {
const locals = {
importFile: importFile,
renderFixture: renderFixture,
renderSchema: renderSchema
};
const indexPath = path.join(ROOT, 'docs', 'src', 'index.md.ejs');
ejs.renderFile(indexPath, locals, function(error, output) {
if (error) {
console.error(error);
process.exit(1);
} else {
const outputPath = path.join(ROOT, 'docs', 'index.md');
fs.writeFileSync(outputPath, output);
execSync('npm run doctoc', {cwd: ROOT});
process.exit(0);
}
});
}
main();

View File

@@ -1,18 +0,0 @@
#!/bin/bash
function checkEOL {
local changedFiles=$(git --no-pager diff --name-only -M100% --diff-filter=AM --relative $(git merge-base FETCH_HEAD origin/HEAD) FETCH_HEAD)
local result=0
for name in $changedFiles; do
grep -c -U -q $'\r' $name
if [ $? -eq 0 ]; then
echo "windows eol found in $name" >&2
result=1
fi
done
if [ $result -eq 1 ]; then
false
fi
}
checkEOL

View File

@@ -1,98 +0,0 @@
#!/bin/bash -ex
NODE_INDEX="$1"
TOTAL_NODES="$2"
function checkEOL {
./scripts/checkeol.sh
}
lint() {
echo "tslint $(node_modules/.bin/tslint --version)"
npm run lint
}
unittest() {
# test "src"
# TODO: replace/upgrade mocha-junit-reporter
#mocha test --reporter mocha-junit-reporter --reporter-options mochaFile=$CIRCLE_TEST_REPORTS/test-results.xml
npm test --coverage
#npm run coveralls
# test compiled version in "dist/npm"
$(npm bin)/babel -D --optional runtime --ignore "**/node_modules/**" -d test-compiled/ test/
echo "--reporter spec --timeout 5000 --slow 500" > test-compiled/mocha.opts
mkdir -p test-compiled/node_modules
ln -nfs ../../dist/npm test-compiled/node_modules/xrpl-local
mocha --opts test-compiled/mocha.opts test-compiled
#compile tests for browser testing
#gulp build-min build-tests
#node --harmony test-compiled/mocked-server.js > /dev/null &
#echo "Running tests in PhantomJS"
#mocha-phantomjs test/localRunner.html
#echo "Running tests using minified version in PhantomJS"
#mocha-phantomjs test/localRunnerMin.html
#echo "Running tests in SauceLabs"
#http-server &
#npm run sauce
#pkill -f mocked-server.js
#pkill -f http-server
rm -rf test-compiled
}
integrationtest() {
mocha test/integration/integration.js
# run integration tests in PhantomJS
#gulp build-tests build-min
#echo "Running integragtion tests in PhantomJS"
#mocha-phantomjs test/localIntegrationRunner.html
}
doctest() {
mv docs/index.md docs/index.md.save
npm run docgen
mv docs/index.md docs/index.md.test
mv docs/index.md.save docs/index.md
cmp docs/index.md docs/index.md.test
rm docs/index.md.test
}
oneNode() {
checkEOL
doctest
lint
unittest
integrationtest
}
twoNodes() {
case "$NODE_INDEX" in
0) doctest; lint; integrationtest;;
1) checkEOL; unittest;;
*) echo "ERROR: invalid usage"; exit 2;;
esac
}
threeNodes() {
case "$NODE_INDEX" in
0) doctest; lint; integrationtest;;
1) checkEOL;;
2) unittest;;
*) echo "ERROR: invalid usage"; exit 2;;
esac
}
case "$TOTAL_NODES" in
"") oneNode;;
1) oneNode;;
2) twoNodes;;
3) threeNodes;;
*) echo "ERROR: invalid usage"; exit 2;;
esac

View File

@@ -1,18 +0,0 @@
echo "PUBLISH"
function exit_on_error {
res=$?
[[ ${res:-99} -eq 0 ]] || exit $res
}
rm -rf build
npm install
gulp
npm test
exit_on_error
echo ""
echo "publish to npm"
npm publish
exit_on_error

View File

@@ -1,18 +0,0 @@
echo "PUBLISH RELEASE CANDIDATE"
function exit_on_error {
res=$?
[[ ${res:-99} -eq 0 ]] || exit $res
}
rm -rf build
npm install
gulp
npm test
exit_on_error
echo ""
echo "publish rc to npm"
npm publish --tag beta
exit_on_error

View File

@@ -250,7 +250,7 @@ export class Connection extends EventEmitter {
this.ws = createWebSocket(this.url, this.config) this.ws = createWebSocket(this.url, this.config)
if (this.ws == null) { if (this.ws == null) {
throw new Error('Connect: created null websocket') throw new XrplError('Connect: created null websocket')
} }
this.ws.on('error', (error) => this.onConnectionFailed(error)) this.ws.on('error', (error) => this.onConnectionFailed(error))
@@ -422,7 +422,7 @@ export class Connection extends EventEmitter {
*/ */
private async onceOpen(connectionTimeoutID: NodeJS.Timeout): Promise<void> { private async onceOpen(connectionTimeoutID: NodeJS.Timeout): Promise<void> {
if (this.ws == null) { if (this.ws == null) {
throw new Error('onceOpen: ws is null') throw new XrplError('onceOpen: ws is null')
} }
// Once the connection completes successfully, remove all old listeners // Once the connection completes successfully, remove all old listeners
@@ -436,7 +436,7 @@ export class Connection extends EventEmitter {
// Handle a closed connection: reconnect if it was unexpected // Handle a closed connection: reconnect if it was unexpected
this.ws.once('close', (code, reason) => { this.ws.once('close', (code, reason) => {
if (this.ws == null) { if (this.ws == null) {
throw new Error('onceClose: ws is null') throw new XrplError('onceClose: ws is null')
} }
this.clearHeartbeatInterval() this.clearHeartbeatInterval()

View File

@@ -3,8 +3,7 @@
import * as assert from 'assert' import * as assert from 'assert'
import { EventEmitter } from 'events' import { EventEmitter } from 'events'
import { ValidationError, XrplError } from '../errors' import { NotFoundError, ValidationError, XrplError } from '../errors'
import * as errors from '../errors'
import { import {
Request, Request,
Response, Response,
@@ -85,18 +84,19 @@ import {
UnsubscribeResponse, UnsubscribeResponse,
} from '../models/methods' } from '../models/methods'
import { BaseRequest, BaseResponse } from '../models/methods/baseMethod' import { BaseRequest, BaseResponse } from '../models/methods/baseMethod'
import autofill from '../sugar/autofill'
import { getBalances, getXrpBalance } from '../sugar/balances'
import getFee from '../sugar/fee'
import getLedgerIndex from '../sugar/ledgerIndex'
import getOrderbook from '../sugar/orderbook'
import { import {
autofill,
ensureClassicAddress,
getLedgerIndex,
getOrderbook,
getFee,
getBalances,
getXrpBalance,
submit, submit,
submitSigned, submitSigned,
submitReliable, submitReliable,
submitSignedReliable, submitSignedReliable,
} from '../sugar/submit' } from '../sugar'
import { ensureClassicAddress } from '../sugar/utils'
import fundWallet from '../wallet/fundWallet' import fundWallet from '../wallet/fundWallet'
import { import {
@@ -394,7 +394,7 @@ class Client extends EventEmitter {
>(req: T, resp: U): Promise<U> { >(req: T, resp: U): Promise<U> {
if (!resp.result.marker) { if (!resp.result.marker) {
return Promise.reject( return Promise.reject(
new errors.NotFoundError('response does not have a next page'), new NotFoundError('response does not have a next page'),
) )
} }
const nextPageRequest = { ...req, marker: resp.result.marker } const nextPageRequest = { ...req, marker: resp.result.marker }

View File

@@ -33,7 +33,7 @@ export default class RequestManager {
public cancel(id: string | number): void { public cancel(id: string | number): void {
const promise = this.promisesAwaitingResponse.get(id) const promise = this.promisesAwaitingResponse.get(id)
if (promise == null) { if (promise == null) {
throw new Error(`No existing promise with id ${id}`) throw new XrplError(`No existing promise with id ${id}`)
} }
clearTimeout(promise.timer) clearTimeout(promise.timer)
this.deletePromise(id) this.deletePromise(id)
@@ -49,7 +49,7 @@ export default class RequestManager {
public resolve(id: string | number, response: Response): void { public resolve(id: string | number, response: Response): void {
const promise = this.promisesAwaitingResponse.get(id) const promise = this.promisesAwaitingResponse.get(id)
if (promise == null) { if (promise == null) {
throw new Error(`No existing promise with id ${id}`) throw new XrplError(`No existing promise with id ${id}`)
} }
clearTimeout(promise.timer) clearTimeout(promise.timer)
promise.resolve(response) promise.resolve(response)
@@ -66,7 +66,7 @@ export default class RequestManager {
public reject(id: string | number, error: Error): void { public reject(id: string | number, error: Error): void {
const promise = this.promisesAwaitingResponse.get(id) const promise = this.promisesAwaitingResponse.get(id)
if (promise == null) { if (promise == null) {
throw new Error(`No existing promise with id ${id}`) throw new XrplError(`No existing promise with id ${id}`)
} }
clearTimeout(promise.timer) clearTimeout(promise.timer)
// TODO: figure out how to have a better stack trace for an error // TODO: figure out how to have a better stack trace for an error

View File

@@ -1,8 +1,6 @@
/* eslint-disable max-classes-per-file -- Errors can be defined in the same file */ /* eslint-disable max-classes-per-file -- Errors can be defined in the same file */
import { inspect } from 'util' import { inspect } from 'util'
// TODO: replace all `new Error`s with `new XrplError`s
/** /**
* Base Error class for xrpl.js. All Errors thrown by xrpl.js should throw * Base Error class for xrpl.js. All Errors thrown by xrpl.js should throw
* XrplErrors. * XrplErrors.

View File

@@ -34,9 +34,11 @@ export interface Signer {
} }
export interface Memo { export interface Memo {
MemoData?: string Memo: {
MemoType?: string MemoData?: string
MemoFormat?: string MemoType?: string
MemoFormat?: string
}
} }
export type StreamType = export type StreamType =

View File

@@ -1,4 +1,4 @@
import { BaseRequest, BaseResponse } from './baseMethod' import type { BaseRequest, BaseResponse } from './baseMethod'
/** /**
* The ping command returns an acknowledgement, so that clients can test the * The ping command returns an acknowledgement, so that clients can test the
@@ -17,7 +17,5 @@ export interface PingRequest extends BaseRequest {
* @category Responses * @category Responses
*/ */
export interface PingResponse extends BaseResponse { export interface PingResponse extends BaseResponse {
// TODO: figure out if there's a better way to type this result: { role?: string; unlimited?: boolean }
// eslint-disable-next-line @typescript-eslint/ban-types -- actually should be an empty object
result: {}
} }

View File

@@ -1,5 +1,5 @@
import type { Amount, Currency, Path, StreamType } from '../common' import type { Amount, Currency, Path, StreamType } from '../common'
import Offer from '../ledger/offer' import { Offer } from '../ledger'
import { OfferCreate, Transaction } from '../transactions' import { OfferCreate, Transaction } from '../transactions'
import TransactionMetadata from '../transactions/metadata' import TransactionMetadata from '../transactions/metadata'
@@ -66,18 +66,17 @@ export interface SubscribeRequest extends BaseRequest {
url_password?: string url_password?: string
} }
type BooksSnapshot = Offer[]
/** /**
* Response expected from a {@link SubscribeRequest}. * Response expected from a {@link SubscribeRequest}.
* *
* @category Responses * @category Responses
*/ */
export interface SubscribeResponse extends BaseResponse { export interface SubscribeResponse extends BaseResponse {
// eslint-disable-next-line @typescript-eslint/ban-types -- actually should be an empty object result: Record<string, never> | LedgerStreamResponse | BooksSnapshot
result: {} | LedgerStreamResponse | BooksSnapshot
} }
type BooksSnapshot = Offer[]
interface BaseStream { interface BaseStream {
type: string type: string
} }

View File

@@ -1,6 +1,6 @@
import { Currency, StreamType } from '../common' import { Currency, StreamType } from '../common'
import { BaseRequest, BaseResponse } from './baseMethod' import type { BaseRequest, BaseResponse } from './baseMethod'
interface Book { interface Book {
taker_gets: Currency taker_gets: Currency

View File

@@ -144,11 +144,10 @@ export interface BaseTransaction {
* validated or rejected. * validated or rejected.
*/ */
LastLedgerSequence?: number LastLedgerSequence?: number
// TODO: Make Memo match the format of Signer (By including the Memo: wrapper inside the Interface)
/** /**
* Additional arbitrary information used to identify this transaction. * Additional arbitrary information used to identify this transaction.
*/ */
Memos?: Array<{ Memo: Memo }> Memos?: Memo[]
/** /**
* Array of objects that represent a multi-signature which authorizes this * Array of objects that represent a multi-signature which authorizes this
* transaction. * transaction.

View File

@@ -1,6 +1,7 @@
import BigNumber from 'bignumber.js' import BigNumber from 'bignumber.js'
import type { Client } from '..' import type { Client } from '..'
import { XrplError } from '../errors'
const NUM_DECIMAL_PLACES = 6 const NUM_DECIMAL_PLACES = 6
const BASE_10 = 10 const BASE_10 = 10
@@ -25,7 +26,7 @@ export default async function getFee(
const baseFee = serverInfo.validated_ledger?.base_fee_xrp const baseFee = serverInfo.validated_ledger?.base_fee_xrp
if (baseFee == null) { if (baseFee == null) {
throw new Error('getFee: Could not get base_fee_xrp from server_info') throw new XrplError('getFee: Could not get base_fee_xrp from server_info')
} }
const baseFeeXrp = new BigNumber(baseFee) const baseFeeXrp = new BigNumber(baseFee)

13
src/sugar/index.ts Normal file
View File

@@ -0,0 +1,13 @@
export { default as autofill } from './autofill'
export { getBalances, getXrpBalance } from './balances'
export { default as getFee } from './fee'
export { default as getLedgerIndex } from './ledgerIndex'
export { default as getOrderbook } from './orderbook'
export * from './submit'
export * from './utils'

View File

@@ -1,76 +0,0 @@
import { classicAddressToXAddress } from 'ripple-address-codec'
import keypairs from 'ripple-keypairs'
import ECDSA from '../ecdsa'
import { UnexpectedError } from '../errors'
export interface GeneratedAddress {
xAddress: string
classicAddress?: string
secret: string
}
interface GenerateAddressOptions {
// The entropy to use to generate the seed.
entropy?: Uint8Array | number[]
// The digital signature algorithm to generate an address for. Can be `ecdsa-secp256k1` (default) or `ed25519`.
algorithm?: ECDSA
/*
* Specifies whether the address is intended for use on a test network such as Testnet or Devnet.
* If `true`, the address should only be used for testing, and will start with `T`.
* If `false` (default), the address should only be used on mainnet, and will start with `X`.
*/
test?: boolean
// If `true`, return the classic address, in addition to the X-address.
includeClassicAddress?: boolean
}
/**
* TODO: Move this function to be a static function of the Wallet Class.
* TODO: Doc this function.
*
* @param options - Options for generating X-Address.
* @returns A generated address.
* @throws When cannot generate an address.
*/
function generateXAddress(
options: GenerateAddressOptions = {},
): GeneratedAddress {
try {
const generateSeedOptions: {
entropy?: Uint8Array
algorithm?: ECDSA
} = {
algorithm: options.algorithm,
}
if (options.entropy) {
generateSeedOptions.entropy = Uint8Array.from(options.entropy)
}
const secret = keypairs.generateSeed(generateSeedOptions)
const keypair = keypairs.deriveKeypair(secret)
const classicAddress = keypairs.deriveAddress(keypair.publicKey)
const returnValue: GeneratedAddress = {
xAddress: classicAddressToXAddress(
classicAddress,
false,
options.test ?? false,
),
secret,
}
if (options.includeClassicAddress) {
returnValue.classicAddress = classicAddress
}
return returnValue
} catch (error) {
if (error instanceof Error) {
throw new UnexpectedError(error.message)
}
throw error
}
}
export { generateXAddress }

View File

@@ -6,7 +6,7 @@
import BigNumber from 'bignumber.js' import BigNumber from 'bignumber.js'
import { decode, encode } from 'ripple-binary-codec' import { decode, encode } from 'ripple-binary-codec'
import { ValidationError } from '../../errors' import { ValidationError, XrplError } from '../../errors'
import type { Ledger } from '../../models/ledger' import type { Ledger } from '../../models/ledger'
import { LedgerEntry } from '../../models/ledger' import { LedgerEntry } from '../../models/ledger'
import { Transaction } from '../../models/transactions' import { Transaction } from '../../models/transactions'
@@ -61,7 +61,7 @@ function addLengthPrefix(hex: string): string {
]) + hex ]) + hex
) )
} }
throw new Error('Variable integer overflow.') throw new XrplError('Variable integer overflow.')
} }
/** /**
@@ -76,7 +76,6 @@ export function hashSignedTx(tx: Transaction | string): string {
let txObject: Transaction let txObject: Transaction
if (typeof tx === 'string') { if (typeof tx === 'string') {
txBlob = tx txBlob = tx
// TODO: type ripple-binary-codec with Transaction
/* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Required until updated in binary codec. */ /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- Required until updated in binary codec. */
txObject = decode(tx) as unknown as Transaction txObject = decode(tx) as unknown as Transaction
} else { } else {

View File

@@ -1,3 +1,4 @@
import { XrplError } from '../../../errors'
import hashPrefix from '../hashPrefix' import hashPrefix from '../hashPrefix'
import sha512Half from '../sha512Half' import sha512Half from '../sha512Half'
@@ -54,7 +55,7 @@ class InnerNode extends Node {
} else if (existingNode instanceof Leaf) { } else if (existingNode instanceof Leaf) {
if (existingNode.tag === tag) { if (existingNode.tag === tag) {
// Collision // Collision
throw new Error( throw new XrplError(
'Tried to add a node to a SHAMap that was already in there.', 'Tried to add a node to a SHAMap that was already in there.',
) )
} else { } else {
@@ -79,7 +80,7 @@ class InnerNode extends Node {
*/ */
public setNode(slot: number, node: Node): void { public setNode(slot: number, node: Node): void {
if (slot < 0 || slot > SLOT_MAX) { if (slot < 0 || slot > SLOT_MAX) {
throw new Error('Invalid slot: slot must be between 0-15.') throw new XrplError('Invalid slot: slot must be between 0-15.')
} }
this.leaves[slot] = node this.leaves[slot] = node
this.empty = false this.empty = false
@@ -94,7 +95,7 @@ class InnerNode extends Node {
*/ */
public getNode(slot: number): Node | undefined { public getNode(slot: number): Node | undefined {
if (slot < 0 || slot > SLOT_MAX) { if (slot < 0 || slot > SLOT_MAX) {
throw new Error('Invalid slot: slot must be between 0-15.') throw new XrplError('Invalid slot: slot must be between 0-15.')
} }
return this.leaves[slot] return this.leaves[slot]
} }

View File

@@ -1,3 +1,4 @@
import { XrplError } from '../../../errors'
import hashPrefix from '../hashPrefix' import hashPrefix from '../hashPrefix'
import sha512Half from '../sha512Half' import sha512Half from '../sha512Half'
@@ -30,13 +31,13 @@ class Leaf extends Node {
/** /**
* Add item to Leaf. * Add item to Leaf.
* *
* @param _tag - Index of the Node. * @param tag - Index of the Node.
* @param _node - Node to insert. * @param node - Node to insert.
* @throws When called, because LeafNodes cannot addItem. * @throws When called, because LeafNodes cannot addItem.
*/ */
// eslint-disable-next-line class-methods-use-this -- no `this` needed here public addItem(tag: string, node: Node): void {
public addItem(_tag: string, _node: Node): void { throw new XrplError('Cannot call addItem on a LeafNode')
throw new Error('Cannot call addItem on a LeafNode') this.addItem(tag, node)
} }
/** /**
@@ -60,7 +61,7 @@ class Leaf extends Node {
return sha512Half(txNodePrefix + this.data + this.tag) return sha512Half(txNodePrefix + this.data + this.tag)
} }
default: default:
throw new Error('Tried to hash a SHAMap node of unknown type.') throw new XrplError('Tried to hash a SHAMap node of unknown type.')
} }
} }
} }

View File

@@ -19,7 +19,6 @@ import { Response } from '../models/methods'
import getBalanceChanges from './balanceChanges' import getBalanceChanges from './balanceChanges'
import { deriveKeypair, deriveXAddress } from './derive' import { deriveKeypair, deriveXAddress } from './derive'
import { generateXAddress } from './generateAddress'
import { import {
hashSignedTx, hashSignedTx,
hashTx, hashTx,
@@ -124,7 +123,6 @@ export {
isValidSecret, isValidSecret,
isValidAddress, isValidAddress,
hashes, hashes,
generateXAddress,
deriveKeypair, deriveKeypair,
deriveXAddress, deriveXAddress,
signPaymentChannelClaim, signPaymentChannelClaim,

View File

@@ -5,12 +5,15 @@ import { isValidClassicAddress } from 'ripple-address-codec'
import type { Client } from '..' import type { Client } from '..'
import { RippledError, XRPLFaucetError } from '../errors' import { RippledError, XRPLFaucetError } from '../errors'
import { GeneratedAddress } from '../utils/generateAddress'
import Wallet from '.' import Wallet from '.'
interface FaucetWallet { interface FaucetWallet {
account: GeneratedAddress account: {
xAddress: string
classicAddress?: string
secret: string
}
amount: number amount: number
balance: number balance: number
} }

View File

@@ -11,8 +11,7 @@ import { sign as signWithKeypair, verify } from 'ripple-keypairs'
import { ValidationError } from '../errors' import { ValidationError } from '../errors'
import { Signer } from '../models/common' import { Signer } from '../models/common'
import { Transaction } from '../models/transactions' import { Transaction, validate } from '../models/transactions'
import { validateBaseTransaction } from '../models/transactions/common'
import Wallet from '.' import Wallet from '.'
@@ -37,11 +36,9 @@ function multisign(transactions: Array<Transaction | string>): string {
/* /*
* This will throw a more clear error for JS users if any of the supplied transactions has incorrect formatting * This will throw a more clear error for JS users if any of the supplied transactions has incorrect formatting
* TODO: Replace this with validate() (The general validation function for all Transactions)
* also make validate accept '| Transaction' to avoid type casting here.
*/ */
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- validate does not accept Transaction type // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- validate does not accept Transaction type
validateBaseTransaction(tx as unknown as Record<string, unknown>) validate(tx as unknown as Record<string, unknown>)
if (tx.Signers == null || tx.Signers.length === 0) { if (tx.Signers == null || tx.Signers.length === 0) {
throw new ValidationError( throw new ValidationError(
"For multisigning all transactions must include a Signers field containing an array of signatures. You may have forgotten to pass the 'forMultisign' parameter when signing.", "For multisigning all transactions must include a Signers field containing an array of signatures. You may have forgotten to pass the 'forMultisign' parameter when signing.",
@@ -162,7 +159,9 @@ function addressToBigNumber(address: string): BigNumber {
function getDecodedTransaction(txOrBlob: Transaction | string): Transaction { function getDecodedTransaction(txOrBlob: Transaction | string): Transaction {
if (typeof txOrBlob === 'object') { if (typeof txOrBlob === 'object') {
return txOrBlob // We need this to handle X-addresses in multisigning
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- We are casting here to get strong typing
return decode(encode(txOrBlob)) as unknown as Transaction
} }
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- We are casting here to get strong typing // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- We are casting here to get strong typing

View File

@@ -1,8 +1,9 @@
import BigNumber from 'bignumber.js' import BigNumber from 'bignumber.js'
import { assert } from 'chai' import { assert } from 'chai'
import { BookOffersRequest, ValidationError } from 'xrpl-local' import { BookOffersRequest } from 'xrpl-local'
import { OfferFlags } from 'xrpl-local/models/ledger/offer' import { ValidationError, XrplError } from 'xrpl-local/errors'
import { OfferFlags } from 'xrpl-local/models/ledger'
import requests from '../fixtures/requests' import requests from '../fixtures/requests'
import responses from '../fixtures/responses' import responses from '../fixtures/responses'
@@ -52,7 +53,7 @@ function normalRippledResponse(
) { ) {
return rippled.book_offers.fabric.requestBookOffersAsksResponse(request) return rippled.book_offers.fabric.requestBookOffersAsksResponse(request)
} }
throw new Error('unexpected end') throw new XrplError('unexpected end')
} }
function xrpRippledResponse( function xrpRippledResponse(

View File

@@ -255,7 +255,7 @@ describe('Connection', function () {
// overload websocket send on open when _ws exists // overload websocket send on open when _ws exists
this.client.connection.ws.send = function (_0, _1, _2): void { this.client.connection.ws.send = function (_0, _1, _2): void {
// recent ws throws this error instead of calling back // recent ws throws this error instead of calling back
throw new Error('WebSocket is not open: readyState 0 (CONNECTING)') throw new XrplError('WebSocket is not open: readyState 0 (CONNECTING)')
} }
const request = { command: 'subscribe', streams: ['ledger'] } const request = { command: 'subscribe', streams: ['ledger'] }
this.client.connection.request(request) this.client.connection.request(request)
@@ -340,20 +340,22 @@ describe('Connection', function () {
if (connectsCount === num) { if (connectsCount === num) {
if (disconnectsCount !== num) { if (disconnectsCount !== num) {
done( done(
new Error( new XrplError(
`disconnectsCount must be equal to ${num}(got ${disconnectsCount} instead)`, `disconnectsCount must be equal to ${num}(got ${disconnectsCount} instead)`,
), ),
) )
} else if (reconnectsCount !== num) { } else if (reconnectsCount !== num) {
done( done(
new Error( new XrplError(
`reconnectsCount must be equal to ${num} (got ${reconnectsCount} instead)`, `reconnectsCount must be equal to ${num} (got ${reconnectsCount} instead)`,
), ),
) )
// eslint-disable-next-line no-negated-condition -- Necessary // eslint-disable-next-line no-negated-condition -- Necessary
} else if (code !== 1006) { } else if (code !== 1006) {
done( done(
new Error(`disconnect must send code 1006 (got ${code} instead)`), new XrplError(
`disconnect must send code 1006 (got ${code} instead)`,
),
) )
} else { } else {
done() done()
@@ -399,14 +401,14 @@ describe('Connection', function () {
this.timeout(5000) this.timeout(5000)
// fail on reconnect/connection // fail on reconnect/connection
this.client.connection.reconnect = async (): Promise<void> => { this.client.connection.reconnect = async (): Promise<void> => {
throw new Error('error on reconnect') throw new XrplError('error on reconnect')
} }
// Hook up a listener for the reconnect error event // Hook up a listener for the reconnect error event
this.client.on('error', (error, message) => { this.client.on('error', (error, message) => {
if (error === 'reconnect' && message === 'error on reconnect') { if (error === 'reconnect' && message === 'error on reconnect') {
return done() return done()
} }
return done(new Error('Expected error on reconnect')) return done(new XrplError('Expected error on reconnect'))
}) })
// Trigger a heartbeat // Trigger a heartbeat
this.client.connection.heartbeat() this.client.connection.heartbeat()
@@ -422,7 +424,7 @@ describe('Connection', function () {
it('should emit disconnected event with code 1006 (CLOSE_ABNORMAL)', function (done) { it('should emit disconnected event with code 1006 (CLOSE_ABNORMAL)', function (done) {
this.client.connection.once('error', (error) => { this.client.connection.once('error', (error) => {
done(new Error(`should not throw error, got ${String(error)}`)) done(new XrplError(`should not throw error, got ${String(error)}`))
}) })
this.client.connection.once('disconnected', (code) => { this.client.connection.once('disconnected', (code) => {
assert.strictEqual(code, 1006) assert.strictEqual(code, 1006)
@@ -597,14 +599,14 @@ describe('Connection', function () {
it('should try to reconnect on empty subscribe response on reconnect', function (done) { it('should try to reconnect on empty subscribe response on reconnect', function (done) {
this.timeout(23000) this.timeout(23000)
this.client.on('error', (error) => { this.client.on('error', (error) => {
done(error || new Error('Should not emit error.')) done(error || new XrplError('Should not emit error.'))
}) })
let disconnectedCount = 0 let disconnectedCount = 0
this.client.on('connected', () => { this.client.on('connected', () => {
done( done(
disconnectedCount === 1 disconnectedCount === 1
? undefined ? undefined
: new Error('Wrong number of disconnects'), : new XrplError('Wrong number of disconnects'),
) )
}) })
this.client.on('disconnected', () => { this.client.on('disconnected', () => {
@@ -622,7 +624,7 @@ describe('Connection', function () {
.request({ .request({
command: 'test_garbage', command: 'test_garbage',
}) })
.then(() => new Error('Should not have succeeded')) .then(() => new XrplError('Should not have succeeded'))
.catch(done()) .catch(done())
}) })

View File

@@ -19,12 +19,11 @@ describe('Utility method integration tests', function () {
const response = await (this.client as Client).request({ const response = await (this.client as Client).request({
command: 'ping', command: 'ping',
}) })
const expected = { const expected: unknown = {
id: 0,
result: { role: 'admin', unlimited: true }, result: { role: 'admin', unlimited: true },
type: 'response', type: 'response',
} }
assert.deepEqual(_.omit(response, 'id'), _.omit(expected, 'id')) assert.deepEqual(_.omit(response, 'id'), expected)
}) })
it('random', async function () { it('random', async function () {

View File

@@ -3,6 +3,7 @@ import _ from 'lodash'
import { Server as WebSocketServer } from 'ws' import { Server as WebSocketServer } from 'ws'
import type { Request } from '../src' import type { Request } from '../src'
import { XrplError } from '../src/errors'
import type { import type {
BaseResponse, BaseResponse,
ErrorResponse, ErrorResponse,
@@ -15,7 +16,7 @@ function createResponse(
response: Record<string, unknown>, response: Record<string, unknown>,
): string { ): string {
if (!('type' in response) && !('error' in response)) { if (!('type' in response) && !('error' in response)) {
throw new Error( throw new XrplError(
`Bad response format. Must contain \`type\` or \`error\`. ${JSON.stringify( `Bad response format. Must contain \`type\` or \`error\`. ${JSON.stringify(
response, response,
)}`, )}`,
@@ -65,10 +66,10 @@ export default function createMockRippled(port: number): MockedWebSocketServer {
try { try {
request = JSON.parse(requestJSON) request = JSON.parse(requestJSON)
if (request.id == null) { if (request.id == null) {
throw new Error(`Request has no id: ${requestJSON}`) throw new XrplError(`Request has no id: ${requestJSON}`)
} }
if (request.command == null) { if (request.command == null) {
throw new Error(`Request has no command: ${requestJSON}`) throw new XrplError(`Request has no id: ${requestJSON}`)
} }
if (request.command === 'ping') { if (request.command === 'ping') {
ping(conn, request) ping(conn, request)
@@ -77,7 +78,7 @@ export default function createMockRippled(port: number): MockedWebSocketServer {
} else if (request.command in mock.responses) { } else if (request.command in mock.responses) {
conn.send(createResponse(request, mock.getResponse(request))) conn.send(createResponse(request, mock.getResponse(request)))
} else { } else {
throw new Error( throw new XrplError(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions -- We know it's there // eslint-disable-next-line @typescript-eslint/restrict-template-expressions -- We know it's there
`No event handler registered in mock rippled for ${request.command}`, `No event handler registered in mock rippled for ${request.command}`,
) )
@@ -117,14 +118,14 @@ export default function createMockRippled(port: number): MockedWebSocketServer {
| ((r: Request) => Response | ErrorResponse), | ((r: Request) => Response | ErrorResponse),
): void { ): void {
if (typeof command !== 'string') { if (typeof command !== 'string') {
throw new Error('command is not a string') throw new XrplError('command is not a string')
} }
if ( if (
typeof response === 'object' && typeof response === 'object' &&
!('type' in response) && !('type' in response) &&
!('error' in response) !('error' in response)
) { ) {
throw new Error( throw new XrplError(
`Bad response format. Must contain \`type\` or \`error\`. ${JSON.stringify( `Bad response format. Must contain \`type\` or \`error\`. ${JSON.stringify(
response, response,
)}`, )}`,
@@ -135,7 +136,7 @@ export default function createMockRippled(port: number): MockedWebSocketServer {
mock.getResponse = (request: Request): Record<string, unknown> => { mock.getResponse = (request: Request): Record<string, unknown> => {
if (!(request.command in mock.responses)) { if (!(request.command in mock.responses)) {
throw new Error(`No handler for ${request.command}`) throw new XrplError(`No handler for ${request.command}`)
} }
const functionOrObject = mock.responses[request.command] const functionOrObject = mock.responses[request.command]
if (typeof functionOrObject === 'function') { if (typeof functionOrObject === 'function') {

View File

@@ -40,6 +40,35 @@ describe('Payment', function () {
assert.doesNotThrow(() => validate(paymentTransaction)) assert.doesNotThrow(() => validate(paymentTransaction))
}) })
it(`Verifies memos correctly`, function () {
paymentTransaction.Memos = [
{
Memo: {
MemoData: '32324324',
},
},
]
assert.doesNotThrow(() => validate(paymentTransaction))
})
it(`Verifies memos correctly`, function () {
paymentTransaction.Memos = [
{
Memo: {
MemoData: '32324324',
MemoType: 121221,
},
},
]
assert.throws(
() => validate(paymentTransaction),
ValidationError,
'BaseTransaction: invalid Memos',
)
})
it(`throws when Amount is missing`, function () { it(`throws when Amount is missing`, function () {
delete paymentTransaction.Amount delete paymentTransaction.Amount
assert.throws( assert.throws(

View File

@@ -4,6 +4,8 @@ import path from 'path'
import { Client } from 'xrpl-local' import { Client } from 'xrpl-local'
import { XrplError } from '../src/errors'
/** /**
* Client Test Runner. * Client Test Runner.
* *
@@ -27,10 +29,10 @@ describe('Client', function () {
) )
for (const methodName of allPublicMethods) { for (const methodName of allPublicMethods) {
if (!allTestedMethods.has(methodName)) { if (!allTestedMethods.has(methodName)) {
// TODO: Once migration is complete, remove `.skip()` so that missing tests are reported as failures. /** TODO: Remove the skip, rename methods. */
// eslint-disable-next-line mocha/no-skipped-tests -- See above TODO // eslint-disable-next-line mocha/no-skipped-tests -- skip these tests for now.
it.skip(`${methodName} - no test suite found`, function () { it.skip(`${methodName} - no test suite found`, function () {
throw new Error( throw new XrplError(
`Test file not found! Create file "test/client/${methodName}.ts".`, `Test file not found! Create file "test/client/${methodName}.ts".`,
) )
}) })

View File

@@ -10,17 +10,15 @@ const HEX_ZERO =
/** /**
* Generates data to hash for testing. * Generates data to hash for testing.
* *
* @param v - TODO: fill in. * @param int - TODO: fill in.
* @returns TODO: fill in. * @returns TODO: fill in.
*/ */
// eslint-disable-next-line id-length -- TODO: figure out what this variable means function intToVuc(int: number): string {
function intToVuc(v: number): string {
let ret = '' let ret = ''
// eslint-disable-next-line id-length -- TODO: figure out what this variable means for (let it = 0; it < 32; it++) {
for (let i = 0; i < 32; i++) {
ret += '0' ret += '0'
ret += v.toString(16).toUpperCase() ret += int.toString(16).toUpperCase()
} }
return ret return ret
} }

View File

@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any -- Necessary for these methods TODO: further cleanup */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any -- required for
/* eslint-disable @typescript-eslint/explicit-module-boundary-types -- Necessary for these methods TODO: further cleanup */ assertions. */
import net from 'net' import net from 'net'
import { assert } from 'chai' import { assert } from 'chai'
@@ -31,8 +31,8 @@ export function assertResultMatch(
if (expected.txJSON) { if (expected.txJSON) {
assert(response.txJSON) assert(response.txJSON)
assert.deepEqual( assert.deepEqual(
JSON.parse(response.txJSON as string), JSON.parse(response.txJSON),
JSON.parse(expected.txJSON as string), JSON.parse(expected.txJSON),
'checkResult: txJSON must match', 'checkResult: txJSON must match',
) )
} }
@@ -58,7 +58,7 @@ export function assertResultMatch(
* @param message - Expected error message/substring of the error message. * @param message - Expected error message/substring of the error message.
*/ */
export async function assertRejects( export async function assertRejects(
promise: PromiseLike<any>, promise: PromiseLike<unknown>,
instanceOf: any, instanceOf: any,
message?: string | RegExp, message?: string | RegExp,
): Promise<void> { ): Promise<void> {

View File

@@ -1,238 +0,0 @@
import { assert } from 'chai'
import { UnexpectedError } from 'xrpl-local'
import ECDSA from 'xrpl-local/ecdsa'
import { generateXAddress } from 'xrpl-local/utils/generateAddress'
import responses from '../fixtures/responses'
describe('generateAddress', function () {
it('generateAddress', function () {
assert.deepEqual(
generateXAddress({ entropy: new Array(16).fill(0) }),
// THEN we get the expected return value
responses.generateXAddress,
)
})
it('generateAddress invalid entropy', function () {
assert.throws(() => {
/*
* GIVEN entropy of 1 byte
* WHEN generating an address
*/
generateXAddress({ entropy: new Array(1).fill(0) })
/*
* THEN an UnexpectedError is thrown
* because 16 bytes of entropy are required
*/
}, UnexpectedError)
})
it('generateAddress with no options object', function () {
// GIVEN no options
// WHEN generating an address
const account = generateXAddress()
// THEN we get an object with an xAddress starting with 'x' and a secret starting with 's'
assert(account.xAddress.startsWith('X'), 'Address must start with `X`')
assert(account.secret.startsWith('s'), 'Secret must start with `s`')
})
it('generateAddress with empty options object', function () {
// GIVEN an empty options object
const options = {}
// WHEN generating an address
const account = generateXAddress(options)
// THEN we get an object with an xAddress starting with 'x' and a secret starting with 's'
assert(account.xAddress.startsWith('X'), 'Address must start with `X`')
assert(account.secret.startsWith('s'), 'Secret must start with `s`')
})
it('generateAddress with algorithm `ecdsa-secp256k1`', function () {
// GIVEN we want to use 'ecdsa-secp256k1'
const options = {
algorithm: ECDSA.secp256k1,
includeClassicAddress: true,
}
// WHEN generating an address
const account = generateXAddress(options)
// THEN we get an object with an address starting with 'r' and a secret starting with 's' (not 'sEd')
assert(
account.classicAddress?.startsWith('r'),
'Address must start with `r`',
)
assert.deepEqual(
account.secret.slice(0, 1),
's',
`Secret ${account.secret} must start with 's'`,
)
assert.notStrictEqual(
account.secret.slice(0, 3),
'sEd',
`secp256k1 secret ${account.secret} must not start with 'sEd'`,
)
})
it('generateAddress with algorithm `ed25519`', function () {
// GIVEN we want to use 'ed25519'
const options = {
algorithm: ECDSA.ed25519,
includeClassicAddress: true,
}
// WHEN generating an address
const account = generateXAddress(options)
// THEN we get an object with an address starting with 'r' and a secret starting with 'sEd'
assert(
account.classicAddress?.startsWith('r'),
'Address must start with `r`',
)
assert.deepEqual(
account.secret.slice(0, 3),
'sEd',
`Ed25519 secret ${account.secret} must start with 'sEd'`,
)
})
it('generateAddress with algorithm `ecdsa-secp256k1` and given entropy', function () {
// GIVEN we want to use 'ecdsa-secp256k1' with entropy of zero
const options = {
algorithm: ECDSA.secp256k1,
entropy: new Array(16).fill(0),
}
// WHEN generating an address
const account = generateXAddress(options)
// THEN we get the expected return value
assert.deepEqual(account, responses.generateXAddress)
})
it('generateAddress with algorithm `ed25519` and given entropy', function () {
// GIVEN we want to use 'ed25519' with entropy of zero
const options = {
algorithm: ECDSA.ed25519,
entropy: new Array(16).fill(0),
}
// WHEN generating an address
const account = generateXAddress(options)
// THEN we get the expected return value
assert.deepEqual(account, {
// generateAddress return value always includes xAddress to encourage X-address adoption
xAddress: 'X7xq1YJ4xmLSGGLhuakFQB9CebWYthQkgsvFC4LGFH871HB',
secret: 'sEdSJHS4oiAdz7w2X2ni1gFiqtbJHqE',
})
})
it('generateAddress with algorithm `ecdsa-secp256k1` and given entropy; include classic address', function () {
// GIVEN we want to use 'ecdsa-secp256k1' with entropy of zero
const options = {
algorithm: ECDSA.secp256k1,
entropy: new Array(16).fill(0),
includeClassicAddress: true,
}
// WHEN generating an address
const account = generateXAddress(options)
// THEN we get the expected return value
assert.deepEqual(account, responses.generateAddress)
})
it('generateAddress with algorithm `ed25519` and given entropy; include classic address', function () {
// GIVEN we want to use 'ed25519' with entropy of zero
const options = {
algorithm: ECDSA.ed25519,
entropy: new Array(16).fill(0),
includeClassicAddress: true,
}
// WHEN generating an address
const account = generateXAddress(options)
// THEN we get the expected return value
assert.deepEqual(account, {
// generateAddress return value always includes xAddress to encourage X-address adoption
xAddress: 'X7xq1YJ4xmLSGGLhuakFQB9CebWYthQkgsvFC4LGFH871HB',
secret: 'sEdSJHS4oiAdz7w2X2ni1gFiqtbJHqE',
classicAddress: 'r9zRhGr7b6xPekLvT6wP4qNdWMryaumZS7',
})
})
it('generateAddress with algorithm `ecdsa-secp256k1` and given entropy; include classic address; for test network use', function () {
// GIVEN we want to use 'ecdsa-secp256k1' with entropy of zero
const options = {
algorithm: ECDSA.secp256k1,
entropy: new Array(16).fill(0),
includeClassicAddress: true,
test: true,
}
// WHEN generating an address
const account = generateXAddress(options)
// THEN we get the expected return value
const response = {
// generateAddress return value always includes xAddress to encourage X-address adoption
...responses.generateAddress,
xAddress: 'TVG3TcCD58BD6MZqsNuTihdrhZwR8SzvYS8U87zvHsAcNw4',
}
assert.deepEqual(account, response)
})
it('generateAddress with algorithm `ed25519` and given entropy; include classic address; for test network use', function () {
// GIVEN we want to use 'ed25519' with entropy of zero
const options = {
algorithm: ECDSA.ed25519,
entropy: new Array(16).fill(0),
includeClassicAddress: true,
test: true,
}
// WHEN generating an address
const account = generateXAddress(options)
// THEN we get the expected return value
assert.deepEqual(account, {
// generateAddress return value always includes xAddress to encourage X-address adoption
xAddress: 'T7t4HeTMF5tT68agwuVbJwu23ssMPeh8dDtGysZoQiij1oo',
secret: 'sEdSJHS4oiAdz7w2X2ni1gFiqtbJHqE',
classicAddress: 'r9zRhGr7b6xPekLvT6wP4qNdWMryaumZS7',
})
})
it('generateAddress for test network use', function () {
// GIVEN we want an address for test network use
const options = { test: true }
// WHEN generating an address
const account = generateXAddress(options)
// THEN we get an object with xAddress starting with 'T' and a secret starting with 's'
// generateAddress return value always includes xAddress to encourage X-address adoption
assert.deepEqual(
account.xAddress.slice(0, 1),
'T',
'Test addresses start with T',
)
assert.deepEqual(
account.secret.slice(0, 1),
's',
`Secret ${account.secret} must start with 's'`,
)
})
})