Skip to content

[wallet/common] feat: create core project #1473

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: dev
Choose a base branch
from
Open

Conversation

OlegMakarenko
Copy link
Contributor

Problem:

We didn't yet have a shared, reusable, protocol-agnostic core library for managing wallets, accounts, signing, storage, and network connections across different wallet apps (mobile, desktop, browser extension).

Each app would have to implement its own controller, storage abstraction, keystore logic, and protocol integration, leading to duplicated effort, inconsistent APIs, and maintenance overhead.

Solution:

This PR creates the new Wallet Common Core project, providing:

  • Project structure and setup.
  • A reusable, modular orchestration layer (WalletController)
  • Pluggable keystore system (MnemonicKeystore, ExternalAccountKeystore)
    - Abstracted storage interface
  • Protocol API and SDK wrappers
  • Entry point exports
  • Added README.md
  • Added tests

This project/package can be consumed by any wallet app <NEM|Symbol><Desktop|Mobile|Extension>Wallet to handle core wallet functionality.

Copy link

codecov bot commented Jul 7, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 98.22%. Comparing base (be149f3) to head (aa84495).
Report is 6 commits behind head on dev.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##              dev    #1473      +/-   ##
==========================================
- Coverage   98.26%   98.22%   -0.05%     
==========================================
  Files         163      157       -6     
  Lines        6691     6477     -214     
  Branches      143      143              
==========================================
- Hits         6575     6362     -213     
+ Misses        116      115       -1     
Flag Coverage Δ
explorer-rest ?

Flags with carried forward coverage won't be shown. Click here to find out more.

see 6 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@OlegMakarenko OlegMakarenko requested a review from Wayonb July 8, 2025 08:00
@OlegMakarenko
Copy link
Contributor Author

@cryptoBeliever thanks, new changes pushed!

@@ -0,0 +1,19 @@
{
"name": "@wallet/common-core",
"main": "./src/index.js",
Copy link
Contributor

Choose a reason for hiding this comment

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

Does the package need a version? How do the other components comsume this module?

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'm thinking of using it as a local package, e.g.:

{
  "dependencies": {
    "@wallet/common-core": "file:../../common/core"
  }
}

Does the package need a version?

I will add the version.

"devDependencies": {
"c8": "~10.1.2",
"eslint": "~8.57.0",
"eslint-config-airbnb": "~19.0.4",
Copy link
Contributor

Choose a reason for hiding this comment

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

eslint-config-airbnb has not been updated to support ESLint 9. We may need to replace it if it is not updated. Don't need to change now, but just making a note.

constructor(code, message) {
super(message);
this.name = this.constructor.name;
this.code = code;
Copy link
Contributor

Choose a reason for hiding this comment

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

Should the code be included in the toString()?

/** @typedef {import('../types/Transaction').SignedTransactionDTO} SignedTransactionDTO */

/**
* @callback FetchAccountInfoFn
Copy link
Contributor

Choose a reason for hiding this comment

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

should this be FetchAccountInfoCallback?

Copy link
Contributor

Choose a reason for hiding this comment

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

This function does not truly represent a callback function. It wasn't clear until I saw the code. For JavaScript, any function parameter is considered a callback. 😅
This is the Strategy pattern, where you allow different implementations? Is this support for NEM and Symbol only?

[wallet-common-core-lint]: https://jenkins.symbolsyndicate.us/buildStatus/icon?job=Symbol%2Fgenerated%2Fproduct%2Fwallet-common-core%2Fdev%2F&config=wallet-common-core-lint
[wallet-common-core-test]: https://jenkins.symbolsyndicate.us/buildStatus/icon?job=Symbol%2Fgenerated%2Fproduct%2Fwallet-common-core%2Fdev%2F&config=wallet-common-core-test
[wallet-common-core-cov]: https://codecov.io/gh/symbol/product/branch/dev/graph/badge.svg?token=SSYYBMK0M7&flag=wallet-common-core
[wallet-common-core-cov-link]: https://codecov.io/gh/symbol/product/tree/dev/tools/vanity
Copy link
Contributor

Choose a reason for hiding this comment

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

tools/vanity => wallet/common/core?

*/
clear = async () => {
await Promise.all(Object.values(PersistentStorageRepository.STORAGE_KEYS).map(key => this.storage.removeItem(key)));
};
Copy link
Contributor

Choose a reason for hiding this comment

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

Would this.storage.clear()work better instead of removing each item, since I am assuming localStorage is used?

Copy link
Contributor

Choose a reason for hiding this comment

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

If not, you might want to check if the storage is empty

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We only want to remove the keys that are managed by this repository. Since the same storage may be shared across multiple repositories, it's important to ensure that only data related to the current repository is cleared.

localStorage is used by the browser extension wallet and mobile wallets use an API for device storage and encrypted storage.

If not, you might want to check if the storage is empty

Removing a non-existent value from localStorage doesn’t throw an error, so checking whether a value exists beforehand is unnecessary in this case. If other storage implementations behave differently and throw an error, such checks should be handled inside the StorageInterface, IMO.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok. That makes sense.

across multiple repositories

What do you mean by multiple repositories? Is it when multiple users use the wallet on the same device?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We have two separate repositories: PersistentStorageRepository for storing WalletController-related data (like public account info), and SecureStorageRepository for storing sensitive encrypted data (such as private keys). Both repositories rely on a shared StorageInterface, which provides methods like set, get, and remove.

In the mobile wallet, these repositories use different StorageInterface implementations (AsyncStorage and EncryptedStorage). However, in the browser extension wallet, both repositories use the same underlying storage - LocalStorage. As a result, calling localStorage.clear() from PersistentStorageRepository would clear data managed by SecureStorageRepository as well. This is why I avoid using .clear() and instead remove only the relevant keys tied to each repository.


packageId = 'wallet-common-core'

codeCoverageTool = 'c8'
Copy link
Contributor

Choose a reason for hiding this comment

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

Did you want to set the minimumCodeCoverage?


/**
* @callback SignTransactionFn
* @param {NetworkProperties} networkProperties - The network properties.
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the networkProperties needed to get the generationHashSeed for tx signing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In Symbol implementation we use networkIdentifier to create SymbolFasade('testnet' | 'mainnet'), networkCurrency.divisibility for fee and epochAdjustment for deadline.

Looks like generationHashSeed is not used.


/**
* @callback CosignTransactionFn
* @param {NetworkProperties} networkProperties - The network properties.
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is the networkProperties needed here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are right, networkProperties doesn't needed here. We don't need to create instance of SymbolFacade since cosignTransactionHash is a static method.

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.

3 participants