Refactor generateFaucetWallet to return a Wallet (#1564)

* add Wallet.generate() and 

* return Wallet in generateFaucetWallet

* refactor Wallet tests

* rename wallet-generation.ts to generateFaucetWallet.ts

* rename and move Wallet.ts to src/wallet/index.ts and update webpack config
This commit is contained in:
Omar Khan
2021-09-01 17:41:43 -04:00
committed by Mayukha Vadari
parent 62538a75b1
commit e0f4d99d86
8 changed files with 116 additions and 48 deletions

View File

@@ -115,7 +115,7 @@ import prepareTrustline from "../transaction/trustline";
import { TransactionJSON, Instructions, Prepare } from "../transaction/types"; import { TransactionJSON, Instructions, Prepare } from "../transaction/types";
import * as transactionUtils from "../transaction/utils"; import * as transactionUtils from "../transaction/utils";
import { deriveAddress, deriveXAddress } from "../utils/derive"; import { deriveAddress, deriveXAddress } from "../utils/derive";
import generateFaucetWallet from "../wallet/wallet-generation"; import generateFaucetWallet from "../wallet/generateFaucetWallet";
import { Connection, ConnectionUserOptions } from "./connection"; import { Connection, ConnectionUserOptions } from "./connection";

View File

@@ -11,4 +11,4 @@ export * from "./utils";
// Broadcast client is experimental // Broadcast client is experimental
export { BroadcastClient } from "./client/broadcastClient"; export { BroadcastClient } from "./client/broadcastClient";
export * from "./Wallet"; export { default as Wallet } from "./wallet";

View File

@@ -3,11 +3,10 @@ import _ from "lodash";
import binaryCodec from "ripple-binary-codec"; import binaryCodec from "ripple-binary-codec";
import keypairs from "ripple-keypairs"; import keypairs from "ripple-keypairs";
import { Client } from ".."; import { Client, Wallet } from "..";
import { SignedTransaction } from "../common/types/objects"; import { SignedTransaction } from "../common/types/objects";
import { xrpToDrops } from "../utils"; import { xrpToDrops } from "../utils";
import { computeBinaryTransactionHash } from "../utils/hashes"; import { computeBinaryTransactionHash } from "../utils/hashes";
import Wallet from "../Wallet";
import { SignOptions, KeyPair, TransactionJSON } from "./types"; import { SignOptions, KeyPair, TransactionJSON } from "./types";
import * as utils from "./utils"; import * as utils from "./utils";

View File

@@ -1,6 +1,6 @@
import https = require("https"); import https = require("https");
import { Client } from ".."; import { Client, Wallet } from "..";
import { errors } from "../common"; import { errors } from "../common";
import { RippledError } from "../common/errors"; import { RippledError } from "../common/errors";
import { isValidAddress } from "../common/schema-validator"; import { isValidAddress } from "../common/schema-validator";
@@ -23,41 +23,44 @@ const MAX_ATTEMPTS = 20; // Maximum attempts to retrieve a balance
/** /**
* Generates a random wallet with some amount of XRP (usually 1000 XRP). * Generates a random wallet with some amount of XRP (usually 1000 XRP).
* *
* @param this * @param client - Client.
* @param address - An existing XRPL address to fund, if undefined, a new wallet will be created. * @param wallet - An existing XRPL Wallet to fund, if undefined, a new Wallet will be created.
* @returns A Wallet on the Testnet or Devnet that contains some amount of XRP. * @returns A Wallet on the Testnet or Devnet that contains some amount of XRP.
* @throws When either Client isn't connected or unable to fund wallet address.
*/ */
async function generateFaucetWallet( async function generateFaucetWallet(
this: Client, client: Client,
address?: string wallet?: Wallet
): Promise<FaucetWallet | void> { ): Promise<Wallet | void> {
if (!this.isConnected()) { if (!client.isConnected()) {
throw new RippledError("Client not connected, cannot call faucet"); throw new RippledError("Client not connected, cannot call faucet");
} }
// Initialize some variables // Generate a new Wallet if no existing Wallet is provided or its address is invalid to fund
let body: Uint8Array | undefined; const fundWallet =
let startingBalance = 0; wallet && isValidAddress(wallet.classicAddress)
const faucetUrl = getFaucetUrl(this); ? wallet
: Wallet.generate();
// If the user provides an existing wallet to fund // Create the POST request body
if (address && isValidAddress(address)) { const body: Uint8Array | undefined = new TextEncoder().encode(
// Create the POST request body JSON.stringify({
body = new TextEncoder().encode( destination: fundWallet.classicAddress,
JSON.stringify({ })
destination: address, );
}) // Retrieve the existing account balance
); const addressToFundBalance = await getAddressXrpBalance(
// Retrieve the existing account balance client,
const addressToFundBalance = await getAddressXrpBalance(this, address); fundWallet.classicAddress
);
// Check the address balance is not undefined and is a number // Check the address balance is not undefined and is a number
if (addressToFundBalance && !isNaN(Number(addressToFundBalance))) { const startingBalance =
startingBalance = Number(addressToFundBalance); addressToFundBalance && !isNaN(Number(addressToFundBalance))
} else { ? Number(addressToFundBalance)
startingBalance = 0; : 0;
}
} const faucetUrl = getFaucetUrl(client);
// Options to pass to https.request // Options to pass to https.request
const options = { const options = {
@@ -82,20 +85,20 @@ async function generateFaucetWallet(
// "application/json; charset=utf-8" // "application/json; charset=utf-8"
if (response.headers["content-type"]?.startsWith("application/json")) { if (response.headers["content-type"]?.startsWith("application/json")) {
const wallet: FaucetWallet = JSON.parse(body); const faucetWallet: FaucetWallet = JSON.parse(body);
const classicAddress = wallet.account.classicAddress; const classicAddress = faucetWallet.account.classicAddress;
if (classicAddress) { if (classicAddress) {
try { try {
// Check at regular interval if the address is enabled on the XRPL and funded // Check at regular interval if the address is enabled on the XRPL and funded
const isFunded = await hasAddressBalanceIncreased( const isFunded = await hasAddressBalanceIncreased(
this, client,
classicAddress, classicAddress,
startingBalance startingBalance
); );
if (isFunded) { if (isFunded) {
resolve(wallet); resolve(fundWallet);
} else { } else {
reject( reject(
new errors.XRPLFaucetError( new errors.XRPLFaucetError(

View File

@@ -9,11 +9,11 @@ import {
verify, verify,
} from "ripple-keypairs"; } from "ripple-keypairs";
import ECDSA from "./common/ecdsa"; import ECDSA from "../common/ecdsa";
import { ValidationError } from "./common/errors"; import { ValidationError } from "../common/errors";
import { SignedTransaction } from "./common/types/objects"; import { SignedTransaction } from "../common/types/objects";
import { signOffline } from "./transaction/sign"; import { signOffline } from "../transaction/sign";
import { SignOptions } from "./transaction/types"; import { SignOptions } from "../transaction/types";
/** /**
* A utility for deriving a wallet composed of a keypair (publicKey/privateKey). * A utility for deriving a wallet composed of a keypair (publicKey/privateKey).
@@ -23,12 +23,27 @@ import { SignOptions } from "./transaction/types";
class Wallet { class Wallet {
readonly publicKey: string; readonly publicKey: string;
readonly privateKey: string; readonly privateKey: string;
readonly classicAddress: string;
readonly seed?: string;
private static readonly defaultAlgorithm: ECDSA = ECDSA.ed25519; private static readonly defaultAlgorithm: ECDSA = ECDSA.ed25519;
private static readonly defaultDerivationPath: string = "m/44'/144'/0'/0/0"; private static readonly defaultDerivationPath: string = "m/44'/144'/0'/0/0";
constructor(publicKey: string, privateKey: string) { constructor(publicKey: string, privateKey: string, seed?: string) {
this.publicKey = publicKey; this.publicKey = publicKey;
this.privateKey = privateKey; this.privateKey = privateKey;
this.classicAddress = deriveAddress(publicKey);
this.seed = seed;
}
/**
* Generates a new Wallet using a generated seed.
*
* @param algorithm - The digital signature algorithm to generate an address for.
* @returns A new Wallet derived from a generated seed.
*/
static generate(algorithm: ECDSA = Wallet.defaultAlgorithm): Wallet {
const seed = generateSeed({ algorithm });
return Wallet.fromSeed(seed);
} }
/** /**
@@ -98,7 +113,7 @@ class Wallet {
algorithm: ECDSA = Wallet.defaultAlgorithm algorithm: ECDSA = Wallet.defaultAlgorithm
): Wallet { ): Wallet {
const { publicKey, privateKey } = deriveKeypair(seed, { algorithm }); const { publicKey, privateKey } = deriveKeypair(seed, { algorithm });
return new Wallet(publicKey, privateKey); return new Wallet(publicKey, privateKey, seed);
} }
/** /**
@@ -136,7 +151,7 @@ class Wallet {
* @returns An X-address. * @returns An X-address.
*/ */
getXAddress(tag: number, test = false): string { getXAddress(tag: number, test = false): string {
return classicAddressToXAddress(deriveAddress(this.publicKey), tag, test); return classicAddressToXAddress(this.classicAddress, tag, test);
} }
} }

View File

@@ -1,7 +1,7 @@
import { assert } from "chai"; import { assert } from "chai";
import ECDSA from "../../src/common/ecdsa"; import ECDSA from "../../src/common/ecdsa";
import Wallet from "../../src/Wallet"; import Wallet from "../../src/wallet";
/** /**
* Wallet testing. * Wallet testing.
@@ -9,6 +9,49 @@ import Wallet from "../../src/Wallet";
* Provides tests for Wallet class. * Provides tests for Wallet class.
*/ */
describe("Wallet", function () { describe("Wallet", function () {
describe("generate", function () {
const classicAddressPrefix = "r";
const ed25519KeyPrefix = "ED";
const secp256k1PrivateKeyPrefix = "00";
it("generates a new wallet using default algorithm", function () {
const wallet = Wallet.generate();
assert.isString(wallet.publicKey);
assert.isString(wallet.privateKey);
assert.isString(wallet.classicAddress);
assert.isString(wallet.seed);
assert.isTrue(wallet.publicKey.startsWith(ed25519KeyPrefix));
assert.isTrue(wallet.privateKey.startsWith(ed25519KeyPrefix));
assert.isTrue(wallet.classicAddress.startsWith(classicAddressPrefix));
});
it("generates a new wallet using algorithm ecdsa-secp256k1", function () {
const algorithm = ECDSA.secp256k1;
const wallet = Wallet.generate(algorithm);
assert.isString(wallet.publicKey);
assert.isString(wallet.privateKey);
assert.isString(wallet.classicAddress);
assert.isString(wallet.seed);
assert.isTrue(wallet.privateKey.startsWith(secp256k1PrivateKeyPrefix));
assert.isTrue(wallet.classicAddress.startsWith(classicAddressPrefix));
});
it("generates a new wallet using algorithm ed25519", function () {
const algorithm = ECDSA.ed25519;
const wallet = Wallet.generate(algorithm);
assert.isString(wallet.publicKey);
assert.isString(wallet.privateKey);
assert.isString(wallet.classicAddress);
assert.isString(wallet.seed);
assert.isTrue(wallet.publicKey.startsWith(ed25519KeyPrefix));
assert.isTrue(wallet.privateKey.startsWith(ed25519KeyPrefix));
assert.isTrue(wallet.classicAddress.startsWith(classicAddressPrefix));
});
});
describe("fromSeed", function () { describe("fromSeed", function () {
const seed = "ssL9dv2W5RK8L3tuzQxYY6EaZhSxW"; const seed = "ssL9dv2W5RK8L3tuzQxYY6EaZhSxW";
const publicKey = const publicKey =
@@ -65,7 +108,7 @@ describe("Wallet", function () {
}); });
describe("fromEntropy", function () { describe("fromEntropy", function () {
const entropy: number[] = new Array(16).fill(0); let entropy;
const publicKey = const publicKey =
"0390A196799EE412284A5D80BF78C3E84CBB80E1437A0AECD9ADF94D7FEAAFA284"; "0390A196799EE412284A5D80BF78C3E84CBB80E1437A0AECD9ADF94D7FEAAFA284";
const privateKey = const privateKey =
@@ -75,6 +118,11 @@ describe("Wallet", function () {
const privateKeyED25519 = const privateKeyED25519 =
"ED0B6CBAC838DFE7F47EA1BD0DF00EC282FDF45510C92161072CCFB84035390C4D"; "ED0B6CBAC838DFE7F47EA1BD0DF00EC282FDF45510C92161072CCFB84035390C4D";
beforeEach(function () {
const entropySize = 16;
entropy = new Array(entropySize).fill(0);
});
it("derives a wallet using entropy", function () { it("derives a wallet using entropy", function () {
const wallet = Wallet.fromEntropy(entropy); const wallet = Wallet.fromEntropy(entropy);

View File

@@ -1,6 +1,9 @@
import { assert } from "chai"; import { assert } from "chai";
import { getFaucetUrl, FaucetNetwork } from "../src/wallet/wallet-generation"; import {
getFaucetUrl,
FaucetNetwork,
} from "../src/wallet/generateFaucetWallet";
import setupClient from "./setupClient"; import setupClient from "./setupClient";

View File

@@ -17,7 +17,7 @@ function getDefaultConfiguration() {
}, },
plugins: [ plugins: [
new webpack.NormalModuleReplacementPlugin(/^ws$/, './wsWrapper'), new webpack.NormalModuleReplacementPlugin(/^ws$/, './wsWrapper'),
new webpack.NormalModuleReplacementPlugin(/^\.\/wallet$/, './wallet-web'), new webpack.NormalModuleReplacementPlugin(/^\.\/wallet\/index$/, './wallet-web'),
new webpack.NormalModuleReplacementPlugin( new webpack.NormalModuleReplacementPlugin(
/^.*setup-api$/, /^.*setup-api$/,
'./setup-api-web' './setup-api-web'