diff --git a/src/client/index.ts b/src/client/index.ts index dbc38c85..ff6216f6 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -115,7 +115,7 @@ import prepareTrustline from "../transaction/trustline"; import { TransactionJSON, Instructions, Prepare } from "../transaction/types"; import * as transactionUtils from "../transaction/utils"; import { deriveAddress, deriveXAddress } from "../utils/derive"; -import generateFaucetWallet from "../wallet/wallet-generation"; +import generateFaucetWallet from "../wallet/generateFaucetWallet"; import { Connection, ConnectionUserOptions } from "./connection"; diff --git a/src/index.ts b/src/index.ts index c713324f..fc8eb966 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,4 +11,4 @@ export * from "./utils"; // Broadcast client is experimental export { BroadcastClient } from "./client/broadcastClient"; -export * from "./Wallet"; +export { default as Wallet } from "./wallet"; diff --git a/src/transaction/sign.ts b/src/transaction/sign.ts index 33a42afa..4479db8f 100644 --- a/src/transaction/sign.ts +++ b/src/transaction/sign.ts @@ -3,11 +3,10 @@ import _ from "lodash"; import binaryCodec from "ripple-binary-codec"; import keypairs from "ripple-keypairs"; -import { Client } from ".."; +import { Client, Wallet } from ".."; import { SignedTransaction } from "../common/types/objects"; import { xrpToDrops } from "../utils"; import { computeBinaryTransactionHash } from "../utils/hashes"; -import Wallet from "../Wallet"; import { SignOptions, KeyPair, TransactionJSON } from "./types"; import * as utils from "./utils"; diff --git a/src/wallet/wallet-generation.ts b/src/wallet/generateFaucetWallet.ts similarity index 79% rename from src/wallet/wallet-generation.ts rename to src/wallet/generateFaucetWallet.ts index bf1a5fef..d129e114 100644 --- a/src/wallet/wallet-generation.ts +++ b/src/wallet/generateFaucetWallet.ts @@ -1,6 +1,6 @@ import https = require("https"); -import { Client } from ".."; +import { Client, Wallet } from ".."; import { errors } from "../common"; import { RippledError } from "../common/errors"; 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). * - * @param this - * @param address - An existing XRPL address to fund, if undefined, a new wallet will be created. + * @param client - Client. + * @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. + * @throws When either Client isn't connected or unable to fund wallet address. */ async function generateFaucetWallet( - this: Client, - address?: string -): Promise { - if (!this.isConnected()) { + client: Client, + wallet?: Wallet +): Promise { + if (!client.isConnected()) { throw new RippledError("Client not connected, cannot call faucet"); } - // Initialize some variables - let body: Uint8Array | undefined; - let startingBalance = 0; - const faucetUrl = getFaucetUrl(this); + // Generate a new Wallet if no existing Wallet is provided or its address is invalid to fund + const fundWallet = + wallet && isValidAddress(wallet.classicAddress) + ? wallet + : Wallet.generate(); - // If the user provides an existing wallet to fund - if (address && isValidAddress(address)) { - // Create the POST request body - body = new TextEncoder().encode( - JSON.stringify({ - destination: address, - }) - ); - // Retrieve the existing account balance - const addressToFundBalance = await getAddressXrpBalance(this, address); + // Create the POST request body + const body: Uint8Array | undefined = new TextEncoder().encode( + JSON.stringify({ + destination: fundWallet.classicAddress, + }) + ); + // Retrieve the existing account balance + const addressToFundBalance = await getAddressXrpBalance( + client, + fundWallet.classicAddress + ); - // Check the address balance is not undefined and is a number - if (addressToFundBalance && !isNaN(Number(addressToFundBalance))) { - startingBalance = Number(addressToFundBalance); - } else { - startingBalance = 0; - } - } + // Check the address balance is not undefined and is a number + const startingBalance = + addressToFundBalance && !isNaN(Number(addressToFundBalance)) + ? Number(addressToFundBalance) + : 0; + + const faucetUrl = getFaucetUrl(client); // Options to pass to https.request const options = { @@ -82,20 +85,20 @@ async function generateFaucetWallet( // "application/json; charset=utf-8" if (response.headers["content-type"]?.startsWith("application/json")) { - const wallet: FaucetWallet = JSON.parse(body); - const classicAddress = wallet.account.classicAddress; + const faucetWallet: FaucetWallet = JSON.parse(body); + const classicAddress = faucetWallet.account.classicAddress; if (classicAddress) { try { // Check at regular interval if the address is enabled on the XRPL and funded const isFunded = await hasAddressBalanceIncreased( - this, + client, classicAddress, startingBalance ); if (isFunded) { - resolve(wallet); + resolve(fundWallet); } else { reject( new errors.XRPLFaucetError( diff --git a/src/Wallet.ts b/src/wallet/index.ts similarity index 82% rename from src/Wallet.ts rename to src/wallet/index.ts index 4043d68d..766c17c4 100644 --- a/src/Wallet.ts +++ b/src/wallet/index.ts @@ -9,11 +9,11 @@ import { verify, } from "ripple-keypairs"; -import ECDSA from "./common/ecdsa"; -import { ValidationError } from "./common/errors"; -import { SignedTransaction } from "./common/types/objects"; -import { signOffline } from "./transaction/sign"; -import { SignOptions } from "./transaction/types"; +import ECDSA from "../common/ecdsa"; +import { ValidationError } from "../common/errors"; +import { SignedTransaction } from "../common/types/objects"; +import { signOffline } from "../transaction/sign"; +import { SignOptions } from "../transaction/types"; /** * A utility for deriving a wallet composed of a keypair (publicKey/privateKey). @@ -23,12 +23,27 @@ import { SignOptions } from "./transaction/types"; class Wallet { readonly publicKey: string; readonly privateKey: string; + readonly classicAddress: string; + readonly seed?: string; private static readonly defaultAlgorithm: ECDSA = ECDSA.ed25519; 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.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 ): Wallet { 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. */ getXAddress(tag: number, test = false): string { - return classicAddressToXAddress(deriveAddress(this.publicKey), tag, test); + return classicAddressToXAddress(this.classicAddress, tag, test); } } diff --git a/test/wallet/index.ts b/test/wallet/index.ts index bbd0f4f3..8011677e 100644 --- a/test/wallet/index.ts +++ b/test/wallet/index.ts @@ -1,7 +1,7 @@ import { assert } from "chai"; import ECDSA from "../../src/common/ecdsa"; -import Wallet from "../../src/Wallet"; +import Wallet from "../../src/wallet"; /** * Wallet testing. @@ -9,6 +9,49 @@ import Wallet from "../../src/Wallet"; * Provides tests for Wallet class. */ 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 () { const seed = "ssL9dv2W5RK8L3tuzQxYY6EaZhSxW"; const publicKey = @@ -65,7 +108,7 @@ describe("Wallet", function () { }); describe("fromEntropy", function () { - const entropy: number[] = new Array(16).fill(0); + let entropy; const publicKey = "0390A196799EE412284A5D80BF78C3E84CBB80E1437A0AECD9ADF94D7FEAAFA284"; const privateKey = @@ -75,6 +118,11 @@ describe("Wallet", function () { const privateKeyED25519 = "ED0B6CBAC838DFE7F47EA1BD0DF00EC282FDF45510C92161072CCFB84035390C4D"; + beforeEach(function () { + const entropySize = 16; + entropy = new Array(entropySize).fill(0); + }); + it("derives a wallet using entropy", function () { const wallet = Wallet.fromEntropy(entropy); diff --git a/test/walletGeneration.ts b/test/walletGeneration.ts index cff70976..3d0b01bf 100644 --- a/test/walletGeneration.ts +++ b/test/walletGeneration.ts @@ -1,6 +1,9 @@ import { assert } from "chai"; -import { getFaucetUrl, FaucetNetwork } from "../src/wallet/wallet-generation"; +import { + getFaucetUrl, + FaucetNetwork, +} from "../src/wallet/generateFaucetWallet"; import setupClient from "./setupClient"; diff --git a/webpack.config.js b/webpack.config.js index 4f610454..29b20ddd 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -17,7 +17,7 @@ function getDefaultConfiguration() { }, plugins: [ new webpack.NormalModuleReplacementPlugin(/^ws$/, './wsWrapper'), - new webpack.NormalModuleReplacementPlugin(/^\.\/wallet$/, './wallet-web'), + new webpack.NormalModuleReplacementPlugin(/^\.\/wallet\/index$/, './wallet-web'), new webpack.NormalModuleReplacementPlugin( /^.*setup-api$/, './setup-api-web'