Skip to content

Commit ae0fd35

Browse files
pranishnepalclaude
andcommitted
fix: properly resolve merge conflict in handleGenerateWallet
Remove all conflict markers and keep both approaches: - Callback approach from PR #234 for sync onchain path - getBaseWalletParams for TSS and worker paths Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
1 parent 5c2c744 commit ae0fd35

14 files changed

Lines changed: 866 additions & 47 deletions
Lines changed: 393 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,393 @@
1+
import 'should';
2+
import nock from 'nock';
3+
import sinon from 'sinon';
4+
import { BitGoAPI } from '@bitgo-beta/sdk-api';
5+
import { Environments } from '@bitgo-beta/sdk-core';
6+
import { OsoBridgeClient } from '../../../masterBitgoExpress/clients/bridgeClient';
7+
import { BridgeJobResponse } from '../../../masterBitgoExpress/clients/bridgeClient.types';
8+
import {
9+
startAsyncJobWorker,
10+
processPendingJobs,
11+
handleKeyGenerationOperation,
12+
} from '../../../masterBitgoExpress/workers/asyncJobWorker';
13+
import { AppMode, MasterExpressConfig, TlsMode } from '../../../shared/types';
14+
import { DEFAULT_ASYNC_MODE_CONFIG } from './testUtils';
15+
16+
const BRIDGE_URL = 'http://bridge.invalid';
17+
const BITGO_API_URL = Environments.test.uri;
18+
const COIN = 'tbtc';
19+
const POLL_INTERVAL_MS = 1000;
20+
21+
function makeUserKeychain() {
22+
return {
23+
id: 'user-key-id',
24+
pub: 'xpub_user',
25+
encryptedPrv: 'encrypted-user-prv',
26+
type: 'independent' as const,
27+
source: 'user' as const,
28+
coin: COIN,
29+
};
30+
}
31+
32+
function makeBackupKeychain() {
33+
return {
34+
id: 'backup-key-id',
35+
pub: 'xpub_backup',
36+
encryptedPrv: 'encrypted-backup-prv',
37+
type: 'independent' as const,
38+
source: 'backup' as const,
39+
coin: COIN,
40+
};
41+
}
42+
43+
function awmOk(body: Record<string, unknown>) {
44+
return { status: 200, body };
45+
}
46+
47+
function makeJob(overrides: Partial<BridgeJobResponse> = {}): BridgeJobResponse {
48+
return {
49+
jobId: 'job-123',
50+
status: 'awaiting_bitgo',
51+
version: 1,
52+
coin: COIN,
53+
operationType: 'multisig_keygen',
54+
awmResponse: awmOk({ ...makeUserKeychain() }),
55+
awmBackupResponse: awmOk({ ...makeBackupKeychain() }),
56+
request: { body: { label: 'test-wallet', enterprise: 'test-enterprise' } },
57+
createdAt: '2026-06-10T00:00:00.000Z',
58+
updatedAt: '2026-06-10T00:00:00.000Z',
59+
...overrides,
60+
};
61+
}
62+
63+
function makeConfig(overrides: Partial<MasterExpressConfig> = {}): MasterExpressConfig {
64+
return {
65+
appMode: AppMode.MASTER_EXPRESS,
66+
port: 0,
67+
bind: 'localhost',
68+
timeout: 60000,
69+
httpLoggerFile: '',
70+
env: 'test',
71+
disableEnvCheck: true,
72+
authVersion: 2,
73+
advancedWalletManagerUrl: 'http://awm.invalid',
74+
awmServerCaCert: 'dummy-cert',
75+
tlsMode: TlsMode.DISABLED,
76+
clientCertAllowSelfSigned: true,
77+
bitgoAccessToken: 'test-access-token',
78+
asyncModeConfig: {
79+
...DEFAULT_ASYNC_MODE_CONFIG,
80+
enabled: true,
81+
awmAsyncUrl: BRIDGE_URL,
82+
pollIntervalInMs: POLL_INTERVAL_MS,
83+
},
84+
...overrides,
85+
};
86+
}
87+
88+
function nockBitgoKeychainRegistration(options: {
89+
pub: string;
90+
source: 'user' | 'backup';
91+
keyId: string;
92+
}) {
93+
return nock(BITGO_API_URL)
94+
.post(
95+
`/api/v2/${COIN}/key`,
96+
(body) => body.pub === options.pub && body.source === options.source,
97+
)
98+
.matchHeader('any', () => true)
99+
.reply(200, { id: options.keyId, pub: options.pub, source: options.source });
100+
}
101+
102+
function nockBitgoKeyCreate(keyId: string) {
103+
return nock(BITGO_API_URL)
104+
.post(`/api/v2/${COIN}/key`, (body) => body.source === 'bitgo')
105+
.matchHeader('any', () => true)
106+
.reply(200, { id: keyId, pub: 'xpub_bitgo', source: 'bitgo' });
107+
}
108+
109+
function nockWalletAdd(walletId: string) {
110+
return nock(BITGO_API_URL)
111+
.post(`/api/v2/${COIN}/wallet/add`)
112+
.matchHeader('any', () => true)
113+
.reply(200, {
114+
id: walletId,
115+
coin: COIN,
116+
label: 'test-wallet',
117+
keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'],
118+
});
119+
}
120+
121+
function nockUpdateJobFailed(jobId: string) {
122+
return nock(BRIDGE_URL)
123+
.patch(`/job/${jobId}`, (body) => body.status === 'failed')
124+
.reply(204);
125+
}
126+
127+
function nockUpdateJobComplete(jobId: string, walletId: string) {
128+
return nock(BRIDGE_URL)
129+
.patch(
130+
`/job/${jobId}`,
131+
(body) => body.status === 'complete' && body.result?.walletId === walletId,
132+
)
133+
.reply(204);
134+
}
135+
136+
describe('asyncJobWorker', () => {
137+
let bitgo: BitGoAPI;
138+
let bridge: OsoBridgeClient;
139+
140+
before(() => {
141+
nock.disableNetConnect();
142+
});
143+
144+
after(() => {
145+
nock.enableNetConnect();
146+
});
147+
148+
beforeEach(() => {
149+
bitgo = new BitGoAPI({ env: 'test', accessToken: 'test-access-token' });
150+
bridge = new OsoBridgeClient(BRIDGE_URL, 60000);
151+
});
152+
153+
afterEach(() => {
154+
nock.cleanAll();
155+
sinon.restore();
156+
});
157+
158+
describe('startAsyncJobWorker()', () => {
159+
it('starts polling at the configured interval', async () => {
160+
const clock = sinon.useFakeTimers();
161+
162+
const listJobsNock = nock(BRIDGE_URL)
163+
.get('/jobs')
164+
.query({ status: 'awaiting_bitgo' })
165+
.reply(200, { jobs: [] });
166+
167+
startAsyncJobWorker(makeConfig());
168+
169+
await clock.tickAsync(POLL_INTERVAL_MS);
170+
171+
listJobsNock.done();
172+
clock.restore();
173+
});
174+
175+
it('does not fire a second handler job while the first is still running', async () => {
176+
const clock = sinon.useFakeTimers();
177+
let callCount = 0;
178+
179+
nock(BRIDGE_URL)
180+
.get('/jobs')
181+
.query({ status: 'awaiting_bitgo' })
182+
.times(1)
183+
.reply(200, () => {
184+
callCount++;
185+
return { jobs: [] };
186+
});
187+
188+
startAsyncJobWorker(makeConfig());
189+
190+
await clock.tickAsync(POLL_INTERVAL_MS * 3);
191+
192+
callCount.should.equal(1);
193+
clock.restore();
194+
});
195+
});
196+
197+
describe('processPendingJobs()', () => {
198+
it('returns early when no awaiting_bitgo jobs exist', async () => {
199+
const n = nock(BRIDGE_URL)
200+
.get('/jobs')
201+
.query({ status: 'awaiting_bitgo' })
202+
.reply(200, { jobs: [] });
203+
204+
await processPendingJobs(bridge, bitgo).should.be.fulfilled();
205+
206+
n.done();
207+
});
208+
209+
it('processes all returned jobs', async () => {
210+
const job1 = makeJob({ jobId: 'job-1' });
211+
const job2 = makeJob({ jobId: 'job-2' });
212+
213+
nock(BRIDGE_URL)
214+
.get('/jobs')
215+
.query({ status: 'awaiting_bitgo' })
216+
.reply(200, { jobs: [job1, job2] });
217+
218+
nockBitgoKeychainRegistration({ pub: 'xpub_user', source: 'user', keyId: 'user-key-id' });
219+
nockBitgoKeychainRegistration({
220+
pub: 'xpub_backup',
221+
source: 'backup',
222+
keyId: 'backup-key-id',
223+
});
224+
nockBitgoKeyCreate('bitgo-key-id');
225+
nockWalletAdd('wallet-1');
226+
nockUpdateJobComplete('job-1', 'wallet-1');
227+
228+
nockBitgoKeychainRegistration({ pub: 'xpub_user', source: 'user', keyId: 'user-key-id' });
229+
nockBitgoKeychainRegistration({
230+
pub: 'xpub_backup',
231+
source: 'backup',
232+
keyId: 'backup-key-id',
233+
});
234+
nockBitgoKeyCreate('bitgo-key-id');
235+
nockWalletAdd('wallet-2');
236+
nockUpdateJobComplete('job-2', 'wallet-2');
237+
238+
await processPendingJobs(bridge, bitgo).should.be.fulfilled();
239+
240+
nock.pendingMocks().should.have.length(0);
241+
});
242+
243+
it('continues processing remaining jobs when one fails', async () => {
244+
const badJob = makeJob({
245+
jobId: 'job-bad',
246+
awmResponse: { status: 200, body: {} },
247+
});
248+
const goodJob = makeJob({ jobId: 'job-good' });
249+
250+
nock(BRIDGE_URL)
251+
.get('/jobs')
252+
.query({ status: 'awaiting_bitgo' })
253+
.reply(200, { jobs: [badJob, goodJob] });
254+
255+
nockUpdateJobFailed('job-bad');
256+
257+
nockBitgoKeychainRegistration({ pub: 'xpub_user', source: 'user', keyId: 'user-key-id' });
258+
nockBitgoKeychainRegistration({
259+
pub: 'xpub_backup',
260+
source: 'backup',
261+
keyId: 'backup-key-id',
262+
});
263+
nockBitgoKeyCreate('bitgo-key-id');
264+
nockWalletAdd('wallet-good');
265+
nockUpdateJobComplete('job-good', 'wallet-good');
266+
267+
await processPendingJobs(bridge, bitgo).should.be.fulfilled();
268+
269+
nock.pendingMocks().should.have.length(0);
270+
});
271+
272+
it('skips jobs with unknown operationType', async () => {
273+
const job = makeJob({ operationType: 'multisig_sign' });
274+
275+
const n = nock(BRIDGE_URL)
276+
.get('/jobs')
277+
.query({ status: 'awaiting_bitgo' })
278+
.reply(200, { jobs: [job] });
279+
280+
await processPendingJobs(bridge, bitgo).should.be.fulfilled();
281+
282+
n.done();
283+
});
284+
});
285+
286+
describe('handleKeyGenerationOperation()', () => {
287+
it('registers keychains, creates wallet, and PATCHes job complete', async () => {
288+
const job = makeJob();
289+
const walletId = 'new-wallet-id';
290+
291+
const userKeyNock = nockBitgoKeychainRegistration({
292+
pub: 'xpub_user',
293+
source: 'user',
294+
keyId: 'user-key-id',
295+
});
296+
const backupKeyNock = nockBitgoKeychainRegistration({
297+
pub: 'xpub_backup',
298+
source: 'backup',
299+
keyId: 'backup-key-id',
300+
});
301+
const bitgoKeyNock = nockBitgoKeyCreate('bitgo-key-id');
302+
const walletNock = nockWalletAdd(walletId);
303+
const updateNock = nockUpdateJobComplete(job.jobId, walletId);
304+
305+
await handleKeyGenerationOperation(job, bridge, bitgo);
306+
307+
userKeyNock.done();
308+
backupKeyNock.done();
309+
bitgoKeyNock.done();
310+
walletNock.done();
311+
updateNock.done();
312+
});
313+
314+
it('throws when awmResponse is missing', async () => {
315+
const job = makeJob({ awmResponse: undefined });
316+
317+
await handleKeyGenerationOperation(job, bridge, bitgo).should.be.rejected();
318+
});
319+
320+
it('throws when awmBackupResponse is missing', async () => {
321+
const job = makeJob({ awmBackupResponse: undefined });
322+
323+
await handleKeyGenerationOperation(job, bridge, bitgo).should.be.rejected();
324+
});
325+
326+
it('throws when awmResponse is not a valid AwmResponse envelope', async () => {
327+
const job = makeJob({
328+
awmResponse: { unexpected: 'shape' } as unknown as BridgeJobResponse['awmResponse'],
329+
});
330+
331+
await handleKeyGenerationOperation(job, bridge, bitgo).should.be.rejected();
332+
});
333+
334+
it('throws when WP keychain registration fails', async () => {
335+
const job = makeJob();
336+
337+
nock(BITGO_API_URL)
338+
.post(`/api/v2/${COIN}/key`)
339+
.matchHeader('any', () => true)
340+
.reply(500, { message: 'internal server error' });
341+
342+
await handleKeyGenerationOperation(job, bridge, bitgo).should.be.rejected();
343+
});
344+
345+
it('throws when wallet creation fails', async () => {
346+
const job = makeJob();
347+
348+
nockBitgoKeychainRegistration({ pub: 'xpub_user', source: 'user', keyId: 'user-key-id' });
349+
nockBitgoKeychainRegistration({
350+
pub: 'xpub_backup',
351+
source: 'backup',
352+
keyId: 'backup-key-id',
353+
});
354+
nockBitgoKeyCreate('bitgo-key-id');
355+
356+
nock(BITGO_API_URL)
357+
.post(`/api/v2/${COIN}/wallet/add`)
358+
.matchHeader('any', () => true)
359+
.reply(500, { message: 'wallet creation failed' });
360+
361+
await handleKeyGenerationOperation(job, bridge, bitgo).should.be.rejected();
362+
});
363+
364+
it('uses enterprise from request body when provided', async () => {
365+
const job = makeJob({
366+
request: { body: { label: 'ent-wallet', enterprise: 'my-enterprise' } },
367+
});
368+
const walletId = 'ent-wallet-id';
369+
370+
nockBitgoKeychainRegistration({ pub: 'xpub_user', source: 'user', keyId: 'user-key-id' });
371+
nockBitgoKeychainRegistration({
372+
pub: 'xpub_backup',
373+
source: 'backup',
374+
keyId: 'backup-key-id',
375+
});
376+
377+
nock(BITGO_API_URL)
378+
.post(
379+
`/api/v2/${COIN}/key`,
380+
(body) => body.source === 'bitgo' && body.enterprise === 'my-enterprise',
381+
)
382+
.matchHeader('any', () => true)
383+
.reply(200, { id: 'bitgo-key-id', pub: 'xpub_bitgo', source: 'bitgo' });
384+
385+
nockWalletAdd(walletId);
386+
nockUpdateJobComplete(job.jobId, walletId);
387+
388+
await handleKeyGenerationOperation(job, bridge, bitgo);
389+
390+
nock.pendingMocks().should.have.length(0);
391+
});
392+
});
393+
});

0 commit comments

Comments
 (0)