Skip to content

Commit 9b2f238

Browse files
authored
Merge pull request #7097 from BitGo/WP-5538/add-clients-constants-support
feat(sdk-api): Add support for passing constants to the SDK as an optional parameter
2 parents 52f91a5 + 7b85881 commit 9b2f238

File tree

4 files changed

+159
-34
lines changed

4 files changed

+159
-34
lines changed

modules/sdk-api/src/bitgoAPI.ts

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import {
5555
CalculateRequestHeadersOptions,
5656
CalculateRequestHmacOptions,
5757
ChangePasswordOptions,
58+
Constants,
5859
DeprecatedVerifyAddressOptions,
5960
EstimateFeeOptions,
6061
ExtendTokenOptions,
@@ -275,6 +276,10 @@ export class BitGoAPI implements BitGoBase {
275276
this._baseApiUrlV2 = this._baseUrl + '/api/v2';
276277
this._baseApiUrlV3 = this._baseUrl + '/api/v3';
277278
this._token = params.accessToken;
279+
280+
const clientConstants = params.clientConstants;
281+
this._initializeClientConstants(clientConstants);
282+
278283
this._userAgent = params.userAgent || 'BitGoJS-api/' + this.version();
279284
this._reqId = undefined;
280285
this._refreshToken = params.refreshToken;
@@ -303,17 +308,34 @@ export class BitGoAPI implements BitGoBase {
303308

304309
this._customProxyAgent = params.customProxyAgent;
305310

306-
// capture outer stack so we have useful debug information if fetch constants fails
307-
const e = new Error();
311+
// Only fetch constants from constructor if clientConstants was not provided
312+
if (!clientConstants) {
313+
// capture outer stack so we have useful debug information if fetch constants fails
314+
const e = new Error();
315+
316+
// Kick off first load of constants
317+
this.fetchConstants().catch((err) => {
318+
if (err) {
319+
// make sure an error does not terminate the entire script
320+
console.error('failed to fetch initial client constants from BitGo');
321+
debug(e.stack);
322+
}
323+
});
324+
}
325+
}
308326

309-
// Kick off first load of constants
310-
this.fetchConstants().catch((err) => {
311-
if (err) {
312-
// make sure an error does not terminate the entire script
313-
console.error('failed to fetch initial client constants from BitGo');
314-
debug(e.stack);
327+
/**
328+
* Initialize client constants if provided.
329+
* @param clientConstants - The client constants from params
330+
* @private
331+
*/
332+
private _initializeClientConstants(clientConstants: any): void {
333+
if (clientConstants) {
334+
if (!BitGoAPI._constants) {
335+
BitGoAPI._constants = {};
315336
}
316-
});
337+
BitGoAPI._constants[this.env] = 'constants' in clientConstants ? clientConstants.constants : clientConstants;
338+
}
317339
}
318340

319341
/**
@@ -531,17 +553,15 @@ export class BitGoAPI implements BitGoBase {
531553
* but are unlikely to change during the lifetime of a BitGo object,
532554
* so they can safely cached.
533555
*/
534-
async fetchConstants(): Promise<any> {
556+
async fetchConstants(): Promise<Constants> {
535557
const env = this.getEnv();
536558

537-
if (!BitGoAPI._constants) {
538-
BitGoAPI._constants = {};
539-
}
540-
if (!BitGoAPI._constantsExpire) {
541-
BitGoAPI._constantsExpire = {};
542-
}
543-
544-
if (BitGoAPI._constants[env] && BitGoAPI._constantsExpire[env] && new Date() < BitGoAPI._constantsExpire[env]) {
559+
// Check if we have cached constants that haven't expired
560+
if (
561+
BitGoAPI._constants &&
562+
BitGoAPI._constants[env] &&
563+
(!BitGoAPI._constantsExpire || !BitGoAPI._constantsExpire[env] || new Date() < BitGoAPI._constantsExpire[env])
564+
) {
545565
return BitGoAPI._constants[env];
546566
}
547567

@@ -560,9 +580,16 @@ export class BitGoAPI implements BitGoBase {
560580
}
561581
}
562582
const result = await resultPromise;
583+
584+
if (!BitGoAPI._constants) {
585+
BitGoAPI._constants = {};
586+
}
563587
BitGoAPI._constants[env] = result.body.constants;
564588

565589
if (result.body?.ttl && typeof result.body?.ttl === 'number') {
590+
if (!BitGoAPI._constantsExpire) {
591+
BitGoAPI._constantsExpire = {};
592+
}
566593
BitGoAPI._constantsExpire[env] = new Date(new Date().getTime() + (result.body.ttl as number) * 1000);
567594
}
568595

@@ -2116,8 +2143,8 @@ export class BitGoAPI implements BitGoBase {
21162143
}
21172144
});
21182145

2119-
// use defaultConstants as the backup for keys that are not set in this._constants
2120-
return _.merge({}, defaultConstants(this.getEnv()), BitGoAPI._constants[this.getEnv()]);
2146+
// use defaultConstants as the backup for keys that are not set in BitGoAPI._constants
2147+
return _.merge({}, defaultConstants(this.getEnv()), BitGoAPI._constants?.[this.getEnv()] || {});
21212148
}
21222149

21232150
/**

modules/sdk-api/src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { EnvironmentName, IRequestTracer, V1Network } from '@bitgo/sdk-core';
22
import { ECPairInterface } from '@bitgo/utxo-lib';
33
import { type Agent } from 'http';
44

5+
export type Constants = Record<string, any>;
6+
57
const patchedRequestMethods = ['get', 'post', 'put', 'del', 'patch', 'options'] as const;
68
export type RequestMethods = (typeof patchedRequestMethods)[number];
79
export type AdditionalHeadersCallback = (
@@ -23,6 +25,12 @@ export {
2325
export interface BitGoAPIOptions {
2426
accessToken?: string;
2527
authVersion?: 2 | 3;
28+
clientConstants?:
29+
| Record<string, any>
30+
| {
31+
constants: Record<string, any>;
32+
ttl?: number;
33+
};
2634
customBitcoinNetwork?: V1Network;
2735
customRootURI?: string;
2836
customSigningAddress?: string;

modules/sdk-api/test/unit/bitgoAPI.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'should';
22
import { BitGoAPI } from '../../src/bitgoAPI';
33
import { ProxyAgent } from 'proxy-agent';
44
import * as sinon from 'sinon';
5+
import nock from 'nock';
56

67
describe('Constructor', function () {
78
describe('cookiesPropagationEnabled argument', function () {
@@ -305,4 +306,102 @@ describe('Constructor', function () {
305306
result.should.containDeep(['wallet-id-2', 'wallet-id-4']);
306307
});
307308
});
309+
310+
describe('constants parameter', function () {
311+
it('should allow passing constants via options and expose via fetchConstants', async function () {
312+
const bitgo = new BitGoAPI({
313+
env: 'custom',
314+
customRootURI: 'https://app.example.local',
315+
clientConstants: { maxFeeRate: '123123123123123' },
316+
});
317+
318+
const constants = await bitgo.fetchConstants();
319+
constants.should.have.property('maxFeeRate', '123123123123123');
320+
});
321+
322+
it('should refresh constants when cache has expired', async function () {
323+
const bitgo = new BitGoAPI({
324+
env: 'custom',
325+
customRootURI: 'https://app.example.local',
326+
});
327+
328+
// Set up cached constants with an expired cache
329+
(BitGoAPI as any)._constants = (BitGoAPI as any)._constants || {};
330+
(BitGoAPI as any)._constantsExpire = (BitGoAPI as any)._constantsExpire || {};
331+
(BitGoAPI as any)._constants['custom'] = { maxFeeRate: 'old-value' };
332+
(BitGoAPI as any)._constantsExpire['custom'] = new Date(Date.now() - 1000); // Expired 1 second ago
333+
334+
const scope = nock('https://app.example.local')
335+
.get('/api/v1/client/constants')
336+
.reply(200, {
337+
constants: { maxFeeRate: 'new-value', newConstant: 'added' },
338+
});
339+
340+
const constants = await bitgo.fetchConstants();
341+
342+
// Should return the new constants from the server
343+
constants.should.have.property('maxFeeRate', 'new-value');
344+
constants.should.have.property('newConstant', 'added');
345+
346+
scope.isDone().should.be.true();
347+
348+
nock.cleanAll();
349+
});
350+
351+
it('should use cached constants when cache is still valid', async function () {
352+
const bitgo = new BitGoAPI({
353+
env: 'custom',
354+
customRootURI: 'https://app.example.local',
355+
});
356+
357+
// Set up cached constants with a future expiry
358+
const cachedConstants = { maxFeeRate: 'cached-value', anotherSetting: 'cached-setting' };
359+
(BitGoAPI as any)._constants = (BitGoAPI as any)._constants || {};
360+
(BitGoAPI as any)._constantsExpire = (BitGoAPI as any)._constantsExpire || {};
361+
(BitGoAPI as any)._constants['custom'] = cachedConstants;
362+
(BitGoAPI as any)._constantsExpire['custom'] = new Date(Date.now() + 5 * 60 * 1000); // Valid for 5 more minutes
363+
364+
const scope = nock('https://app.example.local')
365+
.get('/api/v1/client/constants')
366+
.reply(200, { constants: { shouldNotBeUsed: true } });
367+
368+
const constants = await bitgo.fetchConstants();
369+
370+
// Should return the cached constants
371+
constants.should.deepEqual(cachedConstants);
372+
373+
// Verify that no HTTP request was made (since cache was valid)
374+
scope.isDone().should.be.false();
375+
376+
nock.cleanAll();
377+
});
378+
379+
it('should use cached constants when no cache expiry is set', async function () {
380+
const bitgo = new BitGoAPI({
381+
env: 'custom',
382+
customRootURI: 'https://app.example.local',
383+
});
384+
385+
// Set up cached constants with no expiry
386+
const cachedConstants = { maxFeeRate: 'no-expiry-value' };
387+
(BitGoAPI as any)._constants = (BitGoAPI as any)._constants || {};
388+
(BitGoAPI as any)._constantsExpire = (BitGoAPI as any)._constantsExpire || {};
389+
(BitGoAPI as any)._constants['custom'] = cachedConstants;
390+
(BitGoAPI as any)._constantsExpire['custom'] = undefined;
391+
392+
const scope = nock('https://app.example.local')
393+
.get('/api/v1/client/constants')
394+
.reply(200, { constants: { shouldNotBeUsed: true } });
395+
396+
const constants = await bitgo.fetchConstants();
397+
398+
// Should return the cached constants
399+
constants.should.deepEqual(cachedConstants);
400+
401+
// Verify that no HTTP request was made (since no expiry means cache is always valid)
402+
scope.isDone().should.be.false();
403+
404+
nock.cleanAll();
405+
});
406+
});
308407
});

modules/sdk-api/test/unit/v1/wallet.ts

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,10 @@ nock.disableNetConnect();
2222
const TestBitGo = {
2323
TEST_WALLET1_PASSCODE: 'iVWeATjqLS1jJShrPpETti0b',
2424
};
25-
const originalFetchConstants = BitGoAPI.prototype.fetchConstants;
26-
BitGoAPI.prototype.fetchConstants = function (this: any) {
27-
nock(this._baseUrl).get('/api/v1/client/constants').reply(200, { ttl: 3600, constants: {} });
28-
29-
// force client constants reload
30-
BitGoAPI['_constants'] = undefined;
31-
32-
return originalFetchConstants.apply(this, arguments as any);
33-
};
3425
describe('Wallet Prototype Methods', function () {
3526
const fixtures = getFixtures();
3627

37-
let bitgo = new BitGoAPI({ env: 'test' });
28+
let bitgo = new BitGoAPI({ env: 'test', clientConstants: { constants: {} } });
3829
// bitgo.initializeTestVars();
3930

4031
const userKeypair = {
@@ -131,7 +122,7 @@ describe('Wallet Prototype Methods', function () {
131122

132123
before(function () {
133124
nock.pendingMocks().should.be.empty();
134-
const prodBitgo = new BitGoAPI({ env: 'prod' });
125+
const prodBitgo = new BitGoAPI({ env: 'prod', clientConstants: { constants: {} } });
135126
// prodBitgo.initializeTestVars();
136127
bgUrl = common.Environments[prodBitgo.getEnv()].uri;
137128
fakeProdWallet = new Wallet(prodBitgo, {
@@ -364,7 +355,7 @@ describe('Wallet Prototype Methods', function () {
364355
const { address, redeemScript, scriptPubKey } = await getFixture<Record<string, unknown>>(
365356
`${__dirname}/fixtures/sign-transaction.json`
366357
);
367-
const testBitgo = new BitGoAPI({ env: 'test' });
358+
const testBitgo = new BitGoAPI({ env: 'test', clientConstants: { constants: {} } });
368359
const fakeTestV1SafeWallet = new Wallet(testBitgo, {
369360
id: address,
370361
private: { safe: { redeemScript } },
@@ -426,7 +417,7 @@ describe('Wallet Prototype Methods', function () {
426417
halfSignedTxHex,
427418
fullSignedTxHex,
428419
} = await getFixture<Record<string, unknown>>(`${__dirname}/fixtures/sign-transaction.json`);
429-
const testBitgo = new BitGoAPI({ env: 'test' });
420+
const testBitgo = new BitGoAPI({ env: 'test', clientConstants: { constants: {} } });
430421
const fakeTestV1SafeWallet = new Wallet(testBitgo, {
431422
id: address,
432423
private: { safe: { redeemScript } },
@@ -745,7 +736,7 @@ describe('Wallet Prototype Methods', function () {
745736
before(function accelerateTxMockedBefore() {
746737
nock.pendingMocks().should.be.empty();
747738

748-
bitgo = new BitGoAPI({ env: 'mock' });
739+
bitgo = new BitGoAPI({ env: 'mock', clientConstants: { constants: {} } });
749740
// bitgo.initializeTestVars();
750741
bitgo.setValidate(false);
751742
wallet = new Wallet(bitgo, { id: walletId, private: { keychains: [userKeypair, backupKeypair, bitgoKey] } });

0 commit comments

Comments
 (0)