@portal-hq/web 3.17.0 → 3.18.0

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.
Files changed (55) hide show
  1. package/lib/commonjs/index.js +62 -20
  2. package/lib/commonjs/index.test.js +51 -10
  3. package/lib/commonjs/integrations/ramps/index.js +2 -0
  4. package/lib/commonjs/integrations/ramps/meld/index.js +121 -0
  5. package/lib/commonjs/integrations/ramps/meld/index.test.js +162 -0
  6. package/lib/commonjs/integrations/ramps/noah/index.js +2 -2
  7. package/lib/commonjs/integrations/ramps/noah/index.test.js +11 -2
  8. package/lib/commonjs/mpc/index.js +199 -3
  9. package/lib/commonjs/mpc/index.test.js +634 -0
  10. package/lib/commonjs/provider/index.js +44 -2
  11. package/lib/commonjs/provider/index.test.js +186 -0
  12. package/lib/commonjs/rpc.test.js +162 -0
  13. package/lib/commonjs/shared/rpc/auth.js +29 -0
  14. package/lib/commonjs/shared/rpc/defaults.js +40 -0
  15. package/lib/commonjs/shared/types/index.js +1 -0
  16. package/lib/commonjs/shared/types/meld.js +2 -0
  17. package/lib/esm/index.js +59 -19
  18. package/lib/esm/index.test.js +51 -10
  19. package/lib/esm/integrations/ramps/index.js +2 -0
  20. package/lib/esm/integrations/ramps/meld/index.js +118 -0
  21. package/lib/esm/integrations/ramps/meld/index.test.js +157 -0
  22. package/lib/esm/integrations/ramps/noah/index.js +2 -2
  23. package/lib/esm/integrations/ramps/noah/index.test.js +11 -2
  24. package/lib/esm/mpc/index.js +199 -3
  25. package/lib/esm/mpc/index.test.js +635 -1
  26. package/lib/esm/provider/index.js +44 -2
  27. package/lib/esm/provider/index.test.js +186 -0
  28. package/lib/esm/rpc.test.js +157 -0
  29. package/lib/esm/shared/rpc/auth.js +25 -0
  30. package/lib/esm/shared/rpc/defaults.js +36 -0
  31. package/lib/esm/shared/types/index.js +1 -0
  32. package/lib/esm/shared/types/meld.js +1 -0
  33. package/noah-types.d.ts +17 -1
  34. package/package.json +1 -1
  35. package/src/__mocks/constants.ts +68 -0
  36. package/src/__mocks/portal/portal.ts +0 -1
  37. package/src/index.test.ts +90 -0
  38. package/src/index.ts +159 -7
  39. package/src/integrations/ramps/index.ts +3 -0
  40. package/src/integrations/ramps/meld/index.test.ts +189 -0
  41. package/src/integrations/ramps/meld/index.ts +149 -0
  42. package/src/integrations/ramps/noah/index.test.ts +11 -2
  43. package/src/integrations/ramps/noah/index.ts +5 -2
  44. package/src/mpc/index.test.ts +804 -7
  45. package/src/mpc/index.ts +247 -3
  46. package/src/provider/index.test.ts +233 -0
  47. package/src/provider/index.ts +64 -1
  48. package/src/rpc.test.ts +190 -0
  49. package/src/shared/rpc/auth.ts +27 -0
  50. package/src/shared/rpc/defaults.ts +41 -0
  51. package/src/shared/types/common.ts +24 -0
  52. package/src/shared/types/index.ts +1 -0
  53. package/src/shared/types/meld.ts +302 -0
  54. package/src/shared/types/noah.ts +191 -29
  55. package/types.d.ts +65 -3
@@ -13,6 +13,7 @@ exports.RequestMethod = void 0;
13
13
  const errors_1 = require("./errors");
14
14
  const logger_1 = require("../logger");
15
15
  const trace_1 = require("../shared/trace");
16
+ const auth_1 = require("../shared/rpc/auth");
16
17
  var RequestMethod;
17
18
  (function (RequestMethod) {
18
19
  RequestMethod["eth_accounts"] = "eth_accounts";
@@ -134,6 +135,7 @@ var RequestMethod;
134
135
  RequestMethod["sol_signTransaction"] = "sol_signTransaction";
135
136
  // TRON Wallet Methods
136
137
  RequestMethod["tron_sendTransaction"] = "tron_sendTransaction";
138
+ RequestMethod["tron_signTypedData"] = "tron_signTypedData";
137
139
  })(RequestMethod = exports.RequestMethod || (exports.RequestMethod = {}));
138
140
  const passiveSignerMethods = [
139
141
  RequestMethod.eth_accounts,
@@ -156,6 +158,7 @@ const signerMethods = [
156
158
  RequestMethod.sol_signMessage,
157
159
  RequestMethod.sol_signTransaction,
158
160
  RequestMethod.tron_sendTransaction,
161
+ RequestMethod.tron_signTypedData,
159
162
  ];
160
163
  const iframeProxiedMethodStrings = [
161
164
  'eth_getTransactionReceipt',
@@ -203,6 +206,31 @@ class Provider {
203
206
  params = [txParams];
204
207
  }
205
208
  break;
209
+ case RequestMethod.tron_signTypedData: {
210
+ const data = Array.isArray(txParams) ? txParams[0] : txParams;
211
+ let parsed;
212
+ if (typeof data === 'string') {
213
+ try {
214
+ parsed = JSON.parse(data);
215
+ }
216
+ catch (_a) {
217
+ throw new Error('[PortalProvider] tron_signTypedData params must be valid JSON');
218
+ }
219
+ params = data;
220
+ }
221
+ else {
222
+ parsed = data;
223
+ params = JSON.stringify(data, (_key, value) => typeof value === 'bigint' ? value.toString() : value);
224
+ }
225
+ if (typeof parsed !== 'object' ||
226
+ parsed === null ||
227
+ !('domain' in parsed) ||
228
+ !('types' in parsed) ||
229
+ !('value' in parsed)) {
230
+ throw new Error('[PortalProvider] tron_signTypedData params must include "domain", "types", and "value"');
231
+ }
232
+ break;
233
+ }
206
234
  default:
207
235
  if (Array.isArray(txParams)) {
208
236
  params = txParams[0];
@@ -404,6 +432,7 @@ class Provider {
404
432
  * @returns Promise<any>
405
433
  */
406
434
  handleGatewayRequest({ chainId, method, params, traceId, }) {
435
+ var _a, _b;
407
436
  return __awaiter(this, void 0, void 0, function* () {
408
437
  if (iframeProxiedMethodStrings.includes(method)) {
409
438
  logger_1.sdkLogger.info(`[PortalProvider] routing ${method} through iframe (chainId=${String(chainId)})`, { traceId });
@@ -417,6 +446,7 @@ class Provider {
417
446
  chainId: chainId,
418
447
  }, { traceId });
419
448
  }
449
+ const rpcUrl = this.portal.getRpcUrl(chainId);
420
450
  const requestBody = {
421
451
  body: JSON.stringify({
422
452
  jsonrpc: '2.0',
@@ -425,9 +455,11 @@ class Provider {
425
455
  params,
426
456
  }),
427
457
  method: 'POST',
428
- headers: Object.assign({ 'Content-Type': 'application/json' }, (traceId && { [trace_1.X_PORTAL_TRACE_ID_HEADER]: traceId })),
458
+ headers: Object.assign(Object.assign({ 'Content-Type': 'application/json' }, ((0, auth_1.isPortalHostedUrl)(rpcUrl) && ((_a = this.portal.apiKey) !== null && _a !== void 0 ? _a : this.portal.authToken)
459
+ ? { Authorization: `Bearer ${(_b = this.portal.apiKey) !== null && _b !== void 0 ? _b : this.portal.authToken}` }
460
+ : {})), (traceId && { [trace_1.X_PORTAL_TRACE_ID_HEADER]: traceId })),
429
461
  };
430
- const result = yield fetch(this.portal.getRpcUrl(chainId), requestBody);
462
+ const result = yield fetch(rpcUrl, requestBody);
431
463
  return result.json();
432
464
  });
433
465
  }
@@ -489,6 +521,16 @@ class Provider {
489
521
  })));
490
522
  return result;
491
523
  }
524
+ case RequestMethod.tron_signTypedData: {
525
+ this.enforceTronChainId(chainId);
526
+ const result = yield this.portal.mpc.sign(Object.assign({ chainId: chainId, method, params: this.buildParams(method, params),
527
+ // tron_signTypedData is a sign-only method; MPC does not require an RPC client for it
528
+ rpcUrl: '', sponsorGas,
529
+ traceId }, (signatureApprovalMemo !== undefined && {
530
+ signatureApprovalMemo,
531
+ })));
532
+ return result;
533
+ }
492
534
  default:
493
535
  throw new Error('[PortalProvider] Method "' + method + '" not supported');
494
536
  }
@@ -11,6 +11,17 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
11
11
  step((generator = generator.apply(thisArg, _arguments || [])).next());
12
12
  });
13
13
  };
14
+ var __rest = (this && this.__rest) || function (s, e) {
15
+ var t = {};
16
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
17
+ t[p] = s[p];
18
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
19
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
20
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
21
+ t[p[i]] = s[p[i]];
22
+ }
23
+ return t;
24
+ };
14
25
  var __importDefault = (this && this.__importDefault) || function (mod) {
15
26
  return (mod && mod.__esModule) ? mod : { "default": mod };
16
27
  };
@@ -587,6 +598,118 @@ describe('Provider', () => {
587
598
  });
588
599
  }));
589
600
  });
601
+ describe('tron_signTypedData', () => {
602
+ const mockTypedData = {
603
+ domain: { name: 'Permit2', version: '1', chainId: 3448148188 },
604
+ types: {
605
+ TokenPermissions: [
606
+ { name: 'token', type: 'address' },
607
+ { name: 'amount', type: 'uint256' },
608
+ ],
609
+ },
610
+ value: { token: 'TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf', amount: '1000000' },
611
+ };
612
+ it('should throw an error if no chainId is provided alongside tron_signTypedData', () => __awaiter(void 0, void 0, void 0, function* () {
613
+ yield expect(provider.request({
614
+ method: __1.RequestMethod.tron_signTypedData,
615
+ params: [mockTypedData],
616
+ })).rejects.toThrow(new Error('[PortalProvider] Chain ID is required for the operation'));
617
+ }));
618
+ it('should throw an error if malformed chainId is provided alongside tron_signTypedData', () => __awaiter(void 0, void 0, void 0, function* () {
619
+ const chainId = 'unsupported:chain';
620
+ yield expect(provider.request({
621
+ chainId,
622
+ method: __1.RequestMethod.tron_signTypedData,
623
+ params: [mockTypedData],
624
+ })).rejects.toThrow(new Error(`[PortalProvider] Chain ID must be prefixed with "tron:" for the operation, got ${chainId}`));
625
+ }));
626
+ it('should successfully handle a tron_signTypedData request with object params', () => __awaiter(void 0, void 0, void 0, function* () {
627
+ const result = yield provider.request({
628
+ chainId: 'tron:nile',
629
+ method: __1.RequestMethod.tron_signTypedData,
630
+ params: [mockTypedData],
631
+ });
632
+ expect(result).toEqual(constants_1.mockSignedHash);
633
+ expect(portal_1.default.mpc.sign).toHaveBeenCalledWith({
634
+ chainId: 'tron:nile',
635
+ method: __1.RequestMethod.tron_signTypedData,
636
+ params: JSON.stringify(mockTypedData),
637
+ rpcUrl: '',
638
+ sponsorGas: undefined,
639
+ traceId: 'mock-trace-id-12345',
640
+ });
641
+ expect(provider.emit).toHaveBeenCalledWith('portal_signatureReceived', {
642
+ chainId: 'tron:nile',
643
+ method: __1.RequestMethod.tron_signTypedData,
644
+ params: [mockTypedData],
645
+ signature: result,
646
+ });
647
+ }));
648
+ it('should successfully handle a tron_signTypedData request with pre-stringified params', () => __awaiter(void 0, void 0, void 0, function* () {
649
+ const stringifiedData = JSON.stringify(mockTypedData);
650
+ const result = yield provider.request({
651
+ chainId: 'tron:nile',
652
+ method: __1.RequestMethod.tron_signTypedData,
653
+ params: [stringifiedData],
654
+ });
655
+ expect(result).toEqual(constants_1.mockSignedHash);
656
+ expect(portal_1.default.mpc.sign).toHaveBeenCalledWith({
657
+ chainId: 'tron:nile',
658
+ method: __1.RequestMethod.tron_signTypedData,
659
+ params: stringifiedData,
660
+ rpcUrl: '',
661
+ sponsorGas: undefined,
662
+ traceId: 'mock-trace-id-12345',
663
+ });
664
+ }));
665
+ it('should throw if params is an invalid JSON string', () => __awaiter(void 0, void 0, void 0, function* () {
666
+ yield expect(provider.request({
667
+ chainId: 'tron:nile',
668
+ method: __1.RequestMethod.tron_signTypedData,
669
+ params: ['not valid json {'],
670
+ })).rejects.toThrow('[PortalProvider] tron_signTypedData params must be valid JSON');
671
+ }));
672
+ it('should throw if object params is missing "domain"', () => __awaiter(void 0, void 0, void 0, function* () {
673
+ const { domain: _omitted } = mockTypedData, withoutDomain = __rest(mockTypedData, ["domain"]);
674
+ yield expect(provider.request({
675
+ chainId: 'tron:nile',
676
+ method: __1.RequestMethod.tron_signTypedData,
677
+ params: [withoutDomain],
678
+ })).rejects.toThrow('[PortalProvider] tron_signTypedData params must include "domain", "types", and "value"');
679
+ }));
680
+ it('should throw if object params is missing "types"', () => __awaiter(void 0, void 0, void 0, function* () {
681
+ const { types: _omitted } = mockTypedData, withoutTypes = __rest(mockTypedData, ["types"]);
682
+ yield expect(provider.request({
683
+ chainId: 'tron:nile',
684
+ method: __1.RequestMethod.tron_signTypedData,
685
+ params: [withoutTypes],
686
+ })).rejects.toThrow('[PortalProvider] tron_signTypedData params must include "domain", "types", and "value"');
687
+ }));
688
+ it('should throw if object params is missing "value"', () => __awaiter(void 0, void 0, void 0, function* () {
689
+ const { value: _omitted } = mockTypedData, withoutValue = __rest(mockTypedData, ["value"]);
690
+ yield expect(provider.request({
691
+ chainId: 'tron:nile',
692
+ method: __1.RequestMethod.tron_signTypedData,
693
+ params: [withoutValue],
694
+ })).rejects.toThrow('[PortalProvider] tron_signTypedData params must include "domain", "types", and "value"');
695
+ }));
696
+ it('should throw if pre-stringified params is missing "types"', () => __awaiter(void 0, void 0, void 0, function* () {
697
+ const { types: _omitted } = mockTypedData, withoutTypes = __rest(mockTypedData, ["types"]);
698
+ yield expect(provider.request({
699
+ chainId: 'tron:nile',
700
+ method: __1.RequestMethod.tron_signTypedData,
701
+ params: [JSON.stringify(withoutTypes)],
702
+ })).rejects.toThrow('[PortalProvider] tron_signTypedData params must include "domain", "types", and "value"');
703
+ }));
704
+ it('should not require an rpcUrl (sends empty string)', () => __awaiter(void 0, void 0, void 0, function* () {
705
+ yield provider.request({
706
+ chainId: 'tron:nile',
707
+ method: __1.RequestMethod.tron_signTypedData,
708
+ params: [mockTypedData],
709
+ });
710
+ expect(portal_1.default.mpc.sign).toHaveBeenCalledWith(expect.objectContaining({ rpcUrl: '' }));
711
+ }));
712
+ });
590
713
  });
591
714
  describe('Signer methods without auto-approval', () => {
592
715
  const mockSigningRequestedHandler = jest.fn();
@@ -1291,6 +1414,69 @@ describe('Provider', () => {
1291
1414
  expect(mockConsoleWarn).toHaveBeenCalledWith("[PortalProvider] Request for signing method 'tron_sendTransaction' could not be completed because it was not approved by the user.");
1292
1415
  }));
1293
1416
  });
1417
+ describe('tron_signTypedData', () => {
1418
+ const mockTypedData = {
1419
+ domain: { name: 'Permit2', version: '1', chainId: 3448148188 },
1420
+ types: {
1421
+ TokenPermissions: [
1422
+ { name: 'token', type: 'address' },
1423
+ { name: 'amount', type: 'uint256' },
1424
+ ],
1425
+ },
1426
+ value: { token: 'TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf', amount: '1000000' },
1427
+ };
1428
+ it('should successfully handle an approved tron_signTypedData request', () => __awaiter(void 0, void 0, void 0, function* () {
1429
+ provider.on('portal_signingRequested', () => {
1430
+ provider.emit('portal_signingApproved', {
1431
+ method: __1.RequestMethod.tron_signTypedData,
1432
+ params: [mockTypedData],
1433
+ });
1434
+ });
1435
+ const result = yield provider.request({
1436
+ chainId: 'tron:nile',
1437
+ method: __1.RequestMethod.tron_signTypedData,
1438
+ params: [mockTypedData],
1439
+ });
1440
+ expect(mockSigningRequestedHandler).toHaveBeenCalledWith({
1441
+ method: __1.RequestMethod.tron_signTypedData,
1442
+ params: [mockTypedData],
1443
+ });
1444
+ expect(result).toEqual(constants_1.mockSignedHash);
1445
+ expect(portal_1.default.mpc.sign).toHaveBeenCalledWith({
1446
+ chainId: 'tron:nile',
1447
+ method: __1.RequestMethod.tron_signTypedData,
1448
+ params: JSON.stringify(mockTypedData),
1449
+ rpcUrl: '',
1450
+ sponsorGas: undefined,
1451
+ traceId: 'mock-trace-id-12345',
1452
+ });
1453
+ expect(mockSignatureReceivedHandler).toHaveBeenCalledWith({
1454
+ chainId: 'tron:nile',
1455
+ method: __1.RequestMethod.tron_signTypedData,
1456
+ params: [mockTypedData],
1457
+ signature: result,
1458
+ });
1459
+ }));
1460
+ it('should successfully handle a rejected tron_signTypedData request', () => __awaiter(void 0, void 0, void 0, function* () {
1461
+ provider.on('portal_signingRequested', () => {
1462
+ provider.emit('portal_signingRejected', {
1463
+ method: __1.RequestMethod.tron_signTypedData,
1464
+ params: [mockTypedData],
1465
+ });
1466
+ });
1467
+ const result = yield provider.request({
1468
+ chainId: 'tron:nile',
1469
+ method: __1.RequestMethod.tron_signTypedData,
1470
+ params: [mockTypedData],
1471
+ });
1472
+ expect(mockSigningRequestedHandler).toHaveBeenCalledWith({
1473
+ method: __1.RequestMethod.tron_signTypedData,
1474
+ params: [mockTypedData],
1475
+ });
1476
+ expect(result).toEqual(undefined);
1477
+ expect(mockConsoleWarn).toHaveBeenCalledWith("[PortalProvider] Request for signing method 'tron_signTypedData' could not be completed because it was not approved by the user.");
1478
+ }));
1479
+ });
1294
1480
  });
1295
1481
  describe('Non-signer methods', () => {
1296
1482
  beforeAll(() => {
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ /**
3
+ * @jest-environment jsdom
4
+ */
5
+ var __importDefault = (this && this.__importDefault) || function (mod) {
6
+ return (mod && mod.__esModule) ? mod : { "default": mod };
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ const auth_1 = require("./shared/rpc/auth");
10
+ const defaults_1 = require("./shared/rpc/defaults");
11
+ const _1 = __importDefault(require("."));
12
+ // ─── isPortalHostedUrl ───────────────────────────────────────────────────────
13
+ describe('isPortalHostedUrl', () => {
14
+ describe('trusted Portal domains', () => {
15
+ const trusted = [
16
+ 'https://web.portalhq.io/rpc/v1/eip155/1',
17
+ 'https://portalhq.io',
18
+ 'https://staging.portalhq.io/rpc/v1/eip155/1',
19
+ 'https://x.portalhq.io',
20
+ 'https://web.portalhq.dev/rpc/v1/eip155/1',
21
+ 'https://portalhq.dev',
22
+ 'https://sub.portalhq.dev',
23
+ 'https://portalhq-passkey.io',
24
+ 'https://sub.portalhq-passkey.io',
25
+ 'https://portalhq-passkey.dev',
26
+ 'https://sub.portalhq-passkey.dev',
27
+ ];
28
+ for (const url of trusted) {
29
+ it(`returns true for ${url}`, () => {
30
+ expect((0, auth_1.isPortalHostedUrl)(url)).toBe(true);
31
+ });
32
+ }
33
+ });
34
+ describe('untrusted / third-party domains', () => {
35
+ const untrusted = [
36
+ 'https://mainnet.infura.io/v3/KEY',
37
+ 'https://eth-mainnet.alchemyapi.io/v2/KEY',
38
+ 'https://rpc.ankr.com/eth',
39
+ 'https://notportalhq.io/rpc/v1/eip155/1',
40
+ 'https://portalhq.io.evil.com/rpc',
41
+ 'https://evil-portalhq.io/rpc',
42
+ 'https://portalhq.iomalicious.com',
43
+ 'http://localhost:8545',
44
+ 'http://127.0.0.1:8545',
45
+ ];
46
+ for (const url of untrusted) {
47
+ it(`returns false for ${url}`, () => {
48
+ expect((0, auth_1.isPortalHostedUrl)(url)).toBe(false);
49
+ });
50
+ }
51
+ });
52
+ it('returns false for an invalid URL', () => {
53
+ expect((0, auth_1.isPortalHostedUrl)('not-a-url')).toBe(false);
54
+ expect((0, auth_1.isPortalHostedUrl)('')).toBe(false);
55
+ });
56
+ it('handles trailing dot in hostname (valid DNS normalisation)', () => {
57
+ // 'web.portalhq.io.' is technically valid DNS — must still be trusted
58
+ expect((0, auth_1.isPortalHostedUrl)('https://web.portalhq.io./rpc/v1/eip155/1')).toBe(true);
59
+ });
60
+ });
61
+ // ─── buildDefaultRpcConfig ───────────────────────────────────────────────────
62
+ describe('buildDefaultRpcConfig', () => {
63
+ it('returns exactly 13 entries', () => {
64
+ expect(Object.keys((0, defaults_1.buildDefaultRpcConfig)(defaults_1.DEFAULT_RPC_HOST))).toHaveLength(13);
65
+ });
66
+ it('includes all 11 expected EVM chains', () => {
67
+ const cfg = (0, defaults_1.buildDefaultRpcConfig)(defaults_1.DEFAULT_RPC_HOST);
68
+ const evmChains = [
69
+ 'eip155:1',
70
+ 'eip155:11155111',
71
+ 'eip155:137',
72
+ 'eip155:80002',
73
+ 'eip155:8453',
74
+ 'eip155:84532',
75
+ 'eip155:143',
76
+ 'eip155:10143',
77
+ 'eip155:10',
78
+ 'eip155:42161',
79
+ 'eip155:43114',
80
+ ];
81
+ for (const chain of evmChains) {
82
+ expect(cfg).toHaveProperty(chain);
83
+ }
84
+ });
85
+ it('includes both Solana chains', () => {
86
+ const cfg = (0, defaults_1.buildDefaultRpcConfig)(defaults_1.DEFAULT_RPC_HOST);
87
+ expect(cfg).toHaveProperty('solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp');
88
+ expect(cfg).toHaveProperty('solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1');
89
+ });
90
+ it('does NOT include deprecated Polygon Mumbai (eip155:80001)', () => {
91
+ expect((0, defaults_1.buildDefaultRpcConfig)(defaults_1.DEFAULT_RPC_HOST)).not.toHaveProperty('eip155:80001');
92
+ });
93
+ it('every URL follows the template https://{rpcHost}/rpc/v1/{namespace}/{reference}', () => {
94
+ const cfg = (0, defaults_1.buildDefaultRpcConfig)(defaults_1.DEFAULT_RPC_HOST);
95
+ for (const [chainId, url] of Object.entries(cfg)) {
96
+ const [namespace, reference] = chainId.split(':');
97
+ expect(url).toBe(`https://${defaults_1.DEFAULT_RPC_HOST}/rpc/v1/${namespace}/${reference}`);
98
+ }
99
+ });
100
+ it('uses the production host by default', () => {
101
+ const cfg = (0, defaults_1.buildDefaultRpcConfig)();
102
+ expect(cfg['eip155:1']).toBe('https://web.portalhq.io/rpc/v1/eip155/1');
103
+ });
104
+ it('uses a custom rpcHost when provided', () => {
105
+ const cfg = (0, defaults_1.buildDefaultRpcConfig)('web.portalhq.dev');
106
+ for (const url of Object.values(cfg)) {
107
+ expect(url).toContain('https://web.portalhq.dev/rpc/v1/');
108
+ }
109
+ expect(cfg['eip155:1']).toBe('https://web.portalhq.dev/rpc/v1/eip155/1');
110
+ });
111
+ it('does not contain the production host when a custom host is used', () => {
112
+ const cfg = (0, defaults_1.buildDefaultRpcConfig)('web.portalhq.dev');
113
+ for (const url of Object.values(cfg)) {
114
+ expect(url).not.toContain(defaults_1.DEFAULT_RPC_HOST);
115
+ }
116
+ });
117
+ });
118
+ // ─── Portal constructor — rpcConfig resolution ───────────────────────────────
119
+ describe('Portal constructor — rpcConfig resolution', () => {
120
+ it('generates 13 default chains when rpcConfig is omitted', () => {
121
+ const portal = new _1.default({ apiKey: 'test-key' });
122
+ expect(Object.keys(portal.rpcConfig)).toHaveLength(13);
123
+ });
124
+ it('uses web.portalhq.io gateway URLs when rpcConfig is omitted', () => {
125
+ const portal = new _1.default({ apiKey: 'test-key' });
126
+ expect(portal.rpcConfig['eip155:1']).toBe('https://web.portalhq.io/rpc/v1/eip155/1');
127
+ });
128
+ it('treats {} identically to omitting rpcConfig', () => {
129
+ const portalA = new _1.default({ apiKey: 'test-key' });
130
+ const portalB = new _1.default({ apiKey: 'test-key', rpcConfig: {} });
131
+ expect(portalB.rpcConfig).toEqual(portalA.rpcConfig);
132
+ });
133
+ it('uses an explicit non-empty rpcConfig verbatim (no merge with defaults)', () => {
134
+ const custom = { 'eip155:1': 'https://my-node.example.com/eth' };
135
+ const portal = new _1.default({ apiKey: 'test-key', rpcConfig: custom });
136
+ expect(portal.rpcConfig).toEqual(custom);
137
+ expect(Object.keys(portal.rpcConfig)).toHaveLength(1);
138
+ });
139
+ it('does not inject defaults alongside an explicit config', () => {
140
+ const custom = { 'eip155:1': 'https://my-node.example.com/eth' };
141
+ const portal = new _1.default({ apiKey: 'test-key', rpcConfig: custom });
142
+ expect(portal.rpcConfig['eip155:137']).toBeUndefined();
143
+ });
144
+ it('uses a custom gatewayHost in the generated default URLs', () => {
145
+ const portal = new _1.default({ apiKey: 'test-key', gatewayHost: 'web.portalhq.dev' });
146
+ expect(portal.rpcConfig['eip155:1']).toBe('https://web.portalhq.dev/rpc/v1/eip155/1');
147
+ });
148
+ it('all default URLs contain the custom gatewayHost', () => {
149
+ const portal = new _1.default({ apiKey: 'test-key', gatewayHost: 'web.portalhq.dev' });
150
+ for (const url of Object.values(portal.rpcConfig)) {
151
+ expect(url).toContain('https://web.portalhq.dev/rpc/v1/');
152
+ }
153
+ });
154
+ it('getRpcUrl returns the default URL for a standard chain', () => {
155
+ const portal = new _1.default({ apiKey: 'test-key' });
156
+ expect(portal.getRpcUrl('eip155:1')).toBe('https://web.portalhq.io/rpc/v1/eip155/1');
157
+ });
158
+ it('getRpcUrl throws for a chain not in the config', () => {
159
+ const portal = new _1.default({ apiKey: 'test-key' });
160
+ expect(() => portal.getRpcUrl('eip155:99999')).toThrow();
161
+ });
162
+ });
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isPortalHostedUrl = void 0;
4
+ const PORTAL_TRUSTED_DOMAINS = [
5
+ 'portalhq.io',
6
+ 'portalhq.dev',
7
+ 'portalhq-passkey.io',
8
+ 'portalhq-passkey.dev',
9
+ ];
10
+ /**
11
+ * Returns true iff the URL's hostname is owned by Portal.
12
+ *
13
+ * Guards Authorization header injection — prevents credential leakage to
14
+ * third-party RPC providers (Infura, Alchemy, etc.).
15
+ *
16
+ * Localhost trust is intentionally excluded: that is an iframe-only
17
+ * concern (dev CORS proxies) and is handled separately in the iframe.
18
+ */
19
+ function isPortalHostedUrl(url) {
20
+ try {
21
+ // Normalise: strip trailing dots from hostname (valid DNS but unusual)
22
+ const hostname = new URL(url).hostname.toLowerCase().replace(/\.$/, '');
23
+ return PORTAL_TRUSTED_DOMAINS.some((domain) => hostname === domain || hostname.endsWith(`.${domain}`));
24
+ }
25
+ catch (_a) {
26
+ return false;
27
+ }
28
+ }
29
+ exports.isPortalHostedUrl = isPortalHostedUrl;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildDefaultRpcConfig = exports.DEFAULT_RPC_HOST = void 0;
4
+ /**
5
+ * Production Portal RPC gateway hostname.
6
+ * Staging equivalent: `web.portalhq.dev`.
7
+ * Used as the fallback when neither `gatewayHost` nor `host` is supplied.
8
+ */
9
+ exports.DEFAULT_RPC_HOST = 'web.portalhq.io';
10
+ /**
11
+ * Builds the default Portal-managed RPC gateway config from a given gateway host.
12
+ *
13
+ * URL template: https://{rpcHost}/rpc/v1/{namespace}/{reference}
14
+ *
15
+ * Covers 13 chains — 11 EVM + 2 Solana. Polygon Amoy (eip155:80002) is used;
16
+ * the deprecated Mumbai (eip155:80001) is intentionally excluded.
17
+ *
18
+ * When rpcConfig is undefined or {} in the Portal constructor, this function is
19
+ * called automatically. An explicit non-empty rpcConfig always takes precedence
20
+ * verbatim — no merging with defaults.
21
+ */
22
+ function buildDefaultRpcConfig(rpcHost = exports.DEFAULT_RPC_HOST) {
23
+ const base = `https://${rpcHost}/rpc/v1`;
24
+ return {
25
+ 'eip155:1': `${base}/eip155/1`,
26
+ 'eip155:11155111': `${base}/eip155/11155111`,
27
+ 'eip155:137': `${base}/eip155/137`,
28
+ 'eip155:80002': `${base}/eip155/80002`,
29
+ 'eip155:8453': `${base}/eip155/8453`,
30
+ 'eip155:84532': `${base}/eip155/84532`,
31
+ 'eip155:143': `${base}/eip155/143`,
32
+ 'eip155:10143': `${base}/eip155/10143`,
33
+ 'eip155:10': `${base}/eip155/10`,
34
+ 'eip155:42161': `${base}/eip155/42161`,
35
+ 'eip155:43114': `${base}/eip155/43114`,
36
+ 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': `${base}/solana/5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp`,
37
+ 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1': `${base}/solana/EtWTRABZaYq6iMfeYKouRu166VU2xqa1`,
38
+ };
39
+ }
40
+ exports.buildDefaultRpcConfig = buildDefaultRpcConfig;
@@ -30,6 +30,7 @@ __exportStar(require("./yieldxyz"), exports);
30
30
  __exportStar(require("./zero-x"), exports);
31
31
  __exportStar(require("./lifi"), exports);
32
32
  __exportStar(require("./noah"), exports);
33
+ __exportStar(require("./meld"), exports);
33
34
  __exportStar(require("./hypernative"), exports);
34
35
  __exportStar(require("./blockaid"), exports);
35
36
  // Account Abstraction types
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });