@bitpay-labs/bitcore-wallet-service 11.11.2 → 11.11.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/config.ts +8 -0
- package/src/externalservices/coingecko.ts +6 -1
- package/src/externalservices/transak.ts +394 -70
- package/src/lib/blockchainexplorer.ts +4 -0
- package/src/lib/blockchainexplorers/v8.ts +1 -0
- package/src/lib/blockchainmonitor.ts +1 -0
- package/src/lib/chain/arc/index.ts +95 -0
- package/src/lib/chain/eth/index.ts +41 -23
- package/src/lib/chain/index.ts +2 -0
- package/src/lib/common/constants.ts +13 -2
- package/src/lib/common/defaults.ts +31 -0
- package/src/lib/common/utils.ts +18 -1
- package/src/lib/emailservice.ts +3 -1
- package/src/lib/errors/errordefinitions.ts +4 -0
- package/src/lib/model/preferences.ts +4 -0
- package/src/lib/model/tsskeygen.ts +16 -0
- package/src/lib/model/wallet.ts +1 -0
- package/src/lib/pushnotificationsservice.ts +2 -0
- package/src/lib/routes/maintenance.ts +11 -2
- package/src/lib/routes/middleware/authTssRequest.ts +28 -16
- package/src/lib/routes/middleware/verifyTssMessage.ts +4 -1
- package/src/lib/routes/services.ts +8 -2
- package/src/lib/routes/tss.ts +33 -30
- package/src/lib/routes/walletdata.ts +2 -0
- package/src/lib/server.ts +82 -19
- package/src/lib/storage.ts +39 -0
- package/src/lib/tss.ts +111 -7
- package/test/blockchainexplorer.test.ts +7 -0
- package/test/expressapp.test.ts +130 -0
- package/test/integration/externalservices/coinGecko.test.ts +42 -2
- package/test/integration/externalservices/transak.test.ts +545 -135
- package/test/integration/fiatrateservice.test.ts +13 -0
- package/test/integration/helpers.ts +1 -0
- package/test/integration/server.test.ts +192 -4
- package/test/integration/tss.test.ts +428 -0
- package/ts_build/package.json +4 -4
- package/ts_build/src/config.d.ts.map +1 -1
- package/ts_build/src/config.js +8 -0
- package/ts_build/src/config.js.map +1 -1
- package/ts_build/src/externalservices/coingecko.d.ts.map +1 -1
- package/ts_build/src/externalservices/coingecko.js +6 -1
- package/ts_build/src/externalservices/coingecko.js.map +1 -1
- package/ts_build/src/externalservices/transak.d.ts +28 -2
- package/ts_build/src/externalservices/transak.d.ts.map +1 -1
- package/ts_build/src/externalservices/transak.js +274 -57
- package/ts_build/src/externalservices/transak.js.map +1 -1
- package/ts_build/src/lib/blockchainexplorer.d.ts.map +1 -1
- package/ts_build/src/lib/blockchainexplorer.js +4 -0
- package/ts_build/src/lib/blockchainexplorer.js.map +1 -1
- package/ts_build/src/lib/blockchainexplorers/v8.d.ts.map +1 -1
- package/ts_build/src/lib/blockchainexplorers/v8.js +1 -0
- package/ts_build/src/lib/blockchainexplorers/v8.js.map +1 -1
- package/ts_build/src/lib/blockchainmonitor.d.ts.map +1 -1
- package/ts_build/src/lib/blockchainmonitor.js +1 -0
- package/ts_build/src/lib/blockchainmonitor.js.map +1 -1
- package/ts_build/src/lib/chain/arc/index.d.ts +20 -0
- package/ts_build/src/lib/chain/arc/index.d.ts.map +1 -0
- package/ts_build/src/lib/chain/arc/index.js +95 -0
- package/ts_build/src/lib/chain/arc/index.js.map +1 -0
- package/ts_build/src/lib/chain/eth/index.d.ts +1 -0
- package/ts_build/src/lib/chain/eth/index.d.ts.map +1 -1
- package/ts_build/src/lib/chain/eth/index.js +38 -19
- package/ts_build/src/lib/chain/eth/index.js.map +1 -1
- package/ts_build/src/lib/chain/index.d.ts.map +1 -1
- package/ts_build/src/lib/chain/index.js +2 -0
- package/ts_build/src/lib/chain/index.js.map +1 -1
- package/ts_build/src/lib/common/constants.d.ts +13 -1
- package/ts_build/src/lib/common/constants.d.ts.map +1 -1
- package/ts_build/src/lib/common/constants.js +13 -2
- package/ts_build/src/lib/common/constants.js.map +1 -1
- package/ts_build/src/lib/common/defaults.d.ts +24 -0
- package/ts_build/src/lib/common/defaults.d.ts.map +1 -1
- package/ts_build/src/lib/common/defaults.js +30 -0
- package/ts_build/src/lib/common/defaults.js.map +1 -1
- package/ts_build/src/lib/common/index.d.ts +38 -1
- package/ts_build/src/lib/common/index.d.ts.map +1 -1
- package/ts_build/src/lib/common/utils.d.ts +1 -0
- package/ts_build/src/lib/common/utils.d.ts.map +1 -1
- package/ts_build/src/lib/common/utils.js +16 -1
- package/ts_build/src/lib/common/utils.js.map +1 -1
- package/ts_build/src/lib/emailservice.d.ts.map +1 -1
- package/ts_build/src/lib/emailservice.js +2 -0
- package/ts_build/src/lib/emailservice.js.map +1 -1
- package/ts_build/src/lib/errors/errordefinitions.d.ts +2 -0
- package/ts_build/src/lib/errors/errordefinitions.d.ts.map +1 -1
- package/ts_build/src/lib/errors/errordefinitions.js +2 -0
- package/ts_build/src/lib/errors/errordefinitions.js.map +1 -1
- package/ts_build/src/lib/model/preferences.d.ts +2 -0
- package/ts_build/src/lib/model/preferences.d.ts.map +1 -1
- package/ts_build/src/lib/model/preferences.js +3 -0
- package/ts_build/src/lib/model/preferences.js.map +1 -1
- package/ts_build/src/lib/model/tsskeygen.d.ts +10 -0
- package/ts_build/src/lib/model/tsskeygen.d.ts.map +1 -1
- package/ts_build/src/lib/model/tsskeygen.js +3 -0
- package/ts_build/src/lib/model/tsskeygen.js.map +1 -1
- package/ts_build/src/lib/model/wallet.d.ts.map +1 -1
- package/ts_build/src/lib/model/wallet.js +1 -0
- package/ts_build/src/lib/model/wallet.js.map +1 -1
- package/ts_build/src/lib/pushnotificationsservice.d.ts.map +1 -1
- package/ts_build/src/lib/pushnotificationsservice.js +2 -0
- package/ts_build/src/lib/pushnotificationsservice.js.map +1 -1
- package/ts_build/src/lib/routes/maintenance.d.ts.map +1 -1
- package/ts_build/src/lib/routes/maintenance.js +12 -4
- package/ts_build/src/lib/routes/maintenance.js.map +1 -1
- package/ts_build/src/lib/routes/middleware/authTssRequest.d.ts +6 -2
- package/ts_build/src/lib/routes/middleware/authTssRequest.d.ts.map +1 -1
- package/ts_build/src/lib/routes/middleware/authTssRequest.js +21 -14
- package/ts_build/src/lib/routes/middleware/authTssRequest.js.map +1 -1
- package/ts_build/src/lib/routes/middleware/verifyTssMessage.d.ts.map +1 -1
- package/ts_build/src/lib/routes/middleware/verifyTssMessage.js +2 -1
- package/ts_build/src/lib/routes/middleware/verifyTssMessage.js.map +1 -1
- package/ts_build/src/lib/routes/services.d.ts.map +1 -1
- package/ts_build/src/lib/routes/services.js +7 -2
- package/ts_build/src/lib/routes/services.js.map +1 -1
- package/ts_build/src/lib/routes/tss.d.ts.map +1 -1
- package/ts_build/src/lib/routes/tss.js +32 -30
- package/ts_build/src/lib/routes/tss.js.map +1 -1
- package/ts_build/src/lib/routes/walletdata.d.ts.map +1 -1
- package/ts_build/src/lib/routes/walletdata.js +2 -0
- package/ts_build/src/lib/routes/walletdata.js.map +1 -1
- package/ts_build/src/lib/server.d.ts +23 -2
- package/ts_build/src/lib/server.d.ts.map +1 -1
- package/ts_build/src/lib/server.js +37 -5
- package/ts_build/src/lib/server.js.map +1 -1
- package/ts_build/src/lib/storage.d.ts +9 -0
- package/ts_build/src/lib/storage.d.ts.map +1 -1
- package/ts_build/src/lib/storage.js +26 -0
- package/ts_build/src/lib/storage.js.map +1 -1
- package/ts_build/src/lib/tss.d.ts +12 -0
- package/ts_build/src/lib/tss.d.ts.map +1 -1
- package/ts_build/src/lib/tss.js +64 -5
- package/ts_build/src/lib/tss.js.map +1 -1
- package/ts_build/test/blockchainexplorer.test.js +6 -0
- package/ts_build/test/blockchainexplorer.test.js.map +1 -1
- package/ts_build/test/expressapp.test.js +120 -0
- package/ts_build/test/expressapp.test.js.map +1 -1
- package/ts_build/test/integration/externalservices/coinGecko.test.js +38 -2
- package/ts_build/test/integration/externalservices/coinGecko.test.js.map +1 -1
- package/ts_build/test/integration/externalservices/transak.test.d.ts.map +1 -1
- package/ts_build/test/integration/externalservices/transak.test.js +498 -128
- package/ts_build/test/integration/externalservices/transak.test.js.map +1 -1
- package/ts_build/test/integration/fiatrateservice.test.js +11 -0
- package/ts_build/test/integration/fiatrateservice.test.js.map +1 -1
- package/ts_build/test/integration/helpers.d.ts.map +1 -1
- package/ts_build/test/integration/helpers.js +1 -0
- package/ts_build/test/integration/helpers.js.map +1 -1
- package/ts_build/test/integration/server.test.js +182 -3
- package/ts_build/test/integration/server.test.js.map +1 -1
- package/ts_build/test/integration/tss.test.d.ts.map +1 -1
- package/ts_build/test/integration/tss.test.js +344 -0
- package/ts_build/test/integration/tss.test.js.map +1 -1
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@bitpay-labs/bitcore-wallet-service",
|
|
3
3
|
"description": "A service for Mutisig HD Bitcoin Wallets",
|
|
4
4
|
"author": "BitPay Inc",
|
|
5
|
-
"version": "11.11.
|
|
5
|
+
"version": "11.11.4",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"main": "ts_build/src/index.js",
|
|
8
8
|
"types": "./ts_build/src/index.d.ts",
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@bitpay-labs/bitcore-logging": "^11.11.2",
|
|
45
|
-
"@bitpay-labs/bitcore-tss": "^11.11.
|
|
46
|
-
"@bitpay-labs/crypto-wallet-core": "^11.11.
|
|
45
|
+
"@bitpay-labs/bitcore-tss": "^11.11.4",
|
|
46
|
+
"@bitpay-labs/crypto-wallet-core": "^11.11.4",
|
|
47
47
|
"@sendgrid/mail": "6.5.4",
|
|
48
48
|
"abi-decoder": "2.4.0",
|
|
49
49
|
"async": "0.9.2",
|
|
@@ -107,5 +107,5 @@
|
|
|
107
107
|
"email": "kjoseph@bitpay.com"
|
|
108
108
|
}
|
|
109
109
|
],
|
|
110
|
-
"gitHead": "
|
|
110
|
+
"gitHead": "1eb34cc33d0628f9304be08a19da5315f140e70f"
|
|
111
111
|
}
|
package/src/config.ts
CHANGED
|
@@ -130,6 +130,14 @@ const Config = (): any => {
|
|
|
130
130
|
url: 'https://api-eth.bitcore.io'
|
|
131
131
|
}
|
|
132
132
|
},
|
|
133
|
+
arc: {
|
|
134
|
+
livenet: {
|
|
135
|
+
url: 'https://api-eth.bitcore.io'
|
|
136
|
+
},
|
|
137
|
+
testnet: {
|
|
138
|
+
url: 'https://api-eth.bitcore.io'
|
|
139
|
+
}
|
|
140
|
+
},
|
|
133
141
|
sol: {
|
|
134
142
|
livenet: {
|
|
135
143
|
url: 'https://api-sol.bitcore.io'
|
|
@@ -64,6 +64,7 @@ const DEFAULT_COINS: Record<string, string> = {
|
|
|
64
64
|
pol: 'polygon-ecosystem-token',
|
|
65
65
|
sol: 'solana',
|
|
66
66
|
xrp: 'ripple',
|
|
67
|
+
arc: 'usd-coin',
|
|
67
68
|
};
|
|
68
69
|
|
|
69
70
|
const DEFAULT_COIN_ENTRIES = Object.entries(DEFAULT_COINS);
|
|
@@ -79,6 +80,7 @@ const CHAIN_ALIASES = {
|
|
|
79
80
|
} as const satisfies Record<string, BitPayTokenChain>;
|
|
80
81
|
|
|
81
82
|
const COINGECKO_PLATFORM_ID_BY_CHAIN = {
|
|
83
|
+
[Constants.EVM_CHAINS.ARC]: 'arc',
|
|
82
84
|
[Constants.EVM_CHAINS.ARB]: 'arbitrum-one',
|
|
83
85
|
[Constants.EVM_CHAINS.BASE]: 'base',
|
|
84
86
|
[Constants.EVM_CHAINS.ETH]: 'ethereum',
|
|
@@ -527,7 +529,7 @@ export class CoinGeckoService {
|
|
|
527
529
|
|
|
528
530
|
const cacheKey = `cgTokenList:${platformId}`;
|
|
529
531
|
|
|
530
|
-
|
|
532
|
+
const tokenList = await this.withGlobalCache(cacheKey, Defaults.COIN_GECKO_CACHE_DURATION, async () => {
|
|
531
533
|
const { API_KEY } = this.coinGeckoGetCredentials();
|
|
532
534
|
const headers = this.coinGeckoGetHeaders(API_KEY);
|
|
533
535
|
|
|
@@ -543,5 +545,8 @@ export class CoinGeckoService {
|
|
|
543
545
|
return { ...t, logoURI };
|
|
544
546
|
});
|
|
545
547
|
});
|
|
548
|
+
return platformId === 'arc'
|
|
549
|
+
? tokenList.filter(token => token?.address?.toLowerCase() !== Constants.ARC.USDC_CONTRACT_ADDRESS)
|
|
550
|
+
: tokenList;
|
|
546
551
|
}
|
|
547
552
|
}
|
|
@@ -1,12 +1,106 @@
|
|
|
1
|
+
import * as crypto from 'crypto';
|
|
2
|
+
import { Web3 } from '@bitpay-labs/crypto-wallet-core';
|
|
1
3
|
import * as request from 'request';
|
|
2
4
|
import config from '../config';
|
|
3
5
|
import { Utils } from '../lib/common/utils';
|
|
4
6
|
import { ClientError } from '../lib/errors/clienterror';
|
|
7
|
+
import { Lock } from '../lib/lock';
|
|
5
8
|
import logger from '../lib/logger';
|
|
6
9
|
import { checkRequired } from '../lib/server';
|
|
10
|
+
import { Storage } from '../lib/storage';
|
|
11
|
+
|
|
12
|
+
interface TransakKeys {
|
|
13
|
+
ENV: string;
|
|
14
|
+
API: string;
|
|
15
|
+
API_KEY: string;
|
|
16
|
+
SECRET_KEY: string;
|
|
17
|
+
WIDGET_API: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface TransakPartnerToken {
|
|
21
|
+
accessToken: string;
|
|
22
|
+
/** Unix epoch in milliseconds */
|
|
23
|
+
expiresAt: number;
|
|
24
|
+
/** Unix epoch in milliseconds */
|
|
25
|
+
fetchedAt: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The partner access token from /partners/api/v2/refresh-token is scoped to
|
|
29
|
+
// BitPay's whole Transak account (it can read every order), so it must never
|
|
30
|
+
// leave BWS. It is cached in the shared global cache so every BWS instance
|
|
31
|
+
// reuses one token until it nears expiry, as Transak requires.
|
|
32
|
+
const PARTNER_TOKEN_CACHE_PREFIX = 'transakPartnerToken:';
|
|
33
|
+
const PARTNER_TOKEN_CACHE_MAX_AGE = 7 * 24 * 60 * 60 * 1000;
|
|
34
|
+
const PARTNER_TOKEN_REFRESH_BUFFER = 10 * 60 * 1000;
|
|
35
|
+
// Used if Transak omits expiresAt or returns one that is already (nearly) past.
|
|
36
|
+
const PARTNER_TOKEN_FALLBACK_TTL = 60 * 60 * 1000;
|
|
37
|
+
// If Transak rejects a token fetched less than this long ago, the rejection
|
|
38
|
+
// isn't about token age, so don't refresh again.
|
|
39
|
+
const PARTNER_TOKEN_MIN_FORCED_REFRESH_AGE = 60 * 1000;
|
|
40
|
+
// Statuses that mean Transak rejected the token itself.
|
|
41
|
+
const PARTNER_TOKEN_REJECTED_STATUSES = [401, 403];
|
|
42
|
+
// If a refresh can't happen, keep serving the current token until this close
|
|
43
|
+
// to its real expiry.
|
|
44
|
+
const PARTNER_TOKEN_EXPIRY_MARGIN = 30 * 1000;
|
|
45
|
+
const PARTNER_TOKEN_REQUEST_TIMEOUT = 10 * 1000;
|
|
46
|
+
// Lock time is well above the request timeout so a holder never outlives its
|
|
47
|
+
// lock (Lock.release is not owner-checked).
|
|
48
|
+
const PARTNER_TOKEN_LOCK_TIME = 30 * 1000;
|
|
49
|
+
const PARTNER_TOKEN_LOCK_WAIT = 15 * 1000;
|
|
50
|
+
// After a failed refresh, don't retry for this long (shared across instances),
|
|
51
|
+
// so an outage or disabled key doesn't turn every checkout into a refresh call.
|
|
52
|
+
const PARTNER_TOKEN_FAILURE_PREFIX = 'transakPartnerTokenFailure:';
|
|
53
|
+
const PARTNER_TOKEN_FAILURE_BACKOFF = 30 * 1000;
|
|
54
|
+
|
|
55
|
+
// App builds and the bitpay.com widget still call getAccessToken and refuse to
|
|
56
|
+
// continue without a non-empty accessToken and an integer expiresAt (seconds).
|
|
57
|
+
// They get this placeholder; BWS ignores whatever token a client sends back.
|
|
58
|
+
const CLIENT_TOKEN_PLACEHOLDER = 'server-managed';
|
|
59
|
+
// Short so a rollback (or old pods during a rolling deploy) only sees the
|
|
60
|
+
// placeholder briefly; getAccessToken no longer calls Transak, so it's cheap.
|
|
61
|
+
const CLIENT_TOKEN_PLACEHOLDER_TTL = 2 * 60; // seconds
|
|
62
|
+
|
|
63
|
+
// Only these client fields are forwarded to Transak's front-end widget.
|
|
64
|
+
const WIDGET_PARAMS = [
|
|
65
|
+
'walletAddress',
|
|
66
|
+
'disableWalletAddressForm',
|
|
67
|
+
'redirectURL',
|
|
68
|
+
'exchangeScreenTitle',
|
|
69
|
+
'fiatAmount',
|
|
70
|
+
'fiatCurrency',
|
|
71
|
+
'network',
|
|
72
|
+
'defaultNetwork',
|
|
73
|
+
'cryptoCurrencyCode',
|
|
74
|
+
'cryptoCurrencyList',
|
|
75
|
+
'hideExchangeScreen',
|
|
76
|
+
'themeColor',
|
|
77
|
+
'hideMenu',
|
|
78
|
+
'partnerOrderId',
|
|
79
|
+
'partnerCustomerId',
|
|
80
|
+
'paymentMethod',
|
|
81
|
+
'defaultPaymentMethod',
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
const ORDER_ID_PATTERN = /^[A-Za-z0-9-]{1,64}$/;
|
|
85
|
+
|
|
86
|
+
// Transak's error message, if any, for logs. Never the whole body.
|
|
87
|
+
function transakErrorDetail(data): string {
|
|
88
|
+
const message = data?.body?.error?.message ?? data?.body?.message;
|
|
89
|
+
if (message == null) return '';
|
|
90
|
+
const text = typeof message === 'string' ? message : JSON.stringify(message);
|
|
91
|
+
return ` (${text.slice(0, 200)})`;
|
|
92
|
+
}
|
|
7
93
|
|
|
8
94
|
export class TransakService {
|
|
9
95
|
request: any = request;
|
|
96
|
+
storage: Storage;
|
|
97
|
+
lock: Lock;
|
|
98
|
+
partnerTokenLockWait = PARTNER_TOKEN_LOCK_WAIT;
|
|
99
|
+
|
|
100
|
+
constructor(storage: Storage, lock?: Lock) {
|
|
101
|
+
this.storage = storage;
|
|
102
|
+
this.lock = lock ?? new Lock(storage);
|
|
103
|
+
}
|
|
10
104
|
|
|
11
105
|
private transakGetKeys(req, cleanBody: boolean = true) {
|
|
12
106
|
if (!config.transak) throw new Error('Transak missing credentials');
|
|
@@ -22,12 +116,8 @@ export class TransakService {
|
|
|
22
116
|
delete req.body.context;
|
|
23
117
|
}
|
|
24
118
|
|
|
25
|
-
const keys: {
|
|
26
|
-
|
|
27
|
-
API_KEY: string;
|
|
28
|
-
SECRET_KEY: string;
|
|
29
|
-
WIDGET_API: string;
|
|
30
|
-
} = {
|
|
119
|
+
const keys: TransakKeys = {
|
|
120
|
+
ENV: env,
|
|
31
121
|
API: config.transak[env].api,
|
|
32
122
|
API_KEY: config.transak[env].apiKey,
|
|
33
123
|
SECRET_KEY: config.transak[env].secretKey,
|
|
@@ -55,15 +145,94 @@ export class TransakService {
|
|
|
55
145
|
return userIp || '';
|
|
56
146
|
}
|
|
57
147
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
148
|
+
/**
|
|
149
|
+
* Kept for clients that still fetch a token before checkout. Returns a
|
|
150
|
+
* placeholder in the shape they expect; the real partner token stays in BWS.
|
|
151
|
+
*/
|
|
152
|
+
async transakGetAccessToken(req): Promise<{ data: { accessToken: string; expiresAt: number } }> {
|
|
153
|
+
this.transakGetKeys(req);
|
|
154
|
+
return {
|
|
155
|
+
data: {
|
|
156
|
+
accessToken: CLIENT_TOKEN_PLACEHOLDER,
|
|
157
|
+
expiresAt: Math.floor(Date.now() / 1000) + CLIENT_TOKEN_PLACEHOLDER_TTL
|
|
66
158
|
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Cache/lock id for a partner token. Includes a hash of the API key so a
|
|
164
|
+
* rotated key never reuses a token minted for the old one.
|
|
165
|
+
*/
|
|
166
|
+
private partnerTokenId(keys: TransakKeys): string {
|
|
167
|
+
const keyHash = crypto.createHash('sha256').update(String(keys.API_KEY)).digest('hex').slice(0, 12);
|
|
168
|
+
return `${keys.ENV}:${keyHash}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private readCachedPartnerToken(id: string): Promise<TransakPartnerToken | null> {
|
|
172
|
+
return new Promise(resolve => {
|
|
173
|
+
this.storage.checkAndUseGlobalCache(PARTNER_TOKEN_CACHE_PREFIX + id, PARTNER_TOKEN_CACHE_MAX_AGE, (err, values) => {
|
|
174
|
+
if (err) {
|
|
175
|
+
logger.warn(`Transak partner token cache read failed: ${err.message || err}`);
|
|
176
|
+
return resolve(null);
|
|
177
|
+
}
|
|
178
|
+
return resolve(values?.accessToken ? values : null);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private storeCachedPartnerToken(id: string, token: TransakPartnerToken): Promise<void> {
|
|
184
|
+
return new Promise(resolve => {
|
|
185
|
+
this.storage.storeGlobalCache(PARTNER_TOKEN_CACHE_PREFIX + id, token, err => {
|
|
186
|
+
if (err) logger.warn(`Transak partner token cache write failed: ${err.message || err}`);
|
|
187
|
+
return resolve();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private recentRefreshFailed(id: string): Promise<boolean> {
|
|
193
|
+
return new Promise(resolve => {
|
|
194
|
+
this.storage.checkAndUseGlobalCache(PARTNER_TOKEN_FAILURE_PREFIX + id, PARTNER_TOKEN_FAILURE_BACKOFF, (err, values) => {
|
|
195
|
+
return resolve(!err && !!values);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private markRefreshFailed(id: string): Promise<void> {
|
|
201
|
+
return new Promise(resolve => {
|
|
202
|
+
this.storage.storeGlobalCache(PARTNER_TOKEN_FAILURE_PREFIX + id, { failedAt: Date.now() }, () => resolve());
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private acquirePartnerTokenLock(id: string): Promise<((cb?: () => void) => void) | null> {
|
|
207
|
+
return new Promise(resolve => {
|
|
208
|
+
this.lock.acquire(
|
|
209
|
+
PARTNER_TOKEN_CACHE_PREFIX + id,
|
|
210
|
+
{ lockTime: PARTNER_TOKEN_LOCK_TIME, waitTime: this.partnerTokenLockWait },
|
|
211
|
+
(err, release) => {
|
|
212
|
+
if (err) {
|
|
213
|
+
logger.warn(`Transak partner token lock not acquired: ${err.message || err}`);
|
|
214
|
+
return resolve(null);
|
|
215
|
+
}
|
|
216
|
+
return resolve(release);
|
|
217
|
+
}
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private isUsablePartnerToken(token: TransakPartnerToken | null, staleToken?: string): boolean {
|
|
223
|
+
return !!token?.accessToken
|
|
224
|
+
&& token.accessToken !== staleToken
|
|
225
|
+
&& token.expiresAt - PARTNER_TOKEN_REFRESH_BUFFER > Date.now();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private isUnexpiredPartnerToken(token: TransakPartnerToken | null, staleToken?: string): boolean {
|
|
229
|
+
return !!token?.accessToken
|
|
230
|
+
&& token.accessToken !== staleToken
|
|
231
|
+
&& token.expiresAt - PARTNER_TOKEN_EXPIRY_MARGIN > Date.now();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
private fetchPartnerToken(keys: TransakKeys, userIp: string): Promise<TransakPartnerToken> {
|
|
235
|
+
return new Promise((resolve, reject) => {
|
|
67
236
|
const API = keys.API;
|
|
68
237
|
const API_KEY = keys.API_KEY;
|
|
69
238
|
const SECRET_KEY = keys.SECRET_KEY;
|
|
@@ -86,27 +255,166 @@ export class TransakService {
|
|
|
86
255
|
{
|
|
87
256
|
headers,
|
|
88
257
|
body,
|
|
89
|
-
json: true
|
|
258
|
+
json: true,
|
|
259
|
+
timeout: PARTNER_TOKEN_REQUEST_TIMEOUT
|
|
90
260
|
},
|
|
91
261
|
(err, data) => {
|
|
92
262
|
if (err) {
|
|
93
263
|
logger.error(`Transak request failed with error (${URL}): ${err.stack || err}`);
|
|
94
264
|
return reject(new ClientError('Failed to get Transak access token'));
|
|
95
|
-
} else {
|
|
96
|
-
const success = data.statusCode >= 200 && data.statusCode < 300;
|
|
97
|
-
if (!success) {
|
|
98
|
-
logger.warn(`Transak request failed with status code ${data.statusCode}: ${URL}`);
|
|
99
|
-
}
|
|
100
|
-
if (!data.body) {
|
|
101
|
-
return reject(new ClientError('Transak did not return an access token'));
|
|
102
|
-
}
|
|
103
|
-
return resolve(data.body);
|
|
104
265
|
}
|
|
266
|
+
const success = data.statusCode >= 200 && data.statusCode < 300;
|
|
267
|
+
const accessToken = data.body?.data?.accessToken;
|
|
268
|
+
if (!success || !accessToken || typeof accessToken !== 'string') {
|
|
269
|
+
logger.warn(`Transak request failed with status code ${data.statusCode}: ${URL}${transakErrorDetail(data)}`);
|
|
270
|
+
return reject(new ClientError('Transak did not return an access token'));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Transak returns expiresAt in seconds; accept milliseconds too. If it
|
|
274
|
+
// is missing or already inside the refresh buffer (e.g. a duration
|
|
275
|
+
// instead of a timestamp), use a fallback TTL rather than treating the
|
|
276
|
+
// token as expired and refreshing on every request.
|
|
277
|
+
const now = Date.now();
|
|
278
|
+
const rawExpiresAt = Number(data.body.data.expiresAt);
|
|
279
|
+
let expiresAt = rawExpiresAt < 1e12 ? rawExpiresAt * 1000 : rawExpiresAt;
|
|
280
|
+
if (!Number.isFinite(expiresAt) || expiresAt - PARTNER_TOKEN_REFRESH_BUFFER <= now) {
|
|
281
|
+
logger.warn(`Transak returned an unusable access token expiresAt (${data.body.data.expiresAt}); using fallback TTL`);
|
|
282
|
+
expiresAt = now + PARTNER_TOKEN_FALLBACK_TTL;
|
|
283
|
+
}
|
|
284
|
+
return resolve({ accessToken, expiresAt, fetchedAt: now });
|
|
105
285
|
}
|
|
106
286
|
);
|
|
107
287
|
});
|
|
108
288
|
}
|
|
109
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Returns the cached partner token if usable, or null if a refresh is
|
|
292
|
+
* needed. Throws if a refresh must not happen right now.
|
|
293
|
+
*/
|
|
294
|
+
private async checkPartnerTokenCache(id: string, staleToken?: string): Promise<string | null> {
|
|
295
|
+
const cached = await this.readCachedPartnerToken(id);
|
|
296
|
+
if (this.isUsablePartnerToken(cached, staleToken)) return cached.accessToken;
|
|
297
|
+
|
|
298
|
+
if (staleToken && cached?.accessToken === staleToken
|
|
299
|
+
&& Date.now() - (cached.fetchedAt || 0) < PARTNER_TOKEN_MIN_FORCED_REFRESH_AGE) {
|
|
300
|
+
throw new ClientError('Transak rejected the access token');
|
|
301
|
+
}
|
|
302
|
+
if (await this.recentRefreshFailed(id)) {
|
|
303
|
+
throw new ClientError('Failed to get Transak access token');
|
|
304
|
+
}
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Returns the cached partner access token, refreshing it at most once across
|
|
310
|
+
* all BWS instances when it is missing, near expiry, or equal to staleToken
|
|
311
|
+
* (a token Transak just rejected).
|
|
312
|
+
*/
|
|
313
|
+
private async getPartnerToken(keys: TransakKeys, userIp: string, staleToken?: string): Promise<string> {
|
|
314
|
+
const id = this.partnerTokenId(keys);
|
|
315
|
+
|
|
316
|
+
const cached = await this.readCachedPartnerToken(id);
|
|
317
|
+
if (this.isUsablePartnerToken(cached, staleToken)) return cached.accessToken;
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
return await this.refreshPartnerToken(keys, userIp, id, staleToken);
|
|
321
|
+
} catch (err) {
|
|
322
|
+
// The refresh buffer is proactive. If the refresh can't happen right now
|
|
323
|
+
// (backoff, lock timeout, Transak error), keep serving the current token
|
|
324
|
+
// while Transak would still accept it.
|
|
325
|
+
const current = await this.readCachedPartnerToken(id);
|
|
326
|
+
if (this.isUnexpiredPartnerToken(current, staleToken)) {
|
|
327
|
+
logger.warn(`Transak partner token refresh unavailable; using current token: ${err?.message || err}`);
|
|
328
|
+
return current.accessToken;
|
|
329
|
+
}
|
|
330
|
+
throw err;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private async refreshPartnerToken(keys: TransakKeys, userIp: string, id: string, staleToken?: string): Promise<string> {
|
|
335
|
+
const cached = await this.checkPartnerTokenCache(id, staleToken);
|
|
336
|
+
if (cached) return cached;
|
|
337
|
+
|
|
338
|
+
const release = await this.acquirePartnerTokenLock(id);
|
|
339
|
+
if (!release) {
|
|
340
|
+
// Never refresh without the lock: if the holder is slow, every waiter
|
|
341
|
+
// would time out together and refresh at once.
|
|
342
|
+
const afterWait = await this.readCachedPartnerToken(id);
|
|
343
|
+
if (this.isUsablePartnerToken(afterWait, staleToken)) return afterWait.accessToken;
|
|
344
|
+
throw new ClientError('Failed to get Transak access token');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
// Another instance may have refreshed (or failed) while we waited.
|
|
349
|
+
const recheck = await this.checkPartnerTokenCache(id, staleToken);
|
|
350
|
+
if (recheck) return recheck;
|
|
351
|
+
|
|
352
|
+
let fresh: TransakPartnerToken;
|
|
353
|
+
try {
|
|
354
|
+
fresh = await this.fetchPartnerToken(keys, userIp);
|
|
355
|
+
} catch (err) {
|
|
356
|
+
await this.markRefreshFailed(id);
|
|
357
|
+
throw err;
|
|
358
|
+
}
|
|
359
|
+
await this.storeCachedPartnerToken(id, fresh);
|
|
360
|
+
return fresh.accessToken;
|
|
361
|
+
} finally {
|
|
362
|
+
release();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Calls a Transak endpoint with the partner token. If Transak rejects the
|
|
368
|
+
* token (revoked or expired early), refreshes it once and retries.
|
|
369
|
+
*/
|
|
370
|
+
private async withPartnerToken(
|
|
371
|
+
keys: TransakKeys,
|
|
372
|
+
userIp: string,
|
|
373
|
+
call: (accessToken: string) => Promise<any>
|
|
374
|
+
): Promise<any> {
|
|
375
|
+
const accessToken = await this.getPartnerToken(keys, userIp);
|
|
376
|
+
const data = await call(accessToken);
|
|
377
|
+
if (!PARTNER_TOKEN_REJECTED_STATUSES.includes(data?.statusCode)) return data;
|
|
378
|
+
|
|
379
|
+
logger.warn('Transak rejected the cached partner token; refreshing');
|
|
380
|
+
const freshToken = await this.getPartnerToken(keys, userIp, accessToken);
|
|
381
|
+
return call(freshToken);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* True if address is one of the wallet's addresses in BWS. EVM addresses
|
|
386
|
+
* are matched in any case, since BWS stores them checksummed.
|
|
387
|
+
*/
|
|
388
|
+
private isWalletAddress(walletId: string, address: string): Promise<boolean> {
|
|
389
|
+
if (!walletId || !address || typeof address !== 'string') return Promise.resolve(false);
|
|
390
|
+
const candidates = new Set([address]);
|
|
391
|
+
if (/^0x[0-9a-fA-F]{40}$/.test(address)) {
|
|
392
|
+
candidates.add(address.toLowerCase());
|
|
393
|
+
candidates.add(Web3.utils.toChecksumAddress(address));
|
|
394
|
+
}
|
|
395
|
+
return new Promise(resolve => {
|
|
396
|
+
this.storage.fetchAddressesByWalletId(walletId, [...candidates], (err, addresses) => {
|
|
397
|
+
if (err) {
|
|
398
|
+
logger.warn(`Transak order address lookup failed: ${err.message || err}`);
|
|
399
|
+
return resolve(false);
|
|
400
|
+
}
|
|
401
|
+
return resolve(!!addresses?.length);
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
private sendRequest(method: 'get' | 'post', URL: string, opts: any, errorMessage: string): Promise<any> {
|
|
407
|
+
return new Promise((resolve, reject) => {
|
|
408
|
+
this.request[method](URL, { ...opts, json: true }, (err, data) => {
|
|
409
|
+
if (err) {
|
|
410
|
+
logger.error(`Transak request failed with error (${URL}): ${err.stack || err}`);
|
|
411
|
+
return reject(new ClientError(errorMessage));
|
|
412
|
+
}
|
|
413
|
+
return resolve(data);
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
|
|
110
418
|
transakGetCryptoCurrencies(req): Promise<any> {
|
|
111
419
|
return new Promise((resolve, reject) => {
|
|
112
420
|
let keys;
|
|
@@ -259,7 +567,6 @@ export class TransakService {
|
|
|
259
567
|
transakGetSignedPaymentUrl(req): Promise<{ urlWithSignature: string }> {
|
|
260
568
|
return new Promise(async (resolve, reject) => {
|
|
261
569
|
const appRequiredParams = [
|
|
262
|
-
'accessToken',
|
|
263
570
|
'walletAddress',
|
|
264
571
|
'redirectURL',
|
|
265
572
|
'fiatAmount',
|
|
@@ -270,7 +577,7 @@ export class TransakService {
|
|
|
270
577
|
'partnerCustomerId',
|
|
271
578
|
];
|
|
272
579
|
|
|
273
|
-
const requiredParams = req.body.context === 'web' ? [
|
|
580
|
+
const requiredParams = req.body.context === 'web' ? [] : appRequiredParams;
|
|
274
581
|
const referrerDomain = req.body.referrerDomain ?? req.body.context === 'web' ? 'bitpay.com' : 'bitpay';
|
|
275
582
|
const userIp = this.transakGetUserIp(req);
|
|
276
583
|
let keys;
|
|
@@ -289,13 +596,16 @@ export class TransakService {
|
|
|
289
596
|
const headers = {
|
|
290
597
|
Accept: 'application/json',
|
|
291
598
|
'Content-Type': 'application/json',
|
|
292
|
-
'access-token': req.body.accessToken,
|
|
293
599
|
'x-api-key': API_KEY,
|
|
294
600
|
'x-user-ip': userIp,
|
|
295
601
|
};
|
|
296
602
|
|
|
297
|
-
|
|
298
|
-
|
|
603
|
+
// Build widgetParams from an allowlist. Spreading req.body here used to
|
|
604
|
+
// forward accessToken, env and context into Transak's front-end widget.
|
|
605
|
+
const widgetBody = {};
|
|
606
|
+
for (const param of WIDGET_PARAMS) {
|
|
607
|
+
if (req.body[param] !== undefined) widgetBody[param] = req.body[param];
|
|
608
|
+
}
|
|
299
609
|
const body = {
|
|
300
610
|
widgetParams: {
|
|
301
611
|
...widgetBody,
|
|
@@ -306,27 +616,31 @@ export class TransakService {
|
|
|
306
616
|
|
|
307
617
|
const URL: string = WIDGET_API + '/api/v2/auth/session';
|
|
308
618
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
619
|
+
try {
|
|
620
|
+
const data = await this.withPartnerToken(keys, userIp, accessToken =>
|
|
621
|
+
this.sendRequest(
|
|
622
|
+
'post',
|
|
623
|
+
URL,
|
|
624
|
+
{ headers: { ...headers, 'access-token': accessToken }, body },
|
|
625
|
+
'Failed to get Transak signed payment URL'
|
|
626
|
+
)
|
|
627
|
+
);
|
|
628
|
+
const widgetUrl = data?.body?.data?.widgetUrl ?? data?.data?.widgetUrl;
|
|
629
|
+
const success = data?.statusCode >= 200 && data?.statusCode < 300;
|
|
630
|
+
if (!success || !widgetUrl) {
|
|
631
|
+
logger.warn(`Transak request failed with status code ${data?.statusCode}: ${URL}${transakErrorDetail(data)}`);
|
|
632
|
+
return reject(new ClientError('Failed to get Transak signed payment URL'));
|
|
323
633
|
}
|
|
324
|
-
|
|
634
|
+
return resolve({ urlWithSignature: widgetUrl });
|
|
635
|
+
} catch (err) {
|
|
636
|
+
return reject(err);
|
|
637
|
+
}
|
|
325
638
|
});
|
|
326
639
|
}
|
|
327
640
|
|
|
328
|
-
transakGetOrderDetails(req): Promise<any> {
|
|
641
|
+
transakGetOrderDetails(req, walletId: string): Promise<any> {
|
|
329
642
|
return new Promise(async (resolve, reject) => {
|
|
643
|
+
const userIp = this.transakGetUserIp(req);
|
|
330
644
|
let keys;
|
|
331
645
|
try {
|
|
332
646
|
keys = this.transakGetKeys(req, false);
|
|
@@ -336,41 +650,51 @@ export class TransakService {
|
|
|
336
650
|
const API = keys.API;
|
|
337
651
|
const API_KEY = keys.API_KEY;
|
|
338
652
|
|
|
339
|
-
if (!checkRequired(req.body, ['orderId'
|
|
653
|
+
if (!checkRequired(req.body, ['orderId'])) {
|
|
340
654
|
return reject(new ClientError("Transak's request missing arguments"));
|
|
341
655
|
}
|
|
342
656
|
|
|
657
|
+
// orderId goes into the URL path of a request made with the partner token;
|
|
658
|
+
// reject anything that could change the path (e.g. "../orders").
|
|
659
|
+
const orderId = String(req.body.orderId);
|
|
660
|
+
if (!ORDER_ID_PATTERN.test(orderId)) {
|
|
661
|
+
return reject(new ClientError('Invalid Transak orderId'));
|
|
662
|
+
}
|
|
663
|
+
|
|
343
664
|
const headers = {
|
|
344
665
|
Accept: 'application/json',
|
|
345
|
-
'access-token': req.body.accessToken,
|
|
346
666
|
'x-api-key': API_KEY,
|
|
347
|
-
'x-user-ip':
|
|
667
|
+
'x-user-ip': userIp,
|
|
348
668
|
};
|
|
349
669
|
|
|
350
|
-
const URL: string = API + `/partners/api/v2/order/${
|
|
670
|
+
const URL: string = API + `/partners/api/v2/order/${orderId}`;
|
|
351
671
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
if (!success) {
|
|
365
|
-
logger.warn(`Transak request failed with status code ${data.statusCode}: ${URL}`);
|
|
366
|
-
}
|
|
367
|
-
if (!data.body) {
|
|
368
|
-
return reject(new ClientError('Transak did not return order details'));
|
|
369
|
-
}
|
|
370
|
-
return resolve(data.body);
|
|
371
|
-
}
|
|
672
|
+
try {
|
|
673
|
+
const data = await this.withPartnerToken(keys, userIp, accessToken =>
|
|
674
|
+
this.sendRequest(
|
|
675
|
+
'get',
|
|
676
|
+
URL,
|
|
677
|
+
{ headers: { ...headers, 'access-token': accessToken } },
|
|
678
|
+
'Failed to get Transak order details'
|
|
679
|
+
)
|
|
680
|
+
);
|
|
681
|
+
const success = data.statusCode >= 200 && data.statusCode < 300;
|
|
682
|
+
if (!success) {
|
|
683
|
+
logger.warn(`Transak request failed with status code ${data.statusCode}: ${URL}${transakErrorDetail(data)}`);
|
|
372
684
|
}
|
|
373
|
-
|
|
685
|
+
if (!data.body) {
|
|
686
|
+
return reject(new ClientError('Transak did not return order details'));
|
|
687
|
+
}
|
|
688
|
+
// An order ID alone isn't proof of ownership: only return an order
|
|
689
|
+
// whose destination address belongs to the requesting wallet.
|
|
690
|
+
if (success && !(await this.isWalletAddress(walletId, data.body?.data?.walletAddress))) {
|
|
691
|
+
logger.warn(`Transak order ${orderId} is not for an address of wallet ${walletId}`);
|
|
692
|
+
return reject(new ClientError('Transak order not found'));
|
|
693
|
+
}
|
|
694
|
+
return resolve(data.body);
|
|
695
|
+
} catch (err) {
|
|
696
|
+
return reject(err);
|
|
697
|
+
}
|
|
374
698
|
});
|
|
375
699
|
}
|
|
376
700
|
}
|
|
@@ -48,6 +48,10 @@ const PROVIDERS = {
|
|
|
48
48
|
livenet: 'https://api-eth.bitcore.io',
|
|
49
49
|
sepolia: 'https://api-eth.bitcore.io'
|
|
50
50
|
},
|
|
51
|
+
arc: {
|
|
52
|
+
livenet: 'https://api-eth.bitcore.io',
|
|
53
|
+
testnet: 'https://api-eth.bitcore.io'
|
|
54
|
+
},
|
|
51
55
|
sol: {
|
|
52
56
|
livenet: 'https://api-sol.bitcore.io',
|
|
53
57
|
testnet: 'https://api-sol.bitcore.io',
|