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