Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions __tests__/wallet/wallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2976,6 +2976,101 @@ describe('HathorWalletServiceWallet start method error conditions', () => {
// Verify walletId was set
expect(wallet.walletId).toBe(mockWalletId);
});

it('should handle async errors from first validateAndRenewAuthToken before await', async () => {
jest
.spyOn(wallet.storage, 'getAccessData')
.mockRejectedValueOnce(new UninitializedWalletError('Wallet not initialized'));

// Mock validateAndRenewAuthToken to reject immediately (async error before await)
const authError = new Error('Auth token validation failed');
jest.spyOn(wallet, 'validateAndRenewAuthToken').mockRejectedValue(authError);

jest.spyOn(walletApi, 'createWallet').mockResolvedValue({
success: true,
status: {
walletId: 'test-wallet-id',
xpubkey: 'test-xpub',
status: 'ready',
maxGap: 20,
createdAt: Date.now(),
readyAt: Date.now(),
},
});

jest.spyOn(walletApi, 'getNewAddresses').mockResolvedValue({
success: true,
addresses: [
{
address: 'test-address',
index: 0,
addressPath: "m/44'/280'/0'/0/0",
},
],
});

// @ts-expect-error - Accessing private method for testing
jest.spyOn(wallet, 'onWalletReady').mockResolvedValue(undefined);

// The wallet should handle the auth error gracefully
await wallet.start({ pinCode: '123', password: '123' });

// Auth token should be cleared when error occurs
expect(wallet.authToken).toBeNull();

// validateAndRenewAuthToken should be called twice:
// 1. First attempt that fails
// 2. Second retry after wallet creation
expect(wallet.validateAndRenewAuthToken).toHaveBeenCalledTimes(2);
});

it('should handle async errors from second validateAndRenewAuthToken in handleCreate', async () => {
jest
.spyOn(wallet.storage, 'getAccessData')
.mockRejectedValueOnce(new UninitializedWalletError('Wallet not initialized'));

// Mock first call to succeed, but second call (retry) to fail
const authError = new Error('Auth token renewal failed on retry');
jest
.spyOn(wallet, 'validateAndRenewAuthToken')
.mockRejectedValueOnce(new Error('First auth attempt failed'))
.mockRejectedValueOnce(authError);

jest.spyOn(walletApi, 'createWallet').mockResolvedValue({
success: true,
status: {
walletId: 'test-wallet-id',
xpubkey: 'test-xpub',
status: 'ready',
maxGap: 20,
createdAt: Date.now(),
readyAt: Date.now(),
},
});

jest.spyOn(walletApi, 'getNewAddresses').mockResolvedValue({
success: true,
addresses: [
{
address: 'test-address',
index: 0,
addressPath: "m/44'/280'/0'/0/0",
},
],
});

// @ts-expect-error - Accessing private method for testing
jest.spyOn(wallet, 'onWalletReady').mockResolvedValue(undefined);

// The wallet should handle both auth errors gracefully
await wallet.start({ pinCode: '123', password: '123' });

// Auth token should be cleared when errors occur
expect(wallet.authToken).toBeNull();

// validateAndRenewAuthToken should be called twice (both failed)
expect(wallet.validateAndRenewAuthToken).toHaveBeenCalledTimes(2);
});
});
});

Expand Down
24 changes: 21 additions & 3 deletions src/wallet/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@
}

let renewPromise: Promise<void> | null = null;
let renewPromiseError = null;
if (accessData.acctPathKey) {
// We can preemtively request/renew the auth token so the wallet can wait for this process
// to finish while we derive and request the wallet creation.
Expand All @@ -427,7 +428,9 @@
const privKeyAccountPath = bitcore.HDPrivateKey(acctKey);
const walletId = HathorWalletServiceWallet.getWalletIdFromXPub(privKeyAccountPath.xpubkey);
this.walletId = walletId;
renewPromise = this.validateAndRenewAuthToken(pinCode);
renewPromise = this.validateAndRenewAuthToken(pinCode).catch(err => {
renewPromiseError = err;
});
}

const {
Expand All @@ -445,12 +448,15 @@
// derive the account path xpubkey on the method above.
const walletId = HathorWalletServiceWallet.getWalletIdFromXPub(xpub);
this.walletId = walletId;
renewPromise = this.validateAndRenewAuthToken(pinCode);
renewPromise = this.validateAndRenewAuthToken(pinCode).catch(err => {
renewPromiseError = err;
});
}

this.xpub = xpub;
this.authPrivKey = authDerivedPrivKey;

let renewPromise2Error = null;
const handleCreate = async (data: WalletStatus, tokenRenewPromise: Promise<void> | null) => {
this.walletId = data.walletId;

Expand All @@ -466,6 +472,11 @@

if (tokenRenewPromise !== null) {
try {
if (renewPromise2Error) {
// If an error happened async before this await, we must
// throw it, for it to be captured by the following catch
throw renewPromise2Error;

Check warning on line 478 in src/wallet/wallet.ts

View check run for this annotation

Codecov / codecov/patch

src/wallet/wallet.ts#L478

Added line #L478 was not covered by tests
}
await tokenRenewPromise;
} catch (err) {
this.authToken = null;
Expand Down Expand Up @@ -497,6 +508,11 @@
// If auth token api fails we can still retry during wallet creation status polling.
let renewPromise2: Promise<void> | null = null;
try {
if (renewPromiseError) {
// If an error happened async before this await, we must
// throw it, for it to be captured by the following catch
throw renewPromiseError;
}
// Here we await the first auth token api call before continuing the startup process.
await renewPromise;
} catch (err) {
Expand All @@ -505,7 +521,9 @@
this.authToken = null;
const walletId = HathorWalletServiceWallet.getWalletIdFromXPub(xpub);
this.walletId = walletId;
renewPromise2 = this.validateAndRenewAuthToken(pinCode);
renewPromise2 = this.validateAndRenewAuthToken(pinCode).catch(renew2Err => {
renewPromise2Error = renew2Err;
});
}

await handleCreate(data.status, renewPromise2);
Expand Down
Loading