@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
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.
|
|
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
|
|
402
|
+
traceId,
|
|
380
403
|
})
|
|
381
404
|
}
|
|
382
405
|
|
|
@@ -906,15 +929,191 @@ class Mpc {
|
|
|
906
929
|
})
|
|
907
930
|
}
|
|
908
931
|
|
|
909
|
-
public async getPaymentMethods(
|
|
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', () => {
|
package/src/provider/index.ts
CHANGED
|
@@ -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(
|
|
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]
|