@portal-hq/web 3.17.0 → 3.18.0-alpha.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.
- package/lib/commonjs/index.js +62 -20
- package/lib/commonjs/index.test.js +51 -10
- package/lib/commonjs/integrations/ramps/index.js +2 -0
- package/lib/commonjs/integrations/ramps/meld/index.js +121 -0
- package/lib/commonjs/integrations/ramps/meld/index.test.js +162 -0
- package/lib/commonjs/integrations/ramps/noah/index.js +2 -2
- package/lib/commonjs/integrations/ramps/noah/index.test.js +11 -2
- package/lib/commonjs/mpc/index.js +199 -3
- package/lib/commonjs/mpc/index.test.js +634 -0
- package/lib/commonjs/provider/index.js +44 -2
- package/lib/commonjs/provider/index.test.js +186 -0
- package/lib/commonjs/rpc.test.js +162 -0
- package/lib/commonjs/shared/rpc/auth.js +29 -0
- package/lib/commonjs/shared/rpc/defaults.js +40 -0
- package/lib/commonjs/shared/types/index.js +1 -0
- package/lib/commonjs/shared/types/meld.js +2 -0
- package/lib/esm/index.js +59 -19
- package/lib/esm/index.test.js +51 -10
- package/lib/esm/integrations/ramps/index.js +2 -0
- package/lib/esm/integrations/ramps/meld/index.js +118 -0
- package/lib/esm/integrations/ramps/meld/index.test.js +157 -0
- package/lib/esm/integrations/ramps/noah/index.js +2 -2
- package/lib/esm/integrations/ramps/noah/index.test.js +11 -2
- package/lib/esm/mpc/index.js +199 -3
- package/lib/esm/mpc/index.test.js +635 -1
- package/lib/esm/provider/index.js +44 -2
- package/lib/esm/provider/index.test.js +186 -0
- package/lib/esm/rpc.test.js +157 -0
- package/lib/esm/shared/rpc/auth.js +25 -0
- package/lib/esm/shared/rpc/defaults.js +36 -0
- package/lib/esm/shared/types/index.js +1 -0
- package/lib/esm/shared/types/meld.js +1 -0
- package/noah-types.d.ts +17 -1
- package/package.json +3 -2
- package/src/__mocks/constants.ts +68 -0
- package/src/__mocks/portal/portal.ts +0 -1
- package/src/index.test.ts +90 -0
- package/src/index.ts +159 -7
- package/src/integrations/ramps/index.ts +3 -0
- package/src/integrations/ramps/meld/index.test.ts +189 -0
- package/src/integrations/ramps/meld/index.ts +149 -0
- package/src/integrations/ramps/noah/index.test.ts +11 -2
- package/src/integrations/ramps/noah/index.ts +5 -2
- package/src/mpc/index.test.ts +804 -7
- package/src/mpc/index.ts +247 -3
- package/src/provider/index.test.ts +233 -0
- package/src/provider/index.ts +64 -1
- package/src/rpc.test.ts +190 -0
- package/src/shared/rpc/auth.ts +27 -0
- package/src/shared/rpc/defaults.ts +41 -0
- package/src/shared/types/common.ts +24 -0
- package/src/shared/types/index.ts +1 -0
- package/src/shared/types/meld.ts +302 -0
- package/src/shared/types/noah.ts +191 -29
- 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' }, (
|
|
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(
|
|
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
|