Skip to content

Conversation

@abk404
Copy link

@abk404 abk404 commented Jan 6, 2026

Ticket: CSI-1548

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Summary

Optimizes createBulkWalletShare to fetch the wallet keychain only once instead of once per user, drastically reducing API calls and preventing rate limiting (HTTP 429) errors during bulk wallet sharing.

Problem

When sharing a wallet with multiple users via createBulkWalletShare, the SDK was fetching the wallet keychain (v2.key.get) for each user in the share list.

For example, sharing 26 wallets with 15 users each resulted in 390 API calls (26 × 15), causing rate limiting errors and failures with the message: "Failed to process wallet shares after retrying with single requests."
Check the support ticket : CS-6701

Solution

Refactored the code to:

  1. Extract common logic into two new helper methods:

    • getDecryptedKeychainForSharing() - Fetches and decrypts the keychain once
    • encryptPrvForUser() - Encrypts the private key for a specific user (no API calls)
  2. Optimize createBulkWalletShare() to fetch the keychain once before the loop, then reuse it for all users

@abk404 abk404 requested review from a team as code owners January 6, 2026 12:07
@abk404 abk404 changed the title feat: fetch keychain per wallet once fix(wallet-key-share): Optimize createBulkWalletShare to Fetch Keychain Once and Prevent Rate Limiting Jan 6, 2026
bulkCreateShareOptions.push({
user: shareOption.userId,
permissions: shareOption.permissions,
keychain: sharedKeychain as BulkWalletShareKeychain,
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing validation: The original code had explicit assertions for keychain fields. Consider keeping runtime validation instead of relying solely on type cast.

Copy link
Author

Choose a reason for hiding this comment

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

makes sense, added the explicit assertions and removed the typecast completely.

*/
async getDecryptedKeychainForSharing(
walletPassphrase: string | undefined
): Promise<{ prv: string; pub: string } | undefined> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Type improvement: Consider extracting { prv: string; pub: string } into a named interface for reusability.

Copy link
Author

Choose a reason for hiding this comment

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

Done

path: user.path,
},
})),
});
Copy link
Contributor

Choose a reason for hiding this comment

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

Expand test coverage: Also verify encryptPrvForUser is called once per user to ensure encryption happens for each recipient.

Copy link
Author

Choose a reason for hiding this comment

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

Added

@Marzooqa Marzooqa requested review from zahin-mohammad and removed request for pranavjain97 January 9, 2026 13:09
Comment on lines +648 to +651
export interface DecryptedKeychainData {
prv: string;
pub: string;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Are we sure we don't already have this type defined somewhere?

Copy link
Author

@abk404 abk404 Jan 15, 2026

Choose a reason for hiding this comment

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

No , we don't have this type defined somewhere else.
This was not required before because the prepareSharedKeychain was doing the operations in the same function.

Since, I broke down that function into 3 different function , we need this new interface for having the return typed for the new function.

There is a Keychain type defined which has these two fields(along with ton of other fields), but there also these fields are marked as optional .
i can do Required<Pick<Keychain, 'prv' | 'pub'>> but the current approcah of defining a new interface seems more readable and explicit.

This also follows the current pattern where a new type BulkWalletShareKeychain was created for the specific usecase instead of usign the Keychain type.

Comment on lines +1811 to +1813
// Only one of pub/commonPub/commonKeychain should be present in the keychain
let pub = keychain.pub ?? keychain.commonPub;
if (keychain.commonKeychain) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Only one of pub/commonPub/commonKeychain should be present in the keychain
let pub = keychain.pub ?? keychain.commonPub;
if (keychain.commonKeychain) {
// Only one of pub/commonKeychain should be present in the keychain
let pub = keychain.pub;

commonPub is deprecated field, we shouldn't have any active wallets that are using this field.

Copy link
Author

Choose a reason for hiding this comment

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

The commonPub fallback was pre-existing code from the original bulk sharing implementation. This PR is a refactoring that moves this logic from prepareSharedKeychain() to getDecryptedKeychainForSharing() without changing any behaviour.

If it's a deprecated field , I can remove this, but just thinking if it will break some other flows in downstream services which calls the /key endpoint and still expect the commonPub field.

Comment on lines +1814 to +1817
pub =
this.baseCoin.getMPCAlgorithm() === 'eddsa'
? EddsaUtils.getPublicKeyFromCommonKeychain(keychain.commonKeychain)
: EcdsaUtils.getPublicKeyFromCommonKeychain(keychain.commonKeychain);
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 this necessary??

Copy link
Contributor

Choose a reason for hiding this comment

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

Will ping on slack.

Copy link
Author

@abk404 abk404 Jan 15, 2026

Choose a reason for hiding this comment

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

This was also there before in the codebase I refactored the code to pull out this pub key creation logic into a separate function.
The /walletshare/{id} responses still include the pub , so I am a little skeptical if removing the field might break some other services consuming this endpoint. Also pub is used when accepting shares.

If we want to remove pub or simplify this extraction logic, I'd suggest doing it in a separate cleanup PR to avoid scope creep and ensure proper testing.

Comment on lines +1842 to +1850
const keychain: BulkWalletShareKeychain = {
pub,
encryptedPrv: newEncryptedPrv,
fromPubKey: eckey.publicKey.toString('hex'),
toPubKey: userPubkey,
path: path,
};

assert(keychain.pub, 'pub must be defined for sharing');
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
const keychain: BulkWalletShareKeychain = {
pub,
encryptedPrv: newEncryptedPrv,
fromPubKey: eckey.publicKey.toString('hex'),
toPubKey: userPubkey,
path: path,
};
assert(keychain.pub, 'pub must be defined for sharing');
const keychain: BulkWalletShareKeychain = {
encryptedPrv: newEncryptedPrv,
fromPubKey: eckey.publicKey.toString('hex'),
toPubKey: userPubkey,
path: path,
};

This shouldn't be necessary.

Copy link
Author

Choose a reason for hiding this comment

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

Same as above comment.

@abk404 abk404 requested a review from zahin-mohammad January 15, 2026 13:21
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.

4 participants