Skip to content

Commit 1dac3b0

Browse files
fix(tron-sdk): parse custom_rpc_header in TronJsonRpcProvider and TronTransactionBuilder (#8558)
Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent eaac4ab commit 1dac3b0

5 files changed

Lines changed: 159 additions & 32 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hyperlane-xyz/tron-sdk": patch
3+
---
4+
5+
TronJsonRpcProvider and TronTransactionBuilder were updated to parse custom_rpc_header query params into HTTP headers, fixing auth with third-party RPC providers like Tatum.

typescript/tron-sdk/src/ethers/TronJsonRpcProvider.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { BigNumber, providers } from 'ethers';
22

33
import { retryAsync } from '@hyperlane-xyz/utils';
44

5+
import { stripCustomRpcHeaders } from './urlUtils.js';
6+
57
const DEFAULT_MAX_RETRIES = 3;
68
const DEFAULT_BASE_RETRY_MS = 250;
79

@@ -31,7 +33,9 @@ export class TronJsonRpcProvider extends providers.StaticJsonRpcProvider {
3133
maxRetries = DEFAULT_MAX_RETRIES,
3234
baseRetryMs = DEFAULT_BASE_RETRY_MS,
3335
) {
34-
super(host, network);
36+
const { url: cleanUrl, headers } = stripCustomRpcHeaders(host);
37+
const hasHeaders = Object.keys(headers).length > 0;
38+
super(hasHeaders ? { url: cleanUrl, headers } : cleanUrl, network);
3539
this.host = host;
3640
this.maxRetries = maxRetries;
3741
this.baseRetryMs = baseRetryMs;

typescript/tron-sdk/src/ethers/TronWallet.ts

Lines changed: 18 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,7 @@ import {
99
TronJsonRpcProvider,
1010
} from './TronJsonRpcProvider.js';
1111
import { TransactionRequest } from '@ethersproject/providers';
12-
13-
/**
14-
* Extract custom_rpc_header query params from a URL into a headers object.
15-
* e.g. "https://api.trongrid.io?custom_rpc_header=TRON-PRO-API-KEY:abc"
16-
* returns { "TRON-PRO-API-KEY": "abc" }
17-
*/
18-
function parseCustomHeaders(url: string): Record<string, string> {
19-
const headers: Record<string, string> = {};
20-
try {
21-
const parsed = new URL(url);
22-
for (const [key, value] of parsed.searchParams) {
23-
if (key !== 'custom_rpc_header') continue;
24-
const colonIdx = value.indexOf(':');
25-
if (colonIdx > 0) {
26-
headers[value.slice(0, colonIdx)] = value.slice(colonIdx + 1);
27-
}
28-
}
29-
} catch {
30-
// Not a valid URL, return empty headers
31-
}
32-
return headers;
33-
}
12+
import { stripCustomRpcHeaders } from './urlUtils.js';
3413

3514
/** Union of possible TronWeb transaction types */
3615
export type TronTransaction =
@@ -80,13 +59,11 @@ export class TronWallet extends Wallet {
8059
// TronWeb needs the base HTTP API URL — strip /jsonrpc path if present, and
8160
// fall back to public TronGrid for third-party providers that only serve JSON-RPC.
8261
// Extract custom headers before stripping path, as they may contain API keys.
83-
const headers = parseCustomHeaders(tronUrl);
84-
const parsed = new URL(tronUrl);
62+
const { url: cleanTronUrl, headers } = stripCustomRpcHeaders(tronUrl);
63+
const parsed = new URL(cleanTronUrl);
8564
if (parsed.pathname.endsWith('/jsonrpc')) {
8665
parsed.pathname = parsed.pathname.slice(0, -8);
8766
}
88-
// Strip custom_rpc_header params from the base URL
89-
parsed.searchParams.delete('custom_rpc_header');
9067
const baseUrl = parsed.toString();
9168
const tronWebUrl =
9269
/^https?:\/\/(localhost|127\.0\.0\.1|[^/]*trongrid)/.test(baseUrl)
@@ -195,14 +172,24 @@ export class TronTransactionBuilder extends TronWeb {
195172
jsonRpcUrl?: string,
196173
headers?: Record<string, string>,
197174
) {
198-
super({ fullHost: tronWebUrl, headers });
175+
// Strip custom_rpc_header from the URL and merge with any provided headers
176+
const { url: cleanTronWebUrl, headers: parsedHeaders } =
177+
stripCustomRpcHeaders(tronWebUrl);
178+
const mergedHeaders = { ...parsedHeaders, ...headers };
179+
super({ fullHost: cleanTronWebUrl, headers: mergedHeaders });
199180

200181
this.tronAddress = tronAddress;
201182
this.setAddress(this.tronAddress);
202-
// Use provided JSON-RPC URL, or derive from TronWeb URL
203-
const rpcUrl =
204-
jsonRpcUrl ??
205-
(tronWebUrl.endsWith('/jsonrpc') ? tronWebUrl : `${tronWebUrl}/jsonrpc`);
183+
// Use provided JSON-RPC URL, or derive from TronWeb URL.
184+
// Use URL API so /jsonrpc goes into the pathname, not after query params.
185+
let rpcUrl = jsonRpcUrl;
186+
if (!rpcUrl) {
187+
const u = new URL(tronWebUrl);
188+
if (!u.pathname.endsWith('/jsonrpc')) {
189+
u.pathname = u.pathname.replace(/\/$/, '') + '/jsonrpc';
190+
}
191+
rpcUrl = u.toString();
192+
}
206193
this.provider = new TronJsonRpcProvider(rpcUrl);
207194
this.tronAddressHex = this.address.toHex(this.tronAddress);
208195
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { expect } from 'chai';
2+
3+
import { parseCustomHeaders, stripCustomRpcHeaders } from './urlUtils.js';
4+
5+
describe('parseCustomHeaders', () => {
6+
it('extracts a single custom_rpc_header', () => {
7+
const url =
8+
'https://api.trongrid.io?custom_rpc_header=TRON-PRO-API-KEY:abc123';
9+
expect(parseCustomHeaders(url)).to.deep.equal({
10+
'TRON-PRO-API-KEY': 'abc123',
11+
});
12+
});
13+
14+
it('extracts multiple custom_rpc_header params', () => {
15+
const url =
16+
'https://host?custom_rpc_header=x-api-key:key1&custom_rpc_header=Authorization:Bearer%20token';
17+
expect(parseCustomHeaders(url)).to.deep.equal({
18+
'x-api-key': 'key1',
19+
Authorization: 'Bearer token',
20+
});
21+
});
22+
23+
it('handles header values with colons', () => {
24+
const url = 'https://host?custom_rpc_header=x-api-key:token:with:colons';
25+
expect(parseCustomHeaders(url)).to.deep.equal({
26+
'x-api-key': 'token:with:colons',
27+
});
28+
});
29+
30+
it('returns empty object for URL without custom_rpc_header', () => {
31+
const url = 'https://api.trongrid.io/jsonrpc';
32+
expect(parseCustomHeaders(url)).to.deep.equal({});
33+
});
34+
35+
it('returns empty object for invalid URL', () => {
36+
expect(parseCustomHeaders('not-a-url')).to.deep.equal({});
37+
});
38+
39+
it('ignores malformed header values without colon', () => {
40+
const url = 'https://host?custom_rpc_header=nocolon';
41+
expect(parseCustomHeaders(url)).to.deep.equal({});
42+
});
43+
});
44+
45+
describe('stripCustomRpcHeaders', () => {
46+
it('strips custom_rpc_header and returns clean URL with headers', () => {
47+
const url =
48+
'https://tron-mainnet.gateway.tatum.io/jsonrpc?custom_rpc_header=x-api-key:abc123';
49+
const result = stripCustomRpcHeaders(url);
50+
expect(result.url).to.equal(
51+
'https://tron-mainnet.gateway.tatum.io/jsonrpc',
52+
);
53+
expect(result.headers).to.deep.equal({ 'x-api-key': 'abc123' });
54+
});
55+
56+
it('preserves non-custom_rpc_header query params', () => {
57+
const url = 'https://host/jsonrpc?custom_rpc_header=x-api-key:abc&other=1';
58+
const result = stripCustomRpcHeaders(url);
59+
expect(result.url).to.equal('https://host/jsonrpc?other=1');
60+
expect(result.headers).to.deep.equal({ 'x-api-key': 'abc' });
61+
});
62+
63+
it('returns original URL unchanged when no custom_rpc_header', () => {
64+
const url = 'https://api.trongrid.io/jsonrpc';
65+
const result = stripCustomRpcHeaders(url);
66+
expect(result.url).to.equal(url);
67+
expect(result.headers).to.deep.equal({});
68+
});
69+
70+
it('returns original URL unchanged when xApiKey query param is used', () => {
71+
const url = 'https://tron-mainnet.gateway.tatum.io/jsonrpc?xApiKey=abc123';
72+
const result = stripCustomRpcHeaders(url);
73+
expect(result.url).to.equal(url);
74+
expect(result.headers).to.deep.equal({});
75+
});
76+
77+
it('strips malformed custom_rpc_header without colon', () => {
78+
const url = 'https://host/jsonrpc?custom_rpc_header=nocolon';
79+
const result = stripCustomRpcHeaders(url);
80+
expect(result.url).to.equal('https://host/jsonrpc');
81+
expect(result.headers).to.deep.equal({});
82+
});
83+
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* Parse custom_rpc_header query params from a URL into a headers object.
3+
* e.g. "https://api.trongrid.io?custom_rpc_header=TRON-PRO-API-KEY:abc"
4+
* returns { "TRON-PRO-API-KEY": "abc" }
5+
*/
6+
export function parseCustomHeaders(url: string): Record<string, string> {
7+
const headers: Record<string, string> = {};
8+
try {
9+
const parsed = new URL(url);
10+
for (const [key, value] of parsed.searchParams) {
11+
if (key !== 'custom_rpc_header') continue;
12+
const colonIdx = value.indexOf(':');
13+
if (colonIdx > 0) {
14+
headers[value.slice(0, colonIdx)] = value.slice(colonIdx + 1);
15+
}
16+
}
17+
} catch {
18+
// Not a valid URL, return empty headers
19+
}
20+
return headers;
21+
}
22+
23+
/**
24+
* Strip custom_rpc_header query params from a URL and return the clean URL
25+
* along with the extracted headers.
26+
*
27+
* e.g. "https://host/jsonrpc?custom_rpc_header=x-api-key:abc&other=1"
28+
* returns { url: "https://host/jsonrpc?other=1", headers: { "x-api-key": "abc" } }
29+
*
30+
* If no custom_rpc_header params are present, returns the original URL unchanged.
31+
*/
32+
export function stripCustomRpcHeaders(url: string): {
33+
url: string;
34+
headers: Record<string, string>;
35+
} {
36+
const headers = parseCustomHeaders(url);
37+
let parsed: URL;
38+
try {
39+
parsed = new URL(url);
40+
} catch {
41+
return { url, headers };
42+
}
43+
if (!parsed.searchParams.has('custom_rpc_header')) {
44+
return { url, headers };
45+
}
46+
parsed.searchParams.delete('custom_rpc_header');
47+
return { url: parsed.toString(), headers };
48+
}

0 commit comments

Comments
 (0)