@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.
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 +3 -2
  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
package/src/mpc/index.ts CHANGED
@@ -44,6 +44,27 @@ import type {
44
44
  import type {
45
45
  GetTransactionHistoryParams,
46
46
  GetTransactionHistoryResponse,
47
+ NoahGetPaymentMethodsRequest,
48
+ MeldCreateCustomerRequest,
49
+ MeldCreateCustomerResponse,
50
+ MeldSearchCustomerResponse,
51
+ MeldGetRetailQuoteRequest,
52
+ MeldGetRetailQuoteResponse,
53
+ MeldCreateRetailWidgetRequest,
54
+ MeldCreateRetailWidgetResponse,
55
+ MeldSearchRetailTransactionsParams,
56
+ MeldSearchRetailTransactionsResponse,
57
+ MeldGetRetailTransactionResponse,
58
+ MeldDiscoveryParams,
59
+ MeldGetServiceProvidersResponse,
60
+ MeldGetCountriesResponse,
61
+ MeldGetFiatCurrenciesResponse,
62
+ MeldGetCryptoCurrenciesResponse,
63
+ MeldGetPaymentMethodsResponse,
64
+ MeldGetDefaultsResponse,
65
+ MeldGetBuyLimitsResponse,
66
+ MeldGetSellLimitsResponse,
67
+ MeldGetKycLimitsResponse,
47
68
  } from '../shared/types'
48
69
  import {
49
70
  YieldXyzEnterRequest,
@@ -138,7 +159,7 @@ import {
138
159
  } from '../../hypernative'
139
160
  import { generateTraceId } from '../shared/trace'
140
161
 
141
- const WEB_SDK_VERSION = '3.17.0'
162
+ const WEB_SDK_VERSION = '3.18.0-alpha.0'
142
163
 
143
164
  class Mpc {
144
165
  public iframe?: HTMLIFrameElement
@@ -365,6 +386,8 @@ class Mpc {
365
386
  param: string,
366
387
  options?: RawSignOptions,
367
388
  ): Promise<string> {
389
+ const traceId = options?.traceId ?? generateTraceId()
390
+ sdkLogger.debug(`[Portal MPC] rawSign started | traceId=${traceId}`)
368
391
  return this.handleRequestToIframeAndPost({
369
392
  methodMessage: 'portal:mpc:rawSign',
370
393
  errorMessage: 'portal:mpc:rawSignError',
@@ -376,7 +399,7 @@ class Mpc {
376
399
  signatureApprovalMemo: options.signatureApprovalMemo,
377
400
  }),
378
401
  },
379
- traceId: options?.traceId,
402
+ traceId,
380
403
  })
381
404
  }
382
405
 
@@ -906,15 +929,191 @@ class Mpc {
906
929
  })
907
930
  }
908
931
 
909
- public async getPaymentMethods(): Promise<NoahGetPaymentMethodsResponse> {
932
+ public async getPaymentMethods(
933
+ data?: NoahGetPaymentMethodsRequest,
934
+ ): Promise<NoahGetPaymentMethodsResponse> {
910
935
  return this.handleRequestToIframeAndPost({
911
936
  methodMessage: 'portal:noah:getPaymentMethods',
912
937
  errorMessage: 'portal:noah:getPaymentMethodsError',
913
938
  resultMessage: 'portal:noah:getPaymentMethodsResult',
939
+ data,
940
+ })
941
+ }
942
+
943
+ public async meldCreateCustomer(
944
+ data: MeldCreateCustomerRequest,
945
+ ): Promise<MeldCreateCustomerResponse> {
946
+ return this.handleRequestToIframeAndPost({
947
+ methodMessage: 'portal:meld:createCustomer',
948
+ errorMessage: 'portal:meld:createCustomerError',
949
+ resultMessage: 'portal:meld:createCustomerResult',
950
+ data,
951
+ })
952
+ }
953
+
954
+ public async meldSearchCustomer(): Promise<MeldSearchCustomerResponse> {
955
+ return this.handleRequestToIframeAndPost({
956
+ methodMessage: 'portal:meld:searchCustomer',
957
+ errorMessage: 'portal:meld:searchCustomerError',
958
+ resultMessage: 'portal:meld:searchCustomerResult',
914
959
  data: {},
915
960
  })
916
961
  }
917
962
 
963
+ public async meldGetRetailQuote(
964
+ data: MeldGetRetailQuoteRequest,
965
+ ): Promise<MeldGetRetailQuoteResponse> {
966
+ return this.handleRequestToIframeAndPost({
967
+ methodMessage: 'portal:meld:getRetailQuote',
968
+ errorMessage: 'portal:meld:getRetailQuoteError',
969
+ resultMessage: 'portal:meld:getRetailQuoteResult',
970
+ data,
971
+ })
972
+ }
973
+
974
+ public async meldCreateRetailWidget(
975
+ data: MeldCreateRetailWidgetRequest,
976
+ ): Promise<MeldCreateRetailWidgetResponse> {
977
+ return this.handleRequestToIframeAndPost({
978
+ methodMessage: 'portal:meld:createRetailWidget',
979
+ errorMessage: 'portal:meld:createRetailWidgetError',
980
+ resultMessage: 'portal:meld:createRetailWidgetResult',
981
+ data,
982
+ })
983
+ }
984
+
985
+ public async meldSearchRetailTransactions(
986
+ data?: MeldSearchRetailTransactionsParams,
987
+ ): Promise<MeldSearchRetailTransactionsResponse> {
988
+ return this.handleRequestToIframeAndPost({
989
+ methodMessage: 'portal:meld:searchRetailTransactions',
990
+ errorMessage: 'portal:meld:searchRetailTransactionsError',
991
+ resultMessage: 'portal:meld:searchRetailTransactionsResult',
992
+ data,
993
+ })
994
+ }
995
+
996
+ public async meldGetRetailTransactionBySession(
997
+ sessionId: string,
998
+ ): Promise<MeldGetRetailTransactionResponse> {
999
+ return this.handleRequestToIframeAndPost({
1000
+ methodMessage: 'portal:meld:getRetailTransactionBySession',
1001
+ errorMessage: 'portal:meld:getRetailTransactionBySessionError',
1002
+ resultMessage: 'portal:meld:getRetailTransactionBySessionResult',
1003
+ data: sessionId,
1004
+ })
1005
+ }
1006
+
1007
+ public async meldGetRetailTransaction(
1008
+ id: string,
1009
+ ): Promise<MeldGetRetailTransactionResponse> {
1010
+ return this.handleRequestToIframeAndPost({
1011
+ methodMessage: 'portal:meld:getRetailTransaction',
1012
+ errorMessage: 'portal:meld:getRetailTransactionError',
1013
+ resultMessage: 'portal:meld:getRetailTransactionResult',
1014
+ data: id,
1015
+ })
1016
+ }
1017
+
1018
+ public async meldGetServiceProviders(
1019
+ data?: MeldDiscoveryParams,
1020
+ ): Promise<MeldGetServiceProvidersResponse> {
1021
+ return this.handleRequestToIframeAndPost({
1022
+ methodMessage: 'portal:meld:getServiceProviders',
1023
+ errorMessage: 'portal:meld:getServiceProvidersError',
1024
+ resultMessage: 'portal:meld:getServiceProvidersResult',
1025
+ data: data ?? {},
1026
+ })
1027
+ }
1028
+
1029
+ public async meldGetCountries(
1030
+ data?: MeldDiscoveryParams,
1031
+ ): Promise<MeldGetCountriesResponse> {
1032
+ return this.handleRequestToIframeAndPost({
1033
+ methodMessage: 'portal:meld:getCountries',
1034
+ errorMessage: 'portal:meld:getCountriesError',
1035
+ resultMessage: 'portal:meld:getCountriesResult',
1036
+ data: data ?? {},
1037
+ })
1038
+ }
1039
+
1040
+ public async meldGetFiatCurrencies(
1041
+ data?: MeldDiscoveryParams,
1042
+ ): Promise<MeldGetFiatCurrenciesResponse> {
1043
+ return this.handleRequestToIframeAndPost({
1044
+ methodMessage: 'portal:meld:getFiatCurrencies',
1045
+ errorMessage: 'portal:meld:getFiatCurrenciesError',
1046
+ resultMessage: 'portal:meld:getFiatCurrenciesResult',
1047
+ data: data ?? {},
1048
+ })
1049
+ }
1050
+
1051
+ public async meldGetCryptoCurrencies(
1052
+ data?: MeldDiscoveryParams,
1053
+ ): Promise<MeldGetCryptoCurrenciesResponse> {
1054
+ return this.handleRequestToIframeAndPost({
1055
+ methodMessage: 'portal:meld:getCryptoCurrencies',
1056
+ errorMessage: 'portal:meld:getCryptoCurrenciesError',
1057
+ resultMessage: 'portal:meld:getCryptoCurrenciesResult',
1058
+ data: data ?? {},
1059
+ })
1060
+ }
1061
+
1062
+ public async meldGetPaymentMethods(
1063
+ data?: MeldDiscoveryParams,
1064
+ ): Promise<MeldGetPaymentMethodsResponse> {
1065
+ return this.handleRequestToIframeAndPost({
1066
+ methodMessage: 'portal:meld:getPaymentMethods',
1067
+ errorMessage: 'portal:meld:getPaymentMethodsError',
1068
+ resultMessage: 'portal:meld:getPaymentMethodsResult',
1069
+ data: data ?? {},
1070
+ })
1071
+ }
1072
+
1073
+ public async meldGetDefaults(
1074
+ data?: MeldDiscoveryParams,
1075
+ ): Promise<MeldGetDefaultsResponse> {
1076
+ return this.handleRequestToIframeAndPost({
1077
+ methodMessage: 'portal:meld:getDefaults',
1078
+ errorMessage: 'portal:meld:getDefaultsError',
1079
+ resultMessage: 'portal:meld:getDefaultsResult',
1080
+ data: data ?? {},
1081
+ })
1082
+ }
1083
+
1084
+ public async meldGetBuyLimits(
1085
+ data?: MeldDiscoveryParams,
1086
+ ): Promise<MeldGetBuyLimitsResponse> {
1087
+ return this.handleRequestToIframeAndPost({
1088
+ methodMessage: 'portal:meld:getBuyLimits',
1089
+ errorMessage: 'portal:meld:getBuyLimitsError',
1090
+ resultMessage: 'portal:meld:getBuyLimitsResult',
1091
+ data: data ?? {},
1092
+ })
1093
+ }
1094
+
1095
+ public async meldGetSellLimits(
1096
+ data?: MeldDiscoveryParams,
1097
+ ): Promise<MeldGetSellLimitsResponse> {
1098
+ return this.handleRequestToIframeAndPost({
1099
+ methodMessage: 'portal:meld:getSellLimits',
1100
+ errorMessage: 'portal:meld:getSellLimitsError',
1101
+ resultMessage: 'portal:meld:getSellLimitsResult',
1102
+ data: data ?? {},
1103
+ })
1104
+ }
1105
+
1106
+ public async meldGetKycLimits(
1107
+ data?: MeldDiscoveryParams,
1108
+ ): Promise<MeldGetKycLimitsResponse> {
1109
+ return this.handleRequestToIframeAndPost({
1110
+ methodMessage: 'portal:meld:getKycLimits',
1111
+ errorMessage: 'portal:meld:getKycLimitsError',
1112
+ resultMessage: 'portal:meld:getKycLimitsResult',
1113
+ data: data ?? {},
1114
+ })
1115
+ }
1116
+
918
1117
  public async getSwapsQuoteV2(
919
1118
  data: ZeroExQuoteRequest,
920
1119
  options?: ZeroExOptions,
@@ -1203,6 +1402,16 @@ class Mpc {
1203
1402
  traceId?: string
1204
1403
  }): Promise<ResponseType> {
1205
1404
  // Single traceId per operation: use provided or generate. Propagates to iframe and all internal REST/MPC calls.
1405
+ if (
1406
+ !providedTraceId &&
1407
+ (methodMessage.startsWith('portal:wasm:') ||
1408
+ methodMessage.startsWith('portal:mpc:') ||
1409
+ methodMessage.startsWith('portal:accountAbstraction:'))
1410
+ ) {
1411
+ sdkLogger.debug(
1412
+ `[Portal] No traceId propagated to handleRequestToIframeAndPost for method=${methodMessage}; generating a new one.`,
1413
+ )
1414
+ }
1206
1415
  const traceId = providedTraceId ?? generateTraceId()
1207
1416
  sdkLogger.debug(
1208
1417
  '[Portal] request traceId:',
@@ -1490,6 +1699,41 @@ class Mpc {
1490
1699
  const address = await this.getAddress()
1491
1700
  this.portal.address = address
1492
1701
 
1702
+ // Detect if wallet shares were cleared from storage (e.g., Safari ITP).
1703
+ // Must be awaited before triggerReady so the event fires before app callbacks run.
1704
+ try {
1705
+ const [client, shares] = await Promise.all([
1706
+ this.getClient(),
1707
+ this.checkSharesOnDevice(),
1708
+ ])
1709
+ const completedWallets = client.wallets.filter((w) =>
1710
+ w.signingSharePairs.some((sp) => sp.status === 'completed'),
1711
+ )
1712
+ const requiredCurves = completedWallets.map((w) => w.curve)
1713
+ const walletOnDevice =
1714
+ requiredCurves.length > 0 &&
1715
+ requiredCurves.every(
1716
+ (curve) => !!shares[curve as keyof SharesOnDeviceResponse],
1717
+ )
1718
+ // No completed wallets means the user is in initial setup — no prior
1719
+ // device state to have been lost.
1720
+ const isInitialSetup = completedWallets.length === 0
1721
+ const shouldEmitWalletNotOnDevice =
1722
+ !isInitialSetup && !walletOnDevice && !client.ejectedAt
1723
+ if (shouldEmitWalletNotOnDevice) {
1724
+ const isBackedUp = client.wallets.some((w) =>
1725
+ w.backupSharePairs.some((bp) => bp.status === 'completed'),
1726
+ )
1727
+ this.portal.triggerWalletNotOnDevice({
1728
+ clientId: client.id,
1729
+ isBackedUp,
1730
+ reason: 'storage_cleared',
1731
+ })
1732
+ }
1733
+ } catch (detectionError) {
1734
+ sdkLogger.warn('[Portal] walletNotOnDevice detection failed (initialization continues):', detectionError)
1735
+ }
1736
+
1493
1737
  // Trigger the ready callback
1494
1738
  this.portal.triggerReady()
1495
1739
  }
@@ -869,6 +869,169 @@ describe('Provider', () => {
869
869
  )
870
870
  })
871
871
  })
872
+
873
+ describe('tron_signTypedData', () => {
874
+ const mockTypedData = {
875
+ domain: { name: 'Permit2', version: '1', chainId: 3448148188 },
876
+ types: {
877
+ TokenPermissions: [
878
+ { name: 'token', type: 'address' },
879
+ { name: 'amount', type: 'uint256' },
880
+ ],
881
+ },
882
+ value: { token: 'TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf', amount: '1000000' },
883
+ }
884
+
885
+ it('should throw an error if no chainId is provided alongside tron_signTypedData', async () => {
886
+ await expect(
887
+ provider.request({
888
+ method: RequestMethod.tron_signTypedData,
889
+ params: [mockTypedData],
890
+ }),
891
+ ).rejects.toThrow(
892
+ new Error(
893
+ '[PortalProvider] Chain ID is required for the operation',
894
+ ),
895
+ )
896
+ })
897
+
898
+ it('should throw an error if malformed chainId is provided alongside tron_signTypedData', async () => {
899
+ const chainId = 'unsupported:chain'
900
+ await expect(
901
+ provider.request({
902
+ chainId,
903
+ method: RequestMethod.tron_signTypedData,
904
+ params: [mockTypedData],
905
+ }),
906
+ ).rejects.toThrow(
907
+ new Error(
908
+ `[PortalProvider] Chain ID must be prefixed with "tron:" for the operation, got ${chainId}`,
909
+ ),
910
+ )
911
+ })
912
+
913
+ it('should successfully handle a tron_signTypedData request with object params', async () => {
914
+ const result = await provider.request({
915
+ chainId: 'tron:nile',
916
+ method: RequestMethod.tron_signTypedData,
917
+ params: [mockTypedData],
918
+ })
919
+
920
+ expect(result).toEqual(mockSignedHash)
921
+ expect(portal.mpc.sign).toHaveBeenCalledWith({
922
+ chainId: 'tron:nile',
923
+ method: RequestMethod.tron_signTypedData,
924
+ params: JSON.stringify(mockTypedData),
925
+ rpcUrl: '',
926
+ sponsorGas: undefined,
927
+ traceId: 'mock-trace-id-12345',
928
+ })
929
+ expect(provider.emit).toHaveBeenCalledWith(
930
+ 'portal_signatureReceived',
931
+ {
932
+ chainId: 'tron:nile',
933
+ method: RequestMethod.tron_signTypedData,
934
+ params: [mockTypedData],
935
+ signature: result,
936
+ },
937
+ )
938
+ })
939
+
940
+ it('should successfully handle a tron_signTypedData request with pre-stringified params', async () => {
941
+ const stringifiedData = JSON.stringify(mockTypedData)
942
+ const result = await provider.request({
943
+ chainId: 'tron:nile',
944
+ method: RequestMethod.tron_signTypedData,
945
+ params: [stringifiedData],
946
+ })
947
+
948
+ expect(result).toEqual(mockSignedHash)
949
+ expect(portal.mpc.sign).toHaveBeenCalledWith({
950
+ chainId: 'tron:nile',
951
+ method: RequestMethod.tron_signTypedData,
952
+ params: stringifiedData,
953
+ rpcUrl: '',
954
+ sponsorGas: undefined,
955
+ traceId: 'mock-trace-id-12345',
956
+ })
957
+ })
958
+
959
+ it('should throw if params is an invalid JSON string', async () => {
960
+ await expect(
961
+ provider.request({
962
+ chainId: 'tron:nile',
963
+ method: RequestMethod.tron_signTypedData,
964
+ params: ['not valid json {'],
965
+ }),
966
+ ).rejects.toThrow(
967
+ '[PortalProvider] tron_signTypedData params must be valid JSON',
968
+ )
969
+ })
970
+
971
+ it('should throw if object params is missing "domain"', async () => {
972
+ const { domain: _omitted, ...withoutDomain } = mockTypedData
973
+ await expect(
974
+ provider.request({
975
+ chainId: 'tron:nile',
976
+ method: RequestMethod.tron_signTypedData,
977
+ params: [withoutDomain],
978
+ }),
979
+ ).rejects.toThrow(
980
+ '[PortalProvider] tron_signTypedData params must include "domain", "types", and "value"',
981
+ )
982
+ })
983
+
984
+ it('should throw if object params is missing "types"', async () => {
985
+ const { types: _omitted, ...withoutTypes } = mockTypedData
986
+ await expect(
987
+ provider.request({
988
+ chainId: 'tron:nile',
989
+ method: RequestMethod.tron_signTypedData,
990
+ params: [withoutTypes],
991
+ }),
992
+ ).rejects.toThrow(
993
+ '[PortalProvider] tron_signTypedData params must include "domain", "types", and "value"',
994
+ )
995
+ })
996
+
997
+ it('should throw if object params is missing "value"', async () => {
998
+ const { value: _omitted, ...withoutValue } = mockTypedData
999
+ await expect(
1000
+ provider.request({
1001
+ chainId: 'tron:nile',
1002
+ method: RequestMethod.tron_signTypedData,
1003
+ params: [withoutValue],
1004
+ }),
1005
+ ).rejects.toThrow(
1006
+ '[PortalProvider] tron_signTypedData params must include "domain", "types", and "value"',
1007
+ )
1008
+ })
1009
+
1010
+ it('should throw if pre-stringified params is missing "types"', async () => {
1011
+ const { types: _omitted, ...withoutTypes } = mockTypedData
1012
+ await expect(
1013
+ provider.request({
1014
+ chainId: 'tron:nile',
1015
+ method: RequestMethod.tron_signTypedData,
1016
+ params: [JSON.stringify(withoutTypes)],
1017
+ }),
1018
+ ).rejects.toThrow(
1019
+ '[PortalProvider] tron_signTypedData params must include "domain", "types", and "value"',
1020
+ )
1021
+ })
1022
+
1023
+ it('should not require an rpcUrl (sends empty string)', async () => {
1024
+ await provider.request({
1025
+ chainId: 'tron:nile',
1026
+ method: RequestMethod.tron_signTypedData,
1027
+ params: [mockTypedData],
1028
+ })
1029
+
1030
+ expect(portal.mpc.sign).toHaveBeenCalledWith(
1031
+ expect.objectContaining({ rpcUrl: '' }),
1032
+ )
1033
+ })
1034
+ })
872
1035
  })
873
1036
 
874
1037
  describe('Signer methods without auto-approval', () => {
@@ -1655,6 +1818,76 @@ describe('Provider', () => {
1655
1818
  )
1656
1819
  })
1657
1820
  })
1821
+
1822
+ describe('tron_signTypedData', () => {
1823
+ const mockTypedData = {
1824
+ domain: { name: 'Permit2', version: '1', chainId: 3448148188 },
1825
+ types: {
1826
+ TokenPermissions: [
1827
+ { name: 'token', type: 'address' },
1828
+ { name: 'amount', type: 'uint256' },
1829
+ ],
1830
+ },
1831
+ value: { token: 'TXYZopYRdj2D9XRtbG411XZZ3kM5VkAeBf', amount: '1000000' },
1832
+ }
1833
+
1834
+ it('should successfully handle an approved tron_signTypedData request', async () => {
1835
+ provider.on('portal_signingRequested', () => {
1836
+ provider.emit('portal_signingApproved', {
1837
+ method: RequestMethod.tron_signTypedData,
1838
+ params: [mockTypedData],
1839
+ })
1840
+ })
1841
+ const result = await provider.request({
1842
+ chainId: 'tron:nile',
1843
+ method: RequestMethod.tron_signTypedData,
1844
+ params: [mockTypedData],
1845
+ })
1846
+
1847
+ expect(mockSigningRequestedHandler).toHaveBeenCalledWith({
1848
+ method: RequestMethod.tron_signTypedData,
1849
+ params: [mockTypedData],
1850
+ })
1851
+ expect(result).toEqual(mockSignedHash)
1852
+ expect(portal.mpc.sign).toHaveBeenCalledWith({
1853
+ chainId: 'tron:nile',
1854
+ method: RequestMethod.tron_signTypedData,
1855
+ params: JSON.stringify(mockTypedData),
1856
+ rpcUrl: '',
1857
+ sponsorGas: undefined,
1858
+ traceId: 'mock-trace-id-12345',
1859
+ })
1860
+ expect(mockSignatureReceivedHandler).toHaveBeenCalledWith({
1861
+ chainId: 'tron:nile',
1862
+ method: RequestMethod.tron_signTypedData,
1863
+ params: [mockTypedData],
1864
+ signature: result,
1865
+ })
1866
+ })
1867
+
1868
+ it('should successfully handle a rejected tron_signTypedData request', async () => {
1869
+ provider.on('portal_signingRequested', () => {
1870
+ provider.emit('portal_signingRejected', {
1871
+ method: RequestMethod.tron_signTypedData,
1872
+ params: [mockTypedData],
1873
+ })
1874
+ })
1875
+
1876
+ const result = await provider.request({
1877
+ chainId: 'tron:nile',
1878
+ method: RequestMethod.tron_signTypedData,
1879
+ params: [mockTypedData],
1880
+ })
1881
+ expect(mockSigningRequestedHandler).toHaveBeenCalledWith({
1882
+ method: RequestMethod.tron_signTypedData,
1883
+ params: [mockTypedData],
1884
+ })
1885
+ expect(result).toEqual(undefined)
1886
+ expect(mockConsoleWarn).toHaveBeenCalledWith(
1887
+ "[PortalProvider] Request for signing method 'tron_signTypedData' could not be completed because it was not approved by the user.",
1888
+ )
1889
+ })
1890
+ })
1658
1891
  })
1659
1892
 
1660
1893
  describe('Non-signer methods', () => {
@@ -3,6 +3,7 @@ import { ProviderRpcError, RpcErrorCodes } from './errors'
3
3
  import Portal from '../index'
4
4
  import { sdkLogger } from '../logger'
5
5
  import { generateTraceId, X_PORTAL_TRACE_ID_HEADER } from '../shared/trace'
6
+ import { isPortalHostedUrl } from '../shared/rpc/auth'
6
7
 
7
8
  import type {
8
9
  EventHandler,
@@ -137,6 +138,7 @@ export enum RequestMethod {
137
138
 
138
139
  // TRON Wallet Methods
139
140
  tron_sendTransaction = 'tron_sendTransaction',
141
+ tron_signTypedData = 'tron_signTypedData',
140
142
  }
141
143
 
142
144
  const passiveSignerMethods = [
@@ -161,6 +163,7 @@ const signerMethods = [
161
163
  RequestMethod.sol_signMessage,
162
164
  RequestMethod.sol_signTransaction,
163
165
  RequestMethod.tron_sendTransaction,
166
+ RequestMethod.tron_signTypedData,
164
167
  ]
165
168
 
166
169
  const iframeProxiedMethodStrings: string[] = [
@@ -450,6 +453,8 @@ class Provider {
450
453
  )
451
454
  }
452
455
 
456
+ const rpcUrl = this.portal.getRpcUrl(chainId)
457
+
453
458
  const requestBody = {
454
459
  body: JSON.stringify({
455
460
  jsonrpc: '2.0',
@@ -460,11 +465,22 @@ class Provider {
460
465
  method: 'POST',
461
466
  headers: {
462
467
  'Content-Type': 'application/json',
468
+ // Portal's /rpc gateway authenticates via `Authorization: Bearer <token>`.
469
+ // It accepts two credential types (backend middleware: clientAuthMiddleware):
470
+ // • apiKey — ClientApiKey: persistent, no expiry. Preferred when set.
471
+ // • authToken — ClientSessionToken: short-lived, auto-refreshing.
472
+ // Both grant identical permissions at the RPC layer.
473
+ // The distinction (apiKey-only for /service MPC routes) does not apply here.
474
+ // Header is injected only for Portal-owned hostnames to prevent credential
475
+ // leakage to third-party RPC providers.
476
+ ...(isPortalHostedUrl(rpcUrl) && (this.portal.apiKey ?? this.portal.authToken)
477
+ ? { Authorization: `Bearer ${this.portal.apiKey ?? this.portal.authToken}` }
478
+ : {}),
463
479
  ...(traceId && { [X_PORTAL_TRACE_ID_HEADER]: traceId }),
464
480
  },
465
481
  }
466
482
 
467
- const result = await fetch(this.portal.getRpcUrl(chainId), requestBody)
483
+ const result = await fetch(rpcUrl, requestBody)
468
484
 
469
485
  return result.json()
470
486
  }
@@ -559,6 +575,22 @@ class Provider {
559
575
  })
560
576
  return result
561
577
  }
578
+ case RequestMethod.tron_signTypedData: {
579
+ this.enforceTronChainId(chainId)
580
+ const result = await this.portal.mpc.sign({
581
+ chainId: chainId as string,
582
+ method,
583
+ params: this.buildParams(method, params),
584
+ // tron_signTypedData is a sign-only method; MPC does not require an RPC client for it
585
+ rpcUrl: '',
586
+ sponsorGas,
587
+ traceId,
588
+ ...(signatureApprovalMemo !== undefined && {
589
+ signatureApprovalMemo,
590
+ }),
591
+ })
592
+ return result
593
+ }
562
594
  default:
563
595
  throw new Error(
564
596
  '[PortalProvider] Method "' + method + '" not supported',
@@ -618,6 +650,37 @@ class Provider {
618
650
  params = [txParams]
619
651
  }
620
652
  break
653
+ case RequestMethod.tron_signTypedData: {
654
+ const data = Array.isArray(txParams) ? txParams[0] : txParams
655
+ let parsed: unknown
656
+ if (typeof data === 'string') {
657
+ try {
658
+ parsed = JSON.parse(data)
659
+ } catch {
660
+ throw new Error(
661
+ '[PortalProvider] tron_signTypedData params must be valid JSON',
662
+ )
663
+ }
664
+ params = data
665
+ } else {
666
+ parsed = data
667
+ params = JSON.stringify(data, (_key, value) =>
668
+ typeof value === 'bigint' ? value.toString() : value,
669
+ )
670
+ }
671
+ if (
672
+ typeof parsed !== 'object' ||
673
+ parsed === null ||
674
+ !('domain' in parsed) ||
675
+ !('types' in parsed) ||
676
+ !('value' in parsed)
677
+ ) {
678
+ throw new Error(
679
+ '[PortalProvider] tron_signTypedData params must include "domain", "types", and "value"',
680
+ )
681
+ }
682
+ break
683
+ }
621
684
  default:
622
685
  if (Array.isArray(txParams)) {
623
686
  params = txParams[0]