Skip to content

Commit 5ca31e7

Browse files
committed
fix: Make Gaia hub profile fetching and uploading in makeAuthResponse optional
1 parent c8267f2 commit 5ca31e7

File tree

4 files changed

+133
-12
lines changed

4 files changed

+133
-12
lines changed

package-lock.json

Lines changed: 0 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/wallet-sdk/src/models/account.ts

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,20 @@ import {
1313
} from '@stacks/encryption';
1414
import { NetworkParam, StacksNetwork, StacksNetworkName } from '@stacks/network';
1515
import { getAddressFromPrivateKey } from '@stacks/transactions';
16-
import { connectToGaiaHubWithConfig, getHubInfo, makeGaiaAssociationToken } from '../utils';
16+
import {
17+
HubInfo,
18+
connectToGaiaHubWithConfig,
19+
getHubInfo,
20+
makeGaiaAssociationToken,
21+
} from '../utils';
1722
import { Account, HARDENED_OFFSET } from './common';
1823
import {
1924
DEFAULT_PROFILE,
2025
fetchAccountProfileUrl,
2126
fetchProfileFromUrl,
2227
signAndUploadProfile,
2328
} from './profile';
29+
import { PublicProfileBase } from '@stacks/profile';
2430

2531
export function getStxAddress(
2632
account: Account,
@@ -76,6 +82,28 @@ export const getAppPrivateKey = ({
7682
return bytesToHex(appKeychain.privateKey);
7783
};
7884

85+
/** @internal helper */
86+
async function optionalGaiaProfileData({
87+
gaiaHubUrl,
88+
fetchFn,
89+
account,
90+
}: {
91+
gaiaHubUrl: string;
92+
fetchFn: FetchFn;
93+
account: Account;
94+
}): Promise<{
95+
hubInfo?: HubInfo;
96+
profileUrl?: string;
97+
profile?: PublicProfileBase | null;
98+
}> {
99+
const hubInfo = await getHubInfo(gaiaHubUrl, fetchFn).catch(() => undefined);
100+
if (!hubInfo) return {}; // keep data undefined if hub is not available
101+
102+
const profileUrl = await fetchAccountProfileUrl({ account, gaiaHubUrl: hubInfo.read_url_prefix });
103+
const profile = (await fetchProfileFromUrl(profileUrl, fetchFn)) || DEFAULT_PROFILE;
104+
return { hubInfo, profileUrl, profile };
105+
}
106+
79107
export const makeAuthResponse = async ({
80108
account,
81109
appDomain,
@@ -96,10 +124,14 @@ export const makeAuthResponse = async ({
96124
fetchFn?: FetchFn;
97125
}) => {
98126
const appPrivateKey = getAppPrivateKey({ account, appDomain });
99-
const hubInfo = await getHubInfo(gaiaHubUrl, fetchFn);
100-
const profileUrl = await fetchAccountProfileUrl({ account, gaiaHubUrl: hubInfo.read_url_prefix });
101-
const profile = (await fetchProfileFromUrl(profileUrl, fetchFn)) || DEFAULT_PROFILE;
102-
if (scopes.includes('publish_data')) {
127+
128+
const { hubInfo, profileUrl, profile } = await optionalGaiaProfileData({
129+
gaiaHubUrl,
130+
fetchFn,
131+
account,
132+
});
133+
134+
if (scopes.includes('publish_data') && hubInfo && profile) {
103135
if (!profile.apps) {
104136
profile.apps = {};
105137
}
@@ -138,9 +170,7 @@ export const makeAuthResponse = async ({
138170
},
139171
...additionalData,
140172
},
141-
{
142-
profileUrl,
143-
},
173+
profileUrl ? { profileUrl } : null,
144174
undefined,
145175
appPrivateKey,
146176
undefined,

packages/wallet-sdk/src/utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ export const getProfileURLFromZoneFile = async (
3434
return;
3535
};
3636

37-
interface HubInfo {
37+
/** @internal */
38+
export interface HubInfo {
3839
challenge_text?: string;
3940
read_url_prefix: string;
4041
}

packages/wallet-sdk/tests/models/account.test.ts

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ test('generates the correct app private key', () => {
2626
expect(appPrivateKey).toEqual(expectedKey);
2727
});
2828

29-
describe(makeAuthResponse, () => {
29+
describe(makeAuthResponse.name, () => {
3030
test('generates an auth response', async () => {
3131
const account = mockAccount;
3232
const appDomain = 'https://banter.pub';
@@ -128,4 +128,96 @@ describe(makeAuthResponse, () => {
128128
expect(appPrivateKey).toEqual(expectedKey);
129129
expect(payload.appPrivateKeyFromWalletSalt).toEqual(appPrivateKeyFromWalletSalt);
130130
});
131+
132+
describe('Gaia optional functionality', () => {
133+
const TEST_CASES = [
134+
{
135+
mock: () => fetchMock.mockRejectOnce(new Error('Network error')),
136+
opts: {},
137+
},
138+
{
139+
mock: () => fetchMock.mockResponseOnce('Internal Server Error', { status: 500 }),
140+
opts: {},
141+
},
142+
{
143+
mock: () => fetchMock.mockRejectOnce(new Error('Connection timeout')),
144+
opts: { scopes: ['publish_data'] },
145+
},
146+
{
147+
mock: () => fetchMock.mockResponseOnce('Not Found', { status: 404 }),
148+
opts: { scopes: ['publish_data'] },
149+
},
150+
{
151+
mock: () =>
152+
fetchMock.mockImplementationOnce(() => Promise.reject(new Error('Request timeout'))),
153+
opts: { scopes: ['read_write'] },
154+
},
155+
];
156+
test.each(TEST_CASES)(makeAuthResponse.name, async ({ mock, opts }) => {
157+
mock();
158+
159+
const account = mockAccount;
160+
const transitPrivateKey = makeECPrivateKey();
161+
const transitPublicKey = getPublicKeyFromPrivate(transitPrivateKey);
162+
163+
const authResponse = await makeAuthResponse({
164+
appDomain: 'https://banter.pub',
165+
gaiaHubUrl,
166+
transitPublicKey,
167+
account,
168+
...opts,
169+
});
170+
171+
// Common verifications
172+
expect(authResponse).toBeTruthy();
173+
const decoded = decodeToken(authResponse);
174+
const { payload } = decoded as Decoded;
175+
176+
expect(payload.profile_url).toBeNull();
177+
expect(payload.profile).toBeDefined();
178+
expect(payload.profile.stxAddress).toBeDefined();
179+
expect(payload.profile.stxAddress.testnet).toBeDefined();
180+
expect(payload.profile.stxAddress.mainnet).toBeDefined();
181+
182+
expect(fetchMock.mock.calls.length).toEqual(1);
183+
});
184+
185+
test('makeAuthResponse handles mixed success/failure gracefully', async () => {
186+
const account = mockAccount;
187+
const transitPrivateKey = makeECPrivateKey();
188+
const transitPublicKey = getPublicKeyFromPrivate(transitPrivateKey);
189+
190+
// First call succeeds with hub info, second call (profile fetch) fails
191+
fetchMock
192+
.mockResponseOnce(mockGaiaHubInfo)
193+
.mockResponseOnce('', { status: 404 }) // profile fetch fails
194+
.mockResponseOnce(JSON.stringify({ publicUrl: 'asdf' })); // profile upload succeeds
195+
196+
const authResponse = await makeAuthResponse({
197+
appDomain: 'https://banter.pub',
198+
gaiaHubUrl,
199+
transitPublicKey,
200+
account,
201+
scopes: ['publish_data'],
202+
});
203+
204+
// Verify auth response is generated successfully
205+
expect(authResponse).toBeTruthy();
206+
const decoded = decodeToken(authResponse);
207+
const { payload } = decoded as Decoded;
208+
209+
// Verify we get profile_url since hub info succeeded
210+
expect(payload.profile_url).toEqual(
211+
`https://gaia.blockstack.org/hub/${getGaiaAddress(account)}/profile.json`
212+
);
213+
214+
// Verify profile upload was successful and profile apps were added
215+
expect(payload.profile).toBeDefined();
216+
expect(payload.profile.apps).toBeDefined();
217+
expect(payload.profile.apps['https://banter.pub']).toBeDefined();
218+
219+
// Verify three fetch calls were made (hub info, profile fetch, profile upload)
220+
expect(fetchMock.mock.calls.length).toEqual(3);
221+
});
222+
});
131223
});

0 commit comments

Comments
 (0)