Skip to content

Conversation

interc0der
Copy link
Contributor

@interc0der interc0der commented Aug 8, 2025

High Level Overview of Change

As there are more wallets natively supporting the XRP Ledger, private keys are being passed around in different context and there should be a method in this codebase to derive a wallet from a private key. Since we have (2) algorithms and this package correctly handles both, this is a necessary addition to the package and offers immense value.

Context of Change

  • Adds a new static method to the Wallet class to derive an XRPL wallet using a private key.
  /**
   * Derives a wallet from a private key.
   *
   * @param privateKey - A string used to generate a keypair (publicKey/privateKey) to derive a wallet.
   * @returns A Wallet derived from a private key.
   */
  public static fromPrivateKey(privateKey: string): Wallet {
    return new Wallet(derivePublicKey(privateKey), privateKey)
  }

Type of Change

  • New feature (non-breaking change which adds functionality)
  • Tests (You added tests for code that already exists, or your new feature included in this PR)

Did you update HISTORY.md?

  • No, this change does not impact library users

Test Plan

  • Added a few tests in the Wallet folder to test derivations from mocked keypairs. These tests ensure that the correct public address and public key are generated from the private key.
  • Ensure that the library correctly handles a malformed private key and properly throws an error

Copy link
Contributor

coderabbitai bot commented Aug 8, 2025

Walkthrough

Added functionality to derive public keys from private keys for secp256k1 and ed25519, including new derive methods in signing-scheme modules, an exported derivePublicKey, an updated SigningScheme interface, a Wallet.fromPrivateKey static constructor, and new tests covering both key types.

Changes

Cohort / File(s) Change Summary
Public Key Derivation Core
packages/ripple-keypairs/src/index.ts
Added and exported derivePublicKey(privateKey: string): string.
Signing Schemes: ed25519
packages/ripple-keypairs/src/signing-schemes/ed25519/index.ts
Added deriveKeypairFromPrivateKey(privateKey: string) to derive public key (returns { privateKey, publicKey }).
Signing Schemes: secp256k1
packages/ripple-keypairs/src/signing-schemes/secp256k1/index.ts
Added deriveKeypairFromPrivateKey(privateKey: string) to derive compressed public key (returns { privateKey, publicKey }).
Type Declarations
packages/ripple-keypairs/src/types.ts
Added deriveKeypairFromPrivateKey(privateKey: HexString): KeyPair to the SigningScheme interface.
Wallet API
packages/xrpl/src/Wallet/index.ts
Added static Wallet.fromPrivateKey(privateKey: string): Wallet that validates input and constructs a Wallet.
Wallet Tests
packages/xrpl/test/wallet/index.test.ts
Added tests for Wallet.fromPrivateKey covering secp256k1 and ed25519 success cases and malformed-private-key errors.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~15–20 minutes

Poem

I nibble code in twilight light,
From secret roots I pull keys bright.
A public leaf from private seed,
Wallets sprout where logic leads.
Hopping home with tests in tow,
🐇🔑✨

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a0e430e and d6ba366.

📒 Files selected for processing (1)
  • packages/ripple-keypairs/src/index.ts (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/ripple-keypairs/src/index.ts
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (6)
packages/ripple-keypairs/src/index.ts (1)

98-105: LGTM! Consider adding input validation.

The implementation correctly determines the algorithm and delegates to the appropriate signing scheme. The logic is clean and follows existing patterns.

Consider adding basic input validation for the privateKey parameter to provide better error messages for invalid inputs:

function derivePublicKey(privateKey: string): string {
+  if (!privateKey || typeof privateKey !== 'string') {
+    throw new Error('privateKey must be a non-empty string')
+  }
  const algorithm = getAlgorithmFromPrivateKey(privateKey)
  
  if (algorithm === 'ecdsa-secp256k1') {
    return secp256k1.deriveKeypairFromPrivateKey(privateKey).publicKey
  }
  return ed25519.deriveKeypairFromPrivateKey(privateKey).publicKey
}
packages/xrpl/src/Wallet/index.ts (1)

177-185: LGTM! Consider adding input validation and security considerations.

The implementation is clean and follows the existing patterns for static factory methods. The JSDoc documentation is thorough and consistent.

Consider these improvements:

  1. Input validation: Add basic validation to provide better error messages:
public static fromPrivateKey(privateKey: string): Wallet {
+  if (!privateKey || typeof privateKey !== 'string') {
+    throw new ValidationError('privateKey must be a non-empty string')
+  }
  return new Wallet(derivePublicKey(privateKey), privateKey)
}
  1. Error handling: Consider wrapping the derivePublicKey call to provide more specific error messages for invalid private keys while being careful not to leak sensitive information.
packages/ripple-keypairs/src/signing-schemes/secp256k1/index.ts (1)

33-46: LGTM! Consider adding input validation.

The implementation correctly handles private key normalization and follows the same pattern as the existing sign method. The use of compressed public keys is consistent with the deriveKeypair method.

Consider adding input validation for robustness:

deriveKeypairFromPrivateKey(privateKey: string): {
  privateKey: string
  publicKey: string
} {
+  assert.ok(
+    (privateKey.length === 66 && privateKey.startsWith(SECP256K1_PREFIX)) ||
+      privateKey.length === 64,
+    'Invalid private key length or format'
+  )
  const normalizedPrivateKey =
    privateKey.length === 66 && privateKey.startsWith(SECP256K1_PREFIX)
      ? privateKey.slice(2)
      : privateKey

  const buffer = Buffer.from(normalizedPrivateKey, 'hex')

  const publicKey = bytesToHex(nobleSecp256k1.getPublicKey(buffer, true))
  return { privateKey, publicKey }
},

This matches the validation pattern used in the sign method (lines 52-55) and provides better error messages for invalid inputs.

packages/ripple-keypairs/src/signing-schemes/ed25519/index.ts (1)

22-34: LGTM! Consider adding input validation for consistency.

The implementation correctly handles private key normalization and is consistent with the existing deriveKeypair method. The ED prefix handling matches the existing patterns.

Consider adding input validation similar to the existing sign method for consistency:

deriveKeypairFromPrivateKey(privateKey: string): {
  privateKey: string
  publicKey: string
} {
+  assert.ok(
+    privateKey.startsWith(ED_PREFIX) ? privateKey.length === 66 : privateKey.length === 64,
+    'Invalid ed25519 private key length'
+  )
  const normalizedPrivateKey = privateKey.startsWith(ED_PREFIX)
    ? privateKey.slice(2)
    : privateKey

  const buffer = Buffer.from(normalizedPrivateKey, 'hex')

  const publicKey = ED_PREFIX + bytesToHex(nobleEd25519.getPublicKey(buffer))
  return { privateKey, publicKey }
},

This provides better error messages and is consistent with the validation patterns used in the sign method (line 39-41).

packages/xrpl/test/wallet/index.test.ts (2)

456-460: Consider improving error handling test specificity.

The error handling tests verify that errors are thrown for malformed private keys but don't validate the specific error type or message. While this approach is consistent with other tests in the codebase, consider making the tests more specific to ensure proper error handling.

Consider this improvement to make error handling more specific:

-      it('expect to throw error if private key incorrect format', function () {
+      it('throws error for malformed secp256k1 private key', function () {
         assert.throws(() =>
           Wallet.fromPrivateKey(mockWallet_secp256k1.privateKey.slice(0, 10)),
+          /Invalid private key format/
         )
       })

-        it('expect to throw error if private key incorrect format', function () {
+        it('throws error for malformed ed25519 private key', function () {
           assert.throws(() =>
             Wallet.fromPrivateKey(mockWallet_ed25519.privateKey.slice(0, 10)),
+            /Invalid private key format/
           )
         })

Note: Update the regex pattern to match the actual error message thrown by the implementation.

Also applies to: 476-480


450-450: Consider more specific test names.

The test names could be more specific and concise. Since the tests are already organized in separate describe blocks by key type, the test names don't need to repeat the key type information.

Consider these more concise and specific names:

-      it('derive keypair from private key', function () {
+      it('derives wallet from valid private key', function () {

-      it('expect to throw error if private key incorrect format', function () {
+      it('throws error for invalid private key format', function () {

-        it('derive keypair from private key', function () {
+        it('derives wallet from valid private key', function () {

-        it('expect to throw error if private key incorrect format', function () {
+        it('throws error for invalid private key format', function () {

Also applies to: 456-456, 470-470, 476-476

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between de28f40 and ec86c3a.

📒 Files selected for processing (6)
  • packages/ripple-keypairs/src/index.ts (2 hunks)
  • packages/ripple-keypairs/src/signing-schemes/ed25519/index.ts (1 hunks)
  • packages/ripple-keypairs/src/signing-schemes/secp256k1/index.ts (1 hunks)
  • packages/ripple-keypairs/src/types.ts (1 hunks)
  • packages/xrpl/src/Wallet/index.ts (2 hunks)
  • packages/xrpl/test/wallet/index.test.ts (1 hunks)
🧰 Additional context used
🧠 Learnings (8)
📚 Learning: 2024-12-06T18:44:55.095Z
Learnt from: shawnxie999
PR: XRPLF/xrpl.js#2661
File: packages/xrpl/test/models/MPTokenAuthorize.test.ts:60-71
Timestamp: 2024-12-06T18:44:55.095Z
Learning: In the XRPL.js library's TypeScript test file `packages/xrpl/test/models/MPTokenAuthorize.test.ts`, negative test cases for invalid `Account` address format, invalid `Holder` address format, invalid `MPTokenIssuanceID` format, and invalid flag combinations are not necessary.

Applied to files:

  • packages/xrpl/test/wallet/index.test.ts
📚 Learning: 2024-12-06T19:25:15.376Z
Learnt from: shawnxie999
PR: XRPLF/xrpl.js#2661
File: packages/xrpl/test/integration/transactions/mptokenAuthorize.test.ts:29-118
Timestamp: 2024-12-06T19:25:15.376Z
Learning: In the XRPLF/xrpl.js TypeScript client library, when writing tests (e.g., in `packages/xrpl/test/integration/transactions/`), we generally do not need to test rippled server behaviors, because those behaviors are covered by rippled's own integration and unit tests.

Applied to files:

  • packages/xrpl/test/wallet/index.test.ts
📚 Learning: 2025-01-08T02:12:28.489Z
Learnt from: ckeshava
PR: XRPLF/xrpl.js#2874
File: packages/xrpl/test/integration/transactions/permissionedDomain.test.ts:25-80
Timestamp: 2025-01-08T02:12:28.489Z
Learning: The rippled C++ implementation (PR #5161) includes comprehensive test coverage for PermissionedDomain (XLS-80d) error cases. The JS SDK tests focus on the happy path since the error cases are already validated at the rippled level, following the principle of not duplicating complex validation testing across SDK implementations.

Applied to files:

  • packages/xrpl/test/wallet/index.test.ts
📚 Learning: 2024-12-06T19:27:11.147Z
Learnt from: shawnxie999
PR: XRPLF/xrpl.js#2661
File: packages/xrpl/test/integration/transactions/clawback.test.ts:165-178
Timestamp: 2024-12-06T19:27:11.147Z
Learning: In the integration tests for `clawback.test.ts`, it's acceptable to use `ts-expect-error` to bypass type checking when verifying ledger entries, and no additional type safety improvements are needed.

Applied to files:

  • packages/xrpl/test/wallet/index.test.ts
📚 Learning: 2025-01-31T17:46:25.375Z
Learnt from: ckeshava
PR: XRPLF/xrpl.js#2873
File: packages/xrpl/test/integration/transactions/trustSet.test.ts:0-0
Timestamp: 2025-01-31T17:46:25.375Z
Learning: For the XRPL implementation, extensive test cases for deep freeze behavior (high/low side interactions, clearing flags, etc.) are maintained in the C++ implementation and don't need to be duplicated in the JavaScript implementation.

Applied to files:

  • packages/xrpl/test/wallet/index.test.ts
📚 Learning: 2025-04-16T15:28:21.204Z
Learnt from: mvadari
PR: XRPLF/xrpl.js#2801
File: packages/xrpl/test/wallet/batchSigner.test.ts:0-0
Timestamp: 2025-04-16T15:28:21.204Z
Learning: In the XRPL.js library, hardcoded seeds in test files are acceptable as they don't represent protected data or real funds - they're only used for consistent test behavior.

Applied to files:

  • packages/xrpl/test/wallet/index.test.ts
📚 Learning: 2025-04-16T15:22:45.633Z
Learnt from: mvadari
PR: XRPLF/xrpl.js#2801
File: packages/xrpl/test/models/Batch.test.ts:0-0
Timestamp: 2025-04-16T15:22:45.633Z
Learning: Using `as any` type assertions is acceptable in test files for the XRPL.js project, as strict typing is not required for test code.

Applied to files:

  • packages/xrpl/test/wallet/index.test.ts
📚 Learning: 2025-02-12T23:30:40.622Z
Learnt from: mvadari
PR: XRPLF/xrpl.js#2895
File: packages/xrpl/test/models/DIDDelete.test.ts:28-31
Timestamp: 2025-02-12T23:30:40.622Z
Learning: In JavaScript/TypeScript transaction validation tests, object key validation can be performed using:
1. Object.keys() comparison with expected set
2. TypeScript interfaces with strict object literal checks
3. Object sanitization by filtering to allowed keys only

Applied to files:

  • packages/xrpl/test/wallet/index.test.ts
🧬 Code Graph Analysis (4)
packages/ripple-keypairs/src/signing-schemes/ed25519/index.ts (1)
packages/isomorphic/src/utils/index.ts (1)
  • bytesToHex (62-65)
packages/xrpl/src/Wallet/index.ts (2)
packages/xrpl/src/index.ts (1)
  • Wallet (12-12)
packages/ripple-keypairs/src/index.ts (1)
  • derivePublicKey (116-116)
packages/ripple-keypairs/src/index.ts (1)
packages/ripple-keypairs/src/utils/getAlgorithmFromKey.ts (1)
  • getAlgorithmFromPrivateKey (119-121)
packages/ripple-keypairs/src/signing-schemes/secp256k1/index.ts (1)
packages/isomorphic/src/utils/index.ts (1)
  • bytesToHex (62-65)
🔇 Additional comments (4)
packages/ripple-keypairs/src/types.ts (1)

21-22: LGTM! Interface addition is well-designed.

The new method signature follows the existing interface patterns and uses appropriate types. The deriveKeypairFromPrivateKey method signature is consistent with the existing deriveKeypair method in terms of return type.

packages/ripple-keypairs/src/index.ts (1)

116-116: LGTM! Export addition is correct.

The new function is properly exported and follows the existing export patterns.

packages/xrpl/src/Wallet/index.ts (1)

20-20: LGTM! Import addition is correct.

The derivePublicKey import is properly added and follows the existing import patterns.

packages/xrpl/test/wallet/index.test.ts (1)

442-448: LGTM! Well-structured test data.

The mock wallet data follows proper XRPL key format conventions:

  • secp256k1 private keys correctly start with '00' and public keys with '03'
  • ed25519 keys correctly start with 'ED'
  • All required fields (address, publicKey, privateKey) are present

The use of hardcoded test data is appropriate and consistent with the project's testing practices.

Also applies to: 463-469

Copy link
Collaborator

@ckeshava ckeshava left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this work. Please address the linter failures.

How do you envision the usage of your method? Will users generate their privateKeys on a trusted execution environment and use it to sign/verify the transactions?

Since there are existing methods to generate key-pairs from Seed, Entropy and Mnemonic, I'd like to understand the need for this method.

const buffer = Buffer.from(normalizedPrivateKey, 'hex')

const publicKey = ED_PREFIX + bytesToHex(nobleEd25519.getPublicKey(buffer))
return { privateKey, publicKey }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you consider adding the 'ED' prefix to the returned privateKey ? In this file, the deriveKeypair method returns both the publicKey and the privateKey along with the ED prefix.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having said that, I'm aware that the deriveKeypair method inside the secp256k1 (packages/ripple-keypairs/src/signing-schemes/secp256k1/index.ts) module does not explicitly add the 00 prefix.

Do you know what is the expected convention in cryptographic methods?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you consider adding the 'ED' prefix to the returned privateKey ? In this file, the deriveKeypair method returns both the publicKey and the privateKey along with the ED prefix.

https://github.com/interc0der/xrpl.js/blob/bed93562ebde0e13a2c919e74a5f3ab12d99ee0b/packages/ripple-keypairs/src/signing-schemes/ed25519/index.ts#L11-L20
The deriveKeypair method does not return the prefix. What are you referencing?

Copy link
Contributor Author

@interc0der interc0der Aug 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having said that, I'm aware that the deriveKeypair method inside the secp256k1 (packages/ripple-keypairs/src/signing-schemes/secp256k1/index.ts) module does not explicitly add the 00 prefix.

Do you know what is the expected convention in cryptographic methods?

This is why the deriveKeypairFromPrivateKey matches the conventions in the sign method. The function allows for a prefixed, or non-prefixed key - and derives the public address with the normalized private key.

Not sure what you mean by the expected convention in cryptographic methods.

  • Secp256k1 expects/generates a 33 byte public key and a 32 byte private key.
  • Ed25519 expects/generates a 32 byte public key and and 32 byte private key.

Key are normalized and padded according to algorithm. See this nice reference table: https://github.com/interc0der/xrpl.js/blob/main/packages/ripple-keypairs/src/utils/getAlgorithmFromKey.ts

Comment on lines 443 to 447
address: 'rhvh5SrgBL5V8oeV9EpDuVszeJSSCEkbPc',
publicKey:
'030E58CDD076E798C84755590AAF6237CA8FAE821070A59F648B517A30DC6F589D',
privateKey:
'00141BA006D3363D2FB2785E8DF4E44D3A49908780CB4FB51F6D217C08C021429F',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello, how did you generate this mocked privateKey and publicKey data? Which library/tool did you use?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe I used this library to generate them. Why? - The unit check confirms that it is a valid keypair...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. As long as the newly introduced method was not used to generate the test case, it is acceptable.

if (algorithm === 'ecdsa-secp256k1') {
return secp256k1.deriveKeypairFromPrivateKey(privateKey).publicKey
}
return ed25519.deriveKeypairFromPrivateKey(privateKey).publicKey
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add an explicit if-condition that does:

if (algorithm === 'ed25519') {
    return ed25519.deriveKeypairFromPrivateKey(privateKey).publicKey
  } else
throw new Error('Unknown signing scheme algorithm')

In the future, if support for new SigningSchemes are provided in the xrpl.js library, then this code is protected from incorrect default behavior.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't do this, because it was causing a linting issue.

I have added it and suppressed the warning.
https://github.com/interc0der/xrpl.js/blob/d6ba366155e56cbd9e736efcd9073717c01b7b69/packages/ripple-keypairs/src/index.ts#L98-L108

@interc0der
Copy link
Contributor Author

interc0der commented Aug 14, 2025

Thanks for this work. Please address the linter failures.

How do you envision the usage of your method? Will users generate their privateKeys on a trusted execution environment and use it to sign/verify the transactions?

Since there are existing methods to generate key-pairs from Seed, Entropy and Mnemonic, I'd like to understand the need for this method.

Exactly - there are methods to instantiate a wallet from Seed, Entropy and Mnemonic. There should also be a method for PrivateKey, not only for completeness, but for wallets that support both generating and deriving public addresses using a pk.

Usage - Hmm, same usage as the methods above (Seed, Entropy and Mnemonic). Users can generate/derive a wallet using a private key. This method could be used for verifying that a private key is valid, and it can also be used to instantiate a Wallet for future signing.

The linting issues seem to be coming from the binary-codec package which I did not modify. I am not going to correct an issue related to another change. I will continue to sync my branch with corrective commits.

@interc0der
Copy link
Contributor Author

interc0der commented Aug 14, 2025

I mistakenly reverted changes breaking the commit tree. Moved the same commits over to a fresh pull request.
#3063

@ckeshava If there are any comments here that you feel have not been resolved, can you move them over?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants