@0xkey-io/core 0.1.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/LICENSE +201 -0
- package/README.MD +7 -0
- package/dist/0xkey-helpers.d.ts +68 -0
- package/dist/0xkey-helpers.d.ts.map +1 -0
- package/dist/0xkey-helpers.js +466 -0
- package/dist/0xkey-helpers.js.map +1 -0
- package/dist/0xkey-helpers.mjs +399 -0
- package/dist/0xkey-helpers.mjs.map +1 -0
- package/dist/__clients__/core.d.ts +1201 -0
- package/dist/__clients__/core.d.ts.map +1 -0
- package/dist/__clients__/core.js +3489 -0
- package/dist/__clients__/core.js.map +1 -0
- package/dist/__clients__/core.mjs +3487 -0
- package/dist/__clients__/core.mjs.map +1 -0
- package/dist/__generated__/sdk-client-base.d.ts +281 -0
- package/dist/__generated__/sdk-client-base.d.ts.map +1 -0
- package/dist/__generated__/sdk-client-base.js +3906 -0
- package/dist/__generated__/sdk-client-base.js.map +1 -0
- package/dist/__generated__/sdk-client-base.mjs +3904 -0
- package/dist/__generated__/sdk-client-base.mjs.map +1 -0
- package/dist/__generated__/version.d.ts +2 -0
- package/dist/__generated__/version.d.ts.map +1 -0
- package/dist/__generated__/version.js +6 -0
- package/dist/__generated__/version.js.map +1 -0
- package/dist/__generated__/version.mjs +4 -0
- package/dist/__generated__/version.mjs.map +1 -0
- package/dist/__inputs__/public_api.types.d.ts +6014 -0
- package/dist/__inputs__/public_api.types.d.ts.map +1 -0
- package/dist/__polyfills__/jest.setup.webcrypto.d.ts +2 -0
- package/dist/__polyfills__/jest.setup.webcrypto.d.ts.map +1 -0
- package/dist/__polyfills__/window.d.ts +15 -0
- package/dist/__polyfills__/window.d.ts.map +1 -0
- package/dist/__polyfills__/window.js +30 -0
- package/dist/__polyfills__/window.js.map +1 -0
- package/dist/__polyfills__/window.mjs +28 -0
- package/dist/__polyfills__/window.mjs.map +1 -0
- package/dist/__stampers__/api/base.d.ts +27 -0
- package/dist/__stampers__/api/base.d.ts.map +1 -0
- package/dist/__stampers__/api/base.js +102 -0
- package/dist/__stampers__/api/base.js.map +1 -0
- package/dist/__stampers__/api/base.mjs +100 -0
- package/dist/__stampers__/api/base.mjs.map +1 -0
- package/dist/__stampers__/api/mobile/stamper.d.ts +15 -0
- package/dist/__stampers__/api/mobile/stamper.d.ts.map +1 -0
- package/dist/__stampers__/api/mobile/stamper.js +119 -0
- package/dist/__stampers__/api/mobile/stamper.js.map +1 -0
- package/dist/__stampers__/api/mobile/stamper.mjs +117 -0
- package/dist/__stampers__/api/mobile/stamper.mjs.map +1 -0
- package/dist/__stampers__/api/web/stamper.d.ts +14 -0
- package/dist/__stampers__/api/web/stamper.d.ts.map +1 -0
- package/dist/__stampers__/api/web/stamper.js +201 -0
- package/dist/__stampers__/api/web/stamper.js.map +1 -0
- package/dist/__stampers__/api/web/stamper.mjs +199 -0
- package/dist/__stampers__/api/web/stamper.mjs.map +1 -0
- package/dist/__stampers__/passkey/base.d.ts +16 -0
- package/dist/__stampers__/passkey/base.d.ts.map +1 -0
- package/dist/__stampers__/passkey/base.js +138 -0
- package/dist/__stampers__/passkey/base.js.map +1 -0
- package/dist/__stampers__/passkey/base.mjs +134 -0
- package/dist/__stampers__/passkey/base.mjs.map +1 -0
- package/dist/__storage__/base.d.ts +3 -0
- package/dist/__storage__/base.d.ts.map +1 -0
- package/dist/__storage__/base.js +27 -0
- package/dist/__storage__/base.js.map +1 -0
- package/dist/__storage__/base.mjs +25 -0
- package/dist/__storage__/base.mjs.map +1 -0
- package/dist/__storage__/mobile/storage.d.ts +18 -0
- package/dist/__storage__/mobile/storage.d.ts.map +1 -0
- package/dist/__storage__/mobile/storage.js +76 -0
- package/dist/__storage__/mobile/storage.js.map +1 -0
- package/dist/__storage__/mobile/storage.mjs +74 -0
- package/dist/__storage__/mobile/storage.mjs.map +1 -0
- package/dist/__storage__/web/storage.d.ts +19 -0
- package/dist/__storage__/web/storage.d.ts.map +1 -0
- package/dist/__storage__/web/storage.js +79 -0
- package/dist/__storage__/web/storage.js.map +1 -0
- package/dist/__storage__/web/storage.mjs +77 -0
- package/dist/__storage__/web/storage.mjs.map +1 -0
- package/dist/__typedoc-entry__/index.d.ts +3 -0
- package/dist/__typedoc-entry__/index.d.ts.map +1 -0
- package/dist/__types__/auth.d.ts +120 -0
- package/dist/__types__/auth.d.ts.map +1 -0
- package/dist/__types__/auth.js +7 -0
- package/dist/__types__/auth.js.map +1 -0
- package/dist/__types__/auth.mjs +5 -0
- package/dist/__types__/auth.mjs.map +1 -0
- package/dist/__types__/config.d.ts +145 -0
- package/dist/__types__/config.d.ts.map +1 -0
- package/dist/__types__/enums.d.ts +59 -0
- package/dist/__types__/enums.d.ts.map +1 -0
- package/dist/__types__/enums.js +72 -0
- package/dist/__types__/enums.js.map +1 -0
- package/dist/__types__/enums.mjs +70 -0
- package/dist/__types__/enums.mjs.map +1 -0
- package/dist/__types__/error.d.ts +13 -0
- package/dist/__types__/error.d.ts.map +1 -0
- package/dist/__types__/error.js +18 -0
- package/dist/__types__/error.js.map +1 -0
- package/dist/__types__/error.mjs +16 -0
- package/dist/__types__/error.mjs.map +1 -0
- package/dist/__types__/export.d.ts +10 -0
- package/dist/__types__/export.d.ts.map +1 -0
- package/dist/__types__/external-wallets.d.ts +203 -0
- package/dist/__types__/external-wallets.d.ts.map +1 -0
- package/dist/__types__/http.d.ts +17 -0
- package/dist/__types__/http.d.ts.map +1 -0
- package/dist/__types__/http.js +11 -0
- package/dist/__types__/http.js.map +1 -0
- package/dist/__types__/http.mjs +9 -0
- package/dist/__types__/http.mjs.map +1 -0
- package/dist/__types__/index.d.ts +10 -0
- package/dist/__types__/index.d.ts.map +1 -0
- package/dist/__types__/method-types/import-export-params.d.ts +38 -0
- package/dist/__types__/method-types/import-export-params.d.ts.map +1 -0
- package/dist/__types__/method-types/shared.d.ts +378 -0
- package/dist/__types__/method-types/shared.d.ts.map +1 -0
- package/dist/__wallet__/base.d.ts +14 -0
- package/dist/__wallet__/base.d.ts.map +1 -0
- package/dist/__wallet__/base.js +35 -0
- package/dist/__wallet__/base.js.map +1 -0
- package/dist/__wallet__/base.mjs +33 -0
- package/dist/__wallet__/base.mjs.map +1 -0
- package/dist/__wallet__/connector.d.ts +60 -0
- package/dist/__wallet__/connector.d.ts.map +1 -0
- package/dist/__wallet__/connector.js +93 -0
- package/dist/__wallet__/connector.js.map +1 -0
- package/dist/__wallet__/connector.mjs +91 -0
- package/dist/__wallet__/connector.mjs.map +1 -0
- package/dist/__wallet__/mobile/manager.d.ts +51 -0
- package/dist/__wallet__/mobile/manager.d.ts.map +1 -0
- package/dist/__wallet__/mobile/manager.js +115 -0
- package/dist/__wallet__/mobile/manager.js.map +1 -0
- package/dist/__wallet__/mobile/manager.mjs +113 -0
- package/dist/__wallet__/mobile/manager.mjs.map +1 -0
- package/dist/__wallet__/stamper.d.ts +90 -0
- package/dist/__wallet__/stamper.d.ts.map +1 -0
- package/dist/__wallet__/stamper.js +183 -0
- package/dist/__wallet__/stamper.js.map +1 -0
- package/dist/__wallet__/stamper.mjs +180 -0
- package/dist/__wallet__/stamper.mjs.map +1 -0
- package/dist/__wallet__/wallet-connect/base.d.ts +182 -0
- package/dist/__wallet__/wallet-connect/base.d.ts.map +1 -0
- package/dist/__wallet__/wallet-connect/base.js +606 -0
- package/dist/__wallet__/wallet-connect/base.js.map +1 -0
- package/dist/__wallet__/wallet-connect/base.mjs +604 -0
- package/dist/__wallet__/wallet-connect/base.mjs.map +1 -0
- package/dist/__wallet__/wallet-connect/client.d.ts +132 -0
- package/dist/__wallet__/wallet-connect/client.d.ts.map +1 -0
- package/dist/__wallet__/wallet-connect/client.js +262 -0
- package/dist/__wallet__/wallet-connect/client.js.map +1 -0
- package/dist/__wallet__/wallet-connect/client.mjs +260 -0
- package/dist/__wallet__/wallet-connect/client.mjs.map +1 -0
- package/dist/__wallet__/web/manager.d.ts +70 -0
- package/dist/__wallet__/web/manager.d.ts.map +1 -0
- package/dist/__wallet__/web/manager.js +177 -0
- package/dist/__wallet__/web/manager.js.map +1 -0
- package/dist/__wallet__/web/manager.mjs +175 -0
- package/dist/__wallet__/web/manager.mjs.map +1 -0
- package/dist/__wallet__/web/native/ethereum.d.ts +101 -0
- package/dist/__wallet__/web/native/ethereum.d.ts.map +1 -0
- package/dist/__wallet__/web/native/ethereum.js +339 -0
- package/dist/__wallet__/web/native/ethereum.js.map +1 -0
- package/dist/__wallet__/web/native/ethereum.mjs +336 -0
- package/dist/__wallet__/web/native/ethereum.mjs.map +1 -0
- package/dist/__wallet__/web/native/solana.d.ts +72 -0
- package/dist/__wallet__/web/native/solana.d.ts.map +1 -0
- package/dist/__wallet__/web/native/solana.js +215 -0
- package/dist/__wallet__/web/native/solana.js.map +1 -0
- package/dist/__wallet__/web/native/solana.mjs +212 -0
- package/dist/__wallet__/web/native/solana.mjs.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +83 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +10 -0
- package/dist/index.mjs.map +1 -0
- package/dist/utils.d.ts +160 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +1047 -0
- package/dist/utils.js.map +1 -0
- package/dist/utils.mjs +1007 -0
- package/dist/utils.mjs.map +1 -0
- package/package.json +80 -0
|
@@ -0,0 +1,3487 @@
|
|
|
1
|
+
import { ZeroXKeySDKClientBase } from '../__generated__/sdk-client-base.mjs';
|
|
2
|
+
import { ZeroXKeyErrorCodes, ZeroXKeyError, AuthAction } from '@0xkey-io/sdk-types';
|
|
3
|
+
import { DEFAULT_SESSION_EXPIRATION_IN_SECONDS } from '../__types__/auth.mjs';
|
|
4
|
+
import { SessionKey, StamperType, WalletSource, FilterType, OtpTypeToFilterTypeMap, OtpType, Curve, Chain, SignIntent } from '../__types__/enums.mjs';
|
|
5
|
+
import { withZeroXKeyErrorHandling, isValidPasskeyName, isWeb, isReactNative, buildSignUpBody, findWalletProviderFromAddress, getPublicKeyFromStampHeader, addressFromPublicKey, getCurveTypeFromProvider, getClientSignatureMessageForLogin, getClientSignatureMessageForSignup, getAuthenticatorAddresses, fetchAllWalletAccountsWithCursor, mapAccountsToWallet, toExternalTimestamp, isEthereumProvider, isSolanaProvider, getActiveSessionOrThrowIfRequired, getHashFunction, getEncodingType, getEncodedMessage, splitSignature, broadcastTransaction, getPolicySignature, googleISS, isWalletAccountArray, generateWalletAccountsFromAddressFormat } from '../utils.mjs';
|
|
6
|
+
import { createStorageManager } from '../__storage__/base.mjs';
|
|
7
|
+
import { CrossPlatformApiKeyStamper } from '../__stampers__/api/base.mjs';
|
|
8
|
+
import { CrossPlatformPasskeyStamper } from '../__stampers__/passkey/base.mjs';
|
|
9
|
+
import { DEFAULT_ETHEREUM_ACCOUNTS, DEFAULT_SOLANA_ACCOUNTS } from '../0xkey-helpers.mjs';
|
|
10
|
+
import { jwtDecode } from 'jwt-decode';
|
|
11
|
+
import { createWalletManager } from '../__wallet__/base.mjs';
|
|
12
|
+
import { toUtf8Bytes } from 'ethers';
|
|
13
|
+
import { verify, hpkeEncrypt, formatHpkeBuf } from '@0xkey-io/crypto';
|
|
14
|
+
import { SignatureFormat } from '@0xkey-io/api-key-stamper';
|
|
15
|
+
import { encodeFunctionData } from 'viem';
|
|
16
|
+
|
|
17
|
+
const ERC20_TRANSFER_ABI = [
|
|
18
|
+
{
|
|
19
|
+
type: "function",
|
|
20
|
+
name: "transfer",
|
|
21
|
+
stateMutability: "nonpayable",
|
|
22
|
+
inputs: [
|
|
23
|
+
{ name: "to", type: "address" },
|
|
24
|
+
{ name: "amount", type: "uint256" },
|
|
25
|
+
],
|
|
26
|
+
outputs: [{ name: "success", type: "bool" }],
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
const hexToBytes = (hex) => {
|
|
30
|
+
const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
31
|
+
if (cleanHex.length % 2 !== 0) {
|
|
32
|
+
throw new Error("invalid hex string");
|
|
33
|
+
}
|
|
34
|
+
const bytes = new Uint8Array(cleanHex.length / 2);
|
|
35
|
+
for (let i = 0; i < cleanHex.length; i += 2) {
|
|
36
|
+
bytes[i / 2] = Number.parseInt(cleanHex.slice(i, i + 2), 16);
|
|
37
|
+
}
|
|
38
|
+
return bytes;
|
|
39
|
+
};
|
|
40
|
+
const utf8Encode = (value) => new TextEncoder().encode(value);
|
|
41
|
+
const utf8Decode = (value) => new TextDecoder().decode(value);
|
|
42
|
+
const encryptOtpAttemptBundle = ({ otpCode, publicKey, otpEncryptionTargetBundle, }) => {
|
|
43
|
+
const targetBundle = JSON.parse(otpEncryptionTargetBundle);
|
|
44
|
+
const targetData = JSON.parse(utf8Decode(hexToBytes(targetBundle.data)));
|
|
45
|
+
const plaintext = utf8Encode(JSON.stringify({
|
|
46
|
+
otp_code: otpCode,
|
|
47
|
+
public_key: publicKey,
|
|
48
|
+
}));
|
|
49
|
+
const encrypted = hpkeEncrypt({
|
|
50
|
+
plainTextBuf: plaintext,
|
|
51
|
+
targetKeyBuf: hexToBytes(targetData.targetPublic),
|
|
52
|
+
hpkeInfo: "0xkey_hpke",
|
|
53
|
+
});
|
|
54
|
+
return formatHpkeBuf(encrypted);
|
|
55
|
+
};
|
|
56
|
+
class ZeroXKeyClient {
|
|
57
|
+
constructor(config,
|
|
58
|
+
// Users can pass in their own stampers, or we will create them. Should we remove this?
|
|
59
|
+
apiKeyStamper, passkeyStamper, walletManager) {
|
|
60
|
+
/**
|
|
61
|
+
* Creates a new ZeroXKeySDKClientBase instance with the provided configuration.
|
|
62
|
+
* This method is used internally to create the HTTP client for making API requests,
|
|
63
|
+
* but can also be used to create an additional client with different configurations if needed.
|
|
64
|
+
* By default, it uses the configuration provided during the ZeroXKeyClient initialization.
|
|
65
|
+
*
|
|
66
|
+
* @param params - Optional configuration parameters to override the default client configuration.
|
|
67
|
+
* @param params.apiBaseUrl - The base URL of the ZeroXKey API (defaults to `https://api.0xkey.com` if not provided).
|
|
68
|
+
* @param params.organizationId - The organization ID to associate requests with.
|
|
69
|
+
* @param params.authProxyUrl - The base URL of the Auth Proxy (defaults to `https://authproxy.0xkey.com` if not provided).
|
|
70
|
+
* @param params.authProxyConfigId - The configuration ID to use when making Auth Proxy requests.
|
|
71
|
+
* @param params.defaultStamperType - The default stamper type to use for signing requests
|
|
72
|
+
* (overrides automatic detection of ApiKey, Passkey, or Wallet stampers).
|
|
73
|
+
*
|
|
74
|
+
* @returns A new instance of {@link ZeroXKeySDKClientBase} configured with the provided parameters.
|
|
75
|
+
*/
|
|
76
|
+
this.createHttpClient = (params) => {
|
|
77
|
+
// We can comfortably default to the prod urls here
|
|
78
|
+
const apiBaseUrl = params?.apiBaseUrl || this.config.apiBaseUrl || "https://api.0xkey.com";
|
|
79
|
+
const authProxyUrl = params?.authProxyUrl ||
|
|
80
|
+
this.config.authProxyUrl ||
|
|
81
|
+
"https://authproxy.0xkey.com";
|
|
82
|
+
const organizationId = params?.organizationId || this.config.organizationId;
|
|
83
|
+
return new ZeroXKeySDKClientBase({
|
|
84
|
+
...this.config,
|
|
85
|
+
...params,
|
|
86
|
+
apiBaseUrl,
|
|
87
|
+
authProxyUrl,
|
|
88
|
+
organizationId,
|
|
89
|
+
apiKeyStamper: this.apiKeyStamper,
|
|
90
|
+
passkeyStamper: this.passkeyStamper,
|
|
91
|
+
walletStamper: this.walletManager?.stamper,
|
|
92
|
+
storageManager: this.storageManager,
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Creates a new passkey authenticator for the user.
|
|
97
|
+
*
|
|
98
|
+
* - This function generates a new passkey attestation and challenge, suitable for registration with the user's device.
|
|
99
|
+
* - Handles both web and React Native environments, automatically selecting the appropriate passkey creation flow.
|
|
100
|
+
* - The resulting attestation and challenge can be used to register the passkey with ZeroXKey.
|
|
101
|
+
*
|
|
102
|
+
* @param params.name - display name for the passkey (defaults to a generated name based on the current timestamp).
|
|
103
|
+
* @param params.challenge - challenge string to use for passkey registration. If not provided, a new challenge will be generated.
|
|
104
|
+
* @returns A promise that resolves to {@link CreatePasskeyResult}
|
|
105
|
+
* @throws {ZeroXKeyError} If there is an error during passkey creation, or if the platform is unsupported.
|
|
106
|
+
*/
|
|
107
|
+
this.createPasskey = async (params) => {
|
|
108
|
+
const { name: nameFromParams, challenge } = params || {};
|
|
109
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
110
|
+
if (!this.passkeyStamper) {
|
|
111
|
+
throw new ZeroXKeyError("Passkey stamper is not initialized", ZeroXKeyErrorCodes.INTERNAL_ERROR);
|
|
112
|
+
}
|
|
113
|
+
const name = isValidPasskeyName(nameFromParams || `passkey-${Date.now()}`);
|
|
114
|
+
let passkey;
|
|
115
|
+
if (isWeb()) {
|
|
116
|
+
const res = await this.passkeyStamper.createWebPasskey({
|
|
117
|
+
publicKey: {
|
|
118
|
+
user: {
|
|
119
|
+
name,
|
|
120
|
+
displayName: name,
|
|
121
|
+
},
|
|
122
|
+
...(challenge && { challenge }),
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
if (!res) {
|
|
126
|
+
throw new ZeroXKeyError("Failed to create Web passkey", ZeroXKeyErrorCodes.INTERNAL_ERROR);
|
|
127
|
+
}
|
|
128
|
+
passkey = {
|
|
129
|
+
encodedChallenge: res?.encodedChallenge,
|
|
130
|
+
attestation: res?.attestation,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
else if (isReactNative()) {
|
|
134
|
+
const res = await this.passkeyStamper.createReactNativePasskey({
|
|
135
|
+
name,
|
|
136
|
+
displayName: name,
|
|
137
|
+
});
|
|
138
|
+
if (!res) {
|
|
139
|
+
throw new ZeroXKeyError("Failed to create React Native passkey", ZeroXKeyErrorCodes.INTERNAL_ERROR);
|
|
140
|
+
}
|
|
141
|
+
passkey = {
|
|
142
|
+
encodedChallenge: res?.challenge,
|
|
143
|
+
attestation: res?.attestation,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
throw new ZeroXKeyError("Unsupported platform for passkey creation", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
148
|
+
}
|
|
149
|
+
return passkey;
|
|
150
|
+
}, {
|
|
151
|
+
errorMessage: "Failed to create passkey",
|
|
152
|
+
errorCode: ZeroXKeyErrorCodes.CREATE_PASSKEY_ERROR,
|
|
153
|
+
customErrorsByMessages: {
|
|
154
|
+
"timed out or was not allowed": {
|
|
155
|
+
message: "Passkey creation was cancelled by the user.",
|
|
156
|
+
code: ZeroXKeyErrorCodes.SELECT_PASSKEY_CANCELLED,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* Logs out the current client session.
|
|
163
|
+
*
|
|
164
|
+
* - This function clears the specified session and removes any associated key pairs from storage.
|
|
165
|
+
* - If a sessionKey is provided, it logs out from that session; otherwise, it logs out from the active session.
|
|
166
|
+
* - Cleans up any api keys associated with the session.
|
|
167
|
+
*
|
|
168
|
+
* @param params.sessionKey - session key to specify which session to log out from (defaults to the active session).
|
|
169
|
+
* @returns A promise that resolves when the logout process is complete.
|
|
170
|
+
* @throws {ZeroXKeyError} If there is no active session or if there is an error during the logout process.
|
|
171
|
+
*/
|
|
172
|
+
this.logout = async (params) => {
|
|
173
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
174
|
+
if (params?.sessionKey) {
|
|
175
|
+
const session = await this.storageManager.getSession(params.sessionKey);
|
|
176
|
+
this.storageManager.clearSession(params.sessionKey);
|
|
177
|
+
this.apiKeyStamper?.deleteKeyPair(session?.publicKey);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
const sessionKey = await this.storageManager.getActiveSessionKey();
|
|
181
|
+
const session = await this.storageManager.getActiveSession();
|
|
182
|
+
if (sessionKey) {
|
|
183
|
+
this.storageManager.clearSession(sessionKey);
|
|
184
|
+
this.apiKeyStamper?.deleteKeyPair(session?.publicKey);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
throw new ZeroXKeyError("No active session found to log out from.", ZeroXKeyErrorCodes.NO_SESSION_FOUND);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}, {
|
|
191
|
+
errorMessage: "Failed to log out",
|
|
192
|
+
errorCode: ZeroXKeyErrorCodes.LOGOUT_ERROR,
|
|
193
|
+
});
|
|
194
|
+
};
|
|
195
|
+
/**
|
|
196
|
+
* Logs in a user using a passkey, optionally specifying the public key, session key, and session expiration.
|
|
197
|
+
*
|
|
198
|
+
* - This function initiates the login process with a passkey and handles session creation and storage.
|
|
199
|
+
* - If a public key is not provided, a new key pair will be generated for authentication.
|
|
200
|
+
* - If a session key is not provided, the default session key will be used.
|
|
201
|
+
* - The session expiration can be customized via the expirationSeconds parameter.
|
|
202
|
+
* - Handles cleanup of unused key pairs if login fails.
|
|
203
|
+
*
|
|
204
|
+
* @param params.publicKey - public key to use for authentication. If not provided, a new key pair will be generated.
|
|
205
|
+
* @param params.sessionKey - session key to use for session creation (defaults to the default session key).
|
|
206
|
+
* @param params.expirationSeconds - session expiration time in seconds (defaults to the configured default).
|
|
207
|
+
* @param params.organizationId - organization ID to target (defaults to the session's organization ID or the parent organization ID).
|
|
208
|
+
* @returns A promise that resolves to a {@link PasskeyAuthResult}, which includes:
|
|
209
|
+
* - `sessionToken`: the signed JWT session token.
|
|
210
|
+
* - `credentialId`: an empty string.
|
|
211
|
+
* @throws {ZeroXKeyError} If there is an error during the passkey login process or if the user cancels the passkey prompt.
|
|
212
|
+
*/
|
|
213
|
+
this.loginWithPasskey = async (params) => {
|
|
214
|
+
let generatedPublicKey = undefined;
|
|
215
|
+
return await withZeroXKeyErrorHandling(async () => {
|
|
216
|
+
generatedPublicKey =
|
|
217
|
+
params?.publicKey || (await this.apiKeyStamper?.createKeyPair());
|
|
218
|
+
const sessionKey = params?.sessionKey || SessionKey.DefaultSessionkey;
|
|
219
|
+
const expirationSeconds = params?.expirationSeconds || DEFAULT_SESSION_EXPIRATION_IN_SECONDS;
|
|
220
|
+
if (!generatedPublicKey) {
|
|
221
|
+
throw new ZeroXKeyError("A publickey could not be found or generated.", ZeroXKeyErrorCodes.INTERNAL_ERROR);
|
|
222
|
+
}
|
|
223
|
+
const sessionResponse = await this.httpClient.stampLogin({
|
|
224
|
+
publicKey: generatedPublicKey,
|
|
225
|
+
organizationId: params?.organizationId ?? this.config.organizationId,
|
|
226
|
+
expirationSeconds,
|
|
227
|
+
}, StamperType.Passkey);
|
|
228
|
+
await this.storeSession({
|
|
229
|
+
sessionToken: sessionResponse.session,
|
|
230
|
+
sessionKey,
|
|
231
|
+
});
|
|
232
|
+
generatedPublicKey = undefined; // Key pair was successfully used, set to null to prevent cleanup
|
|
233
|
+
return {
|
|
234
|
+
sessionToken: sessionResponse.session,
|
|
235
|
+
// TODO: can we return the credentialId here?
|
|
236
|
+
// from a quick glance this is going to be difficult
|
|
237
|
+
// for now we return an empty string
|
|
238
|
+
credentialId: "",
|
|
239
|
+
};
|
|
240
|
+
}, {
|
|
241
|
+
errorMessage: "Unable to log in with the provided passkey",
|
|
242
|
+
errorCode: ZeroXKeyErrorCodes.PASSKEY_LOGIN_AUTH_ERROR,
|
|
243
|
+
customErrorsByMessages: {
|
|
244
|
+
"timed out or was not allowed": {
|
|
245
|
+
message: "Passkey login was cancelled by the user.",
|
|
246
|
+
code: ZeroXKeyErrorCodes.SELECT_PASSKEY_CANCELLED,
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
}, {
|
|
250
|
+
finallyFn: async () => {
|
|
251
|
+
if (generatedPublicKey) {
|
|
252
|
+
try {
|
|
253
|
+
await this.apiKeyStamper?.deleteKeyPair(generatedPublicKey);
|
|
254
|
+
}
|
|
255
|
+
catch (cleanupError) {
|
|
256
|
+
throw new ZeroXKeyError(`Failed to clean up generated key pair`, ZeroXKeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
};
|
|
262
|
+
/**
|
|
263
|
+
* Signs up a user using a passkey, creating a new sub-organization and session.
|
|
264
|
+
*
|
|
265
|
+
* - This function creates a new passkey authenticator and uses it to register a new sub-organization for the user.
|
|
266
|
+
* - Handles both passkey creation and sub-organization creation in a single flow.
|
|
267
|
+
* - Optionally accepts additional sub-organization parameters, a custom session key, a custom passkey display name, and a custom session expiration.
|
|
268
|
+
* - Automatically generates a new API key pair for authentication and session management.
|
|
269
|
+
* - Stores the resulting session token and manages cleanup of unused key pairs.
|
|
270
|
+
*
|
|
271
|
+
* @param params.passkeyDisplayName - display name for the passkey (defaults to a generated name based on the current timestamp).
|
|
272
|
+
* @param params.challenge - challenge string to use for passkey registration. If not provided, a new challenge will be generated.
|
|
273
|
+
* @param params.expirationSeconds - session expiration time in seconds (defaults to the configured default).
|
|
274
|
+
* @param params.createSubOrgParams - parameters for creating a sub-organization (e.g., authenticators, user metadata).
|
|
275
|
+
* @param params.sessionKey - session key to use for storing the session (defaults to the default session key).
|
|
276
|
+
* @param params.organizationId - organization ID to target (defaults to the session's organization ID or the parent organization ID).
|
|
277
|
+
* @returns A promise that resolves to a {@link PasskeyAuthResult}, which includes:
|
|
278
|
+
* - `sessionToken`: the signed JWT session token.
|
|
279
|
+
* - `credentialId`: the credential ID associated with the passkey created.
|
|
280
|
+
* @throws {ZeroXKeyError} If there is an error during passkey creation, sub-organization creation, or session storage.
|
|
281
|
+
*/
|
|
282
|
+
this.signUpWithPasskey = async (params) => {
|
|
283
|
+
const { passkeyDisplayName, challenge, expirationSeconds = DEFAULT_SESSION_EXPIRATION_IN_SECONDS, createSubOrgParams, sessionKey = SessionKey.DefaultSessionkey, organizationId, } = params || {};
|
|
284
|
+
let generatedPublicKey = undefined;
|
|
285
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
286
|
+
generatedPublicKey = await this.apiKeyStamper?.createKeyPair();
|
|
287
|
+
const passkeyName = passkeyDisplayName || `passkey-${Date.now()}`;
|
|
288
|
+
// A passkey will be created automatically when you call this function. The name is passed in
|
|
289
|
+
const passkey = await this.createPasskey({
|
|
290
|
+
name: passkeyName,
|
|
291
|
+
...(challenge && { challenge }),
|
|
292
|
+
});
|
|
293
|
+
if (!passkey) {
|
|
294
|
+
throw new ZeroXKeyError("Failed to create passkey: encoded challenge or attestation is missing", ZeroXKeyErrorCodes.INTERNAL_ERROR);
|
|
295
|
+
}
|
|
296
|
+
const signUpBody = buildSignUpBody({
|
|
297
|
+
createSubOrgParams: {
|
|
298
|
+
...createSubOrgParams,
|
|
299
|
+
authenticators: [
|
|
300
|
+
...(createSubOrgParams?.authenticators ?? []), // Any extra authenticators can be passed into createSubOrgParams
|
|
301
|
+
{
|
|
302
|
+
// Add our passkey that we made earlier.
|
|
303
|
+
authenticatorName: passkeyName, // Ensure the name in orgData is the same name as the created passkey
|
|
304
|
+
challenge: passkey.encodedChallenge,
|
|
305
|
+
attestation: passkey.attestation,
|
|
306
|
+
},
|
|
307
|
+
],
|
|
308
|
+
apiKeys: [
|
|
309
|
+
{
|
|
310
|
+
apiKeyName: `passkey-auth-${generatedPublicKey}`,
|
|
311
|
+
publicKey: generatedPublicKey,
|
|
312
|
+
curveType: "API_KEY_CURVE_P256",
|
|
313
|
+
expirationSeconds: "60",
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
const res = await this.httpClient.proxySignup(signUpBody);
|
|
319
|
+
if (!res) {
|
|
320
|
+
throw new ZeroXKeyError(`Sign up failed`, ZeroXKeyErrorCodes.PASSKEY_SIGNUP_AUTH_ERROR);
|
|
321
|
+
}
|
|
322
|
+
const newGeneratedKeyPair = await this.apiKeyStamper?.createKeyPair();
|
|
323
|
+
this.apiKeyStamper?.setTemporaryPublicKey(generatedPublicKey);
|
|
324
|
+
const sessionResponse = await this.httpClient.stampLogin({
|
|
325
|
+
publicKey: newGeneratedKeyPair,
|
|
326
|
+
organizationId: organizationId ?? this.config.organizationId,
|
|
327
|
+
expirationSeconds,
|
|
328
|
+
});
|
|
329
|
+
await Promise.all([
|
|
330
|
+
this.apiKeyStamper?.deleteKeyPair(generatedPublicKey),
|
|
331
|
+
this.storeSession({
|
|
332
|
+
sessionToken: sessionResponse.session,
|
|
333
|
+
sessionKey,
|
|
334
|
+
}),
|
|
335
|
+
]);
|
|
336
|
+
generatedPublicKey = undefined; // Key pair was successfully used, set to null to prevent cleanup
|
|
337
|
+
return {
|
|
338
|
+
sessionToken: sessionResponse.session,
|
|
339
|
+
appProofs: res.appProofs,
|
|
340
|
+
credentialId: passkey.attestation.credentialId,
|
|
341
|
+
};
|
|
342
|
+
}, {
|
|
343
|
+
errorCode: ZeroXKeyErrorCodes.PASSKEY_SIGNUP_AUTH_ERROR,
|
|
344
|
+
errorMessage: "Failed to sign up with passkey",
|
|
345
|
+
}, {
|
|
346
|
+
finallyFn: async () => {
|
|
347
|
+
this.apiKeyStamper?.clearTemporaryPublicKey();
|
|
348
|
+
if (generatedPublicKey) {
|
|
349
|
+
try {
|
|
350
|
+
await this.apiKeyStamper?.deleteKeyPair(generatedPublicKey);
|
|
351
|
+
}
|
|
352
|
+
catch (cleanupError) {
|
|
353
|
+
throw new ZeroXKeyError(`Failed to clean up generated key pair`, ZeroXKeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
};
|
|
359
|
+
/**
|
|
360
|
+
* Retrieves wallet providers from the initialized wallet manager.
|
|
361
|
+
*
|
|
362
|
+
* - Optionally filters providers by the specified blockchain chain.
|
|
363
|
+
* - Throws an error if the wallet manager is not initialized.
|
|
364
|
+
*
|
|
365
|
+
* @param chain - optional blockchain chain to filter the returned providers.
|
|
366
|
+
* @returns A promise that resolves to an array of wallet providers.
|
|
367
|
+
* @throws {ZeroXKeyError} If the wallet manager is uninitialized or provider retrieval fails.
|
|
368
|
+
*/
|
|
369
|
+
this.fetchWalletProviders = async (chain) => {
|
|
370
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
371
|
+
if (!this.walletManager) {
|
|
372
|
+
throw new ZeroXKeyError("Wallet manager is not initialized", ZeroXKeyErrorCodes.WALLET_MANAGER_COMPONENT_NOT_INITIALIZED);
|
|
373
|
+
}
|
|
374
|
+
return await this.walletManager.getProviders(chain);
|
|
375
|
+
}, {
|
|
376
|
+
errorMessage: "Unable to get wallet providers",
|
|
377
|
+
errorCode: ZeroXKeyErrorCodes.FETCH_WALLETS_ERROR,
|
|
378
|
+
});
|
|
379
|
+
};
|
|
380
|
+
/**
|
|
381
|
+
* Connects the specified wallet account.
|
|
382
|
+
*
|
|
383
|
+
* - Requires the wallet manager and its connector to be initialized.
|
|
384
|
+
*
|
|
385
|
+
* @param walletProvider - wallet provider to connect.
|
|
386
|
+
* @returns A promise that resolves with the connected wallet's address.
|
|
387
|
+
* @throws {ZeroXKeyError} If the wallet manager is uninitialized or the connection fails.
|
|
388
|
+
*/
|
|
389
|
+
this.connectWalletAccount = async (walletProvider) => {
|
|
390
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
391
|
+
if (!this.walletManager?.connector) {
|
|
392
|
+
throw new ZeroXKeyError("Wallet connector is not initialized", ZeroXKeyErrorCodes.WALLET_MANAGER_COMPONENT_NOT_INITIALIZED);
|
|
393
|
+
}
|
|
394
|
+
return await this.walletManager.connector.connectWalletAccount(walletProvider);
|
|
395
|
+
}, {
|
|
396
|
+
errorMessage: "Unable to connect wallet account",
|
|
397
|
+
errorCode: ZeroXKeyErrorCodes.CONNECT_WALLET_ACCOUNT_ERROR,
|
|
398
|
+
customErrorsByMessages: {
|
|
399
|
+
"WalletConnect: The connection request has expired. Please scan the QR code again.": {
|
|
400
|
+
message: "Your WalletConnect session expired. Please scan the QR code again.",
|
|
401
|
+
code: ZeroXKeyErrorCodes.WALLET_CONNECT_EXPIRED,
|
|
402
|
+
},
|
|
403
|
+
"User rejected the request": {
|
|
404
|
+
message: "Connect wallet was cancelled by the user.",
|
|
405
|
+
code: ZeroXKeyErrorCodes.CONNECT_WALLET_CANCELLED,
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
};
|
|
410
|
+
/**
|
|
411
|
+
* Disconnects the specified wallet account.
|
|
412
|
+
*
|
|
413
|
+
* - Requires the wallet manager and its connector to be initialized.
|
|
414
|
+
*
|
|
415
|
+
* @param walletProvider - wallet provider to disconnect.
|
|
416
|
+
* @returns A promise that resolves once the wallet account is disconnected.
|
|
417
|
+
* @throws {ZeroXKeyError} If the wallet manager is uninitialized or the disconnection fails.
|
|
418
|
+
*/
|
|
419
|
+
this.disconnectWalletAccount = async (walletProvider) => {
|
|
420
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
421
|
+
if (!this.walletManager?.connector) {
|
|
422
|
+
throw new ZeroXKeyError("Wallet connector is not initialized", ZeroXKeyErrorCodes.WALLET_MANAGER_COMPONENT_NOT_INITIALIZED);
|
|
423
|
+
}
|
|
424
|
+
await this.walletManager.connector.disconnectWalletAccount(walletProvider);
|
|
425
|
+
}, {
|
|
426
|
+
errorMessage: "Unable to disconnect wallet account",
|
|
427
|
+
errorCode: ZeroXKeyErrorCodes.DISCONNECT_WALLET_ACCOUNT_ERROR,
|
|
428
|
+
});
|
|
429
|
+
};
|
|
430
|
+
/**
|
|
431
|
+
* Switches the wallet provider associated with a given wallet account
|
|
432
|
+
* to a different chain.
|
|
433
|
+
*
|
|
434
|
+
* - Requires the wallet manager and its connector to be initialized
|
|
435
|
+
* - Only works for connected wallet accounts
|
|
436
|
+
* - Looks up the provider for the given account address
|
|
437
|
+
* - Does nothing if the provider is already on the desired chain.
|
|
438
|
+
*
|
|
439
|
+
* @param params.walletAccount - The wallet account whose provider should be switched.
|
|
440
|
+
* @param params.chainOrId - The target chain, specified as a chain ID string or a SwitchableChain object.
|
|
441
|
+
* @param params.walletProviders - Optional list of wallet providers to search; falls back to `fetchWalletProviders()` if omitted.
|
|
442
|
+
* @returns A promise that resolves once the chain switch is complete.
|
|
443
|
+
*
|
|
444
|
+
* @throws {ZeroXKeyError} If the wallet manager is uninitialized, the provider is not connected, or the switch fails.
|
|
445
|
+
*/
|
|
446
|
+
this.switchWalletAccountChain = async (params) => {
|
|
447
|
+
const { walletAccount, chainOrId, walletProviders } = params;
|
|
448
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
449
|
+
if (!this.walletManager?.connector) {
|
|
450
|
+
throw new ZeroXKeyError("Wallet connector is not initialized", ZeroXKeyErrorCodes.WALLET_MANAGER_COMPONENT_NOT_INITIALIZED);
|
|
451
|
+
}
|
|
452
|
+
if (walletAccount.source === WalletSource.Embedded) {
|
|
453
|
+
throw new ZeroXKeyError("You can only switch chains for connected wallet accounts", ZeroXKeyErrorCodes.NOT_FOUND);
|
|
454
|
+
}
|
|
455
|
+
const providers = walletProviders ?? (await this.fetchWalletProviders());
|
|
456
|
+
const walletProvider = findWalletProviderFromAddress(walletAccount.address, providers);
|
|
457
|
+
if (!walletProvider) {
|
|
458
|
+
throw new ZeroXKeyError("Wallet provider not found", ZeroXKeyErrorCodes.SWITCH_WALLET_CHAIN_ERROR);
|
|
459
|
+
}
|
|
460
|
+
// if the wallet provider is already on the desired chain, do nothing
|
|
461
|
+
if (walletProvider.chainInfo.namespace === chainOrId) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
await this.walletManager.connector.switchChain(walletProvider, chainOrId);
|
|
465
|
+
}, {
|
|
466
|
+
errorMessage: "Unable to switch wallet account chain",
|
|
467
|
+
errorCode: ZeroXKeyErrorCodes.SWITCH_WALLET_CHAIN_ERROR,
|
|
468
|
+
});
|
|
469
|
+
};
|
|
470
|
+
/**
|
|
471
|
+
* Builds and signs a wallet login request without submitting it to ZeroXKey.
|
|
472
|
+
*
|
|
473
|
+
* - This function prepares a signed request for wallet authentication, which can later be used
|
|
474
|
+
* to log in or sign up a user with ZeroXKey.
|
|
475
|
+
* - It initializes the wallet stamper, ensures a valid session public key (generating one if needed),
|
|
476
|
+
* and signs the login intent with the connected wallet.
|
|
477
|
+
* - For Ethereum wallets, derives the public key from the stamped request header.
|
|
478
|
+
* - For Solana wallets, retrieves the public key directly from the connected wallet.
|
|
479
|
+
* - The signed request is not sent to ZeroXKey immediately; it is meant to be used in a subsequent flow
|
|
480
|
+
* (e.g., `loginOrSignupWithWallet`) where sub-organization existence is verified or created first.
|
|
481
|
+
*
|
|
482
|
+
* @param params.walletProvider - the wallet provider used for authentication and signing.
|
|
483
|
+
* @param params.publicKey - optional pre-generated session public key (auto-generated if not provided).
|
|
484
|
+
* @param params.expirationSeconds - optional session expiration time in seconds (defaults to the configured default).
|
|
485
|
+
* @returns A promise resolving to an object containing:
|
|
486
|
+
* - `signedRequest`: the signed wallet login request.
|
|
487
|
+
* - `publicKey`: the public key associated with the signed request.
|
|
488
|
+
* @throws {ZeroXKeyError} If the wallet stamper is not initialized, the signing process fails,
|
|
489
|
+
* or the public key cannot be derived or generated.
|
|
490
|
+
*/
|
|
491
|
+
this.buildWalletLoginRequest = async (params) => {
|
|
492
|
+
const { walletProvider, publicKey: providedPublicKey } = params;
|
|
493
|
+
const expirationSeconds = params.expirationSeconds || DEFAULT_SESSION_EXPIRATION_IN_SECONDS;
|
|
494
|
+
let generatedPublicKey = undefined;
|
|
495
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
496
|
+
if (!this.walletManager?.stamper) {
|
|
497
|
+
throw new ZeroXKeyError("Wallet stamper is not initialized", ZeroXKeyErrorCodes.WALLET_MANAGER_COMPONENT_NOT_INITIALIZED);
|
|
498
|
+
}
|
|
499
|
+
const futureSessionPublicKey = providedPublicKey ??
|
|
500
|
+
(generatedPublicKey = await this.apiKeyStamper?.createKeyPair());
|
|
501
|
+
if (!futureSessionPublicKey) {
|
|
502
|
+
throw new ZeroXKeyError("Failed to find or generate a public key for building the wallet login request", ZeroXKeyErrorCodes.WALLET_BUILD_LOGIN_REQUEST_ERROR);
|
|
503
|
+
}
|
|
504
|
+
this.walletManager.stamper.setProvider(walletProvider.interfaceType, walletProvider);
|
|
505
|
+
// here we sign the request with the wallet, but we don't send it to ZeroXKey yet
|
|
506
|
+
// this is because we need to check if the subOrg exists first, and create one if it doesn't
|
|
507
|
+
// once we have the subOrg for the publicKey, we then can send the request to ZeroXKey
|
|
508
|
+
const signedRequest = await withZeroXKeyErrorHandling(async () => {
|
|
509
|
+
return this.httpClient.stampStampLogin({
|
|
510
|
+
publicKey: futureSessionPublicKey,
|
|
511
|
+
organizationId: this.config.organizationId,
|
|
512
|
+
expirationSeconds,
|
|
513
|
+
}, StamperType.Wallet);
|
|
514
|
+
}, {
|
|
515
|
+
errorMessage: "Failed to create stamped request for wallet login",
|
|
516
|
+
errorCode: ZeroXKeyErrorCodes.WALLET_BUILD_LOGIN_REQUEST_ERROR,
|
|
517
|
+
customErrorsByMessages: {
|
|
518
|
+
"WalletConnect: The connection request has expired. Please scan the QR code again.": {
|
|
519
|
+
message: "Your WalletConnect session expired. Please scan the QR code again.",
|
|
520
|
+
code: ZeroXKeyErrorCodes.WALLET_CONNECT_EXPIRED,
|
|
521
|
+
},
|
|
522
|
+
"Failed to sign the message": {
|
|
523
|
+
message: "Wallet auth was cancelled by the user.",
|
|
524
|
+
code: ZeroXKeyErrorCodes.CONNECT_WALLET_CANCELLED,
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
});
|
|
528
|
+
if (!signedRequest) {
|
|
529
|
+
throw new ZeroXKeyError("Failed to create stamped request for wallet login", ZeroXKeyErrorCodes.BAD_RESPONSE);
|
|
530
|
+
}
|
|
531
|
+
// the wallet's public key is embedded in the stamp header by the wallet stamper
|
|
532
|
+
// so we extract it from there
|
|
533
|
+
const publicKey = getPublicKeyFromStampHeader(signedRequest.stamp.stampHeaderValue);
|
|
534
|
+
return {
|
|
535
|
+
signedRequest,
|
|
536
|
+
publicKey,
|
|
537
|
+
};
|
|
538
|
+
}, {
|
|
539
|
+
errorCode: ZeroXKeyErrorCodes.WALLET_BUILD_LOGIN_REQUEST_ERROR,
|
|
540
|
+
errorMessage: "Failed to build wallet login request",
|
|
541
|
+
catchFn: async () => {
|
|
542
|
+
if (generatedPublicKey) {
|
|
543
|
+
try {
|
|
544
|
+
await this.apiKeyStamper?.deleteKeyPair(generatedPublicKey);
|
|
545
|
+
}
|
|
546
|
+
catch (cleanupError) {
|
|
547
|
+
throw new ZeroXKeyError(`Failed to clean up generated key pair`, ZeroXKeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
});
|
|
552
|
+
};
|
|
553
|
+
/**
|
|
554
|
+
* Logs in a user using the specified wallet provider.
|
|
555
|
+
*
|
|
556
|
+
* - This function logs in a user by authenticating with the provided wallet provider via a wallet-based signature.
|
|
557
|
+
* - If a public key is not provided, a new one will be generated for authentication.
|
|
558
|
+
* - Optionally accepts a custom session key and session expiration time.
|
|
559
|
+
* - Stores the resulting session token under the specified session key, or the default session key if not provided.
|
|
560
|
+
* - Throws an error if a public key cannot be found or generated, or if the login process fails.
|
|
561
|
+
*
|
|
562
|
+
* @param params.walletProvider - wallet provider to use for authentication.
|
|
563
|
+
* @param params.publicKey - optional public key to associate with the session (generated if not provided).
|
|
564
|
+
* @param params.sessionKey - optional key to store the session under (defaults to the default session key).
|
|
565
|
+
* @param params.expirationSeconds - optional session expiration time in seconds (defaults to the configured default).
|
|
566
|
+
* @param params.organizationId - organization ID to target (defaults to the session's organization ID or the parent organization ID).
|
|
567
|
+
* @returns A promise that resolves to a {@link WalletAuthResult}, which includes:
|
|
568
|
+
* - `sessionToken`: the signed JWT session token.
|
|
569
|
+
* - `address`: the authenticated wallet address.
|
|
570
|
+
* @throws {ZeroXKeyError} If the wallet stamper is uninitialized, a public key cannot be found or generated, or login fails.
|
|
571
|
+
*/
|
|
572
|
+
this.loginWithWallet = async (params) => {
|
|
573
|
+
const { walletProvider, sessionKey = SessionKey.DefaultSessionkey } = params;
|
|
574
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
575
|
+
const { signedRequest, publicKey } = await this.buildWalletLoginRequest(params);
|
|
576
|
+
const sessionResponse = await this.httpClient.sendSignedRequest(signedRequest);
|
|
577
|
+
const sessionToken = sessionResponse.session;
|
|
578
|
+
if (!sessionToken) {
|
|
579
|
+
throw new ZeroXKeyError("Session token not found in the response", ZeroXKeyErrorCodes.BAD_RESPONSE);
|
|
580
|
+
}
|
|
581
|
+
await this.storeSession({
|
|
582
|
+
sessionToken: sessionResponse.session,
|
|
583
|
+
sessionKey,
|
|
584
|
+
});
|
|
585
|
+
return {
|
|
586
|
+
sessionToken: sessionResponse.session,
|
|
587
|
+
address: addressFromPublicKey(walletProvider.chainInfo.namespace, publicKey),
|
|
588
|
+
};
|
|
589
|
+
}, {
|
|
590
|
+
errorMessage: "Unable to log in with the provided wallet",
|
|
591
|
+
errorCode: ZeroXKeyErrorCodes.WALLET_LOGIN_AUTH_ERROR,
|
|
592
|
+
});
|
|
593
|
+
};
|
|
594
|
+
/**
|
|
595
|
+
* Signs up a user using a wallet, creating a new sub-organization and session.
|
|
596
|
+
*
|
|
597
|
+
* - This function creates a new wallet authenticator and uses it to register a new sub-organization for the user.
|
|
598
|
+
* - Handles both wallet authentication and sub-organization creation in a single flow.
|
|
599
|
+
* - Optionally accepts additional sub-organization parameters, a custom session key, and a custom session expiration.
|
|
600
|
+
* - Automatically generates additional API key pairs for authentication and session management.
|
|
601
|
+
* - Stores the resulting session token under the specified session key, or the default session key if not provided, and manages cleanup of unused key pairs.
|
|
602
|
+
*
|
|
603
|
+
* @param params.walletProvider - wallet provider to use for authentication.
|
|
604
|
+
* @param params.createSubOrgParams - parameters for creating a sub-organization (e.g., authenticators, user metadata).
|
|
605
|
+
* @param params.sessionKey - session key to use for storing the session (defaults to the default session key).
|
|
606
|
+
* @param params.expirationSeconds - session expiration time in seconds (defaults to the configured default).
|
|
607
|
+
* @param params.organizationId - organization ID to target (defaults to the session's organization ID or the parent organization ID).
|
|
608
|
+
* @returns A promise that resolves to a {@link WalletAuthResult}, which includes:
|
|
609
|
+
* - `sessionToken`: the signed JWT session token.
|
|
610
|
+
* - `address`: the authenticated wallet address.
|
|
611
|
+
* @throws {ZeroXKeyError} If there is an error during wallet authentication, sub-organization creation, session storage, or cleanup.
|
|
612
|
+
*/
|
|
613
|
+
this.signUpWithWallet = async (params) => {
|
|
614
|
+
const { walletProvider, createSubOrgParams, sessionKey = SessionKey.DefaultSessionkey, } = params;
|
|
615
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
616
|
+
const { signedRequest, publicKey } = await this.buildWalletLoginRequest(params);
|
|
617
|
+
const signUpBody = buildSignUpBody({
|
|
618
|
+
createSubOrgParams: {
|
|
619
|
+
...createSubOrgParams,
|
|
620
|
+
apiKeys: [
|
|
621
|
+
{
|
|
622
|
+
apiKeyName: `wallet-auth:${publicKey}`,
|
|
623
|
+
publicKey: publicKey,
|
|
624
|
+
curveType: getCurveTypeFromProvider(walletProvider),
|
|
625
|
+
},
|
|
626
|
+
],
|
|
627
|
+
},
|
|
628
|
+
});
|
|
629
|
+
const res = await this.httpClient.proxySignup(signUpBody);
|
|
630
|
+
if (!res) {
|
|
631
|
+
throw new ZeroXKeyError(`Sign up failed`, ZeroXKeyErrorCodes.WALLET_SIGNUP_AUTH_ERROR);
|
|
632
|
+
}
|
|
633
|
+
// now we can send the stamped request to ZeroXKey
|
|
634
|
+
const sessionResponse = await this.httpClient.sendSignedRequest(signedRequest);
|
|
635
|
+
const sessionToken = sessionResponse.session;
|
|
636
|
+
if (!sessionToken) {
|
|
637
|
+
throw new ZeroXKeyError("Session token not found in the response", ZeroXKeyErrorCodes.BAD_RESPONSE);
|
|
638
|
+
}
|
|
639
|
+
await this.storeSession({
|
|
640
|
+
sessionToken: sessionToken,
|
|
641
|
+
sessionKey,
|
|
642
|
+
});
|
|
643
|
+
return {
|
|
644
|
+
sessionToken: sessionToken,
|
|
645
|
+
appProofs: res.appProofs,
|
|
646
|
+
address: addressFromPublicKey(walletProvider.chainInfo.namespace, publicKey),
|
|
647
|
+
};
|
|
648
|
+
}, {
|
|
649
|
+
errorMessage: "Failed to sign up with wallet",
|
|
650
|
+
errorCode: ZeroXKeyErrorCodes.WALLET_SIGNUP_AUTH_ERROR,
|
|
651
|
+
});
|
|
652
|
+
};
|
|
653
|
+
/**
|
|
654
|
+
* Logs in an existing user or signs up a new user using a wallet, creating a new sub-organization if needed.
|
|
655
|
+
*
|
|
656
|
+
* - This function attempts to log in the user by stamping a login request with the provided wallet.
|
|
657
|
+
* - If the wallet’s public key is not associated with an existing sub-organization, a new one is created.
|
|
658
|
+
* - Handles both wallet authentication and sub-organization creation in a single flow.
|
|
659
|
+
* - For Ethereum wallets, derives the public key from the signed request header; for Solana wallets, retrieves it directly from the wallet.
|
|
660
|
+
* - Optionally accepts additional sub-organization parameters, a custom session key, and a custom session expiration.
|
|
661
|
+
* - Stores the resulting session token under the specified session key, or the default session key if not provided.
|
|
662
|
+
*
|
|
663
|
+
* @param params.walletProvider - wallet provider to use for authentication.
|
|
664
|
+
* @param params.publicKey - optional public key to associate with the session (generated if not provided).
|
|
665
|
+
* @param params.createSubOrgParams - optional parameters for creating a sub-organization (e.g., authenticators, user metadata).
|
|
666
|
+
* @param params.sessionKey - session key to use for storing the session (defaults to the default session key).
|
|
667
|
+
* @param params.expirationSeconds - session expiration time in seconds (defaults to the configured default).
|
|
668
|
+
* @param params.organizationId - organization ID to target (defaults to the session's organization ID or the parent organization ID).
|
|
669
|
+
* @returns A promise that resolves to an object containing:
|
|
670
|
+
* - `sessionToken`: the signed JWT session token.
|
|
671
|
+
* - `address`: the authenticated wallet address.
|
|
672
|
+
* - `action`: whether the flow resulted in a login or signup ({@link AuthAction}).
|
|
673
|
+
* @throws {ZeroXKeyError} If there is an error during wallet authentication, sub-organization creation, or session storage.
|
|
674
|
+
*/
|
|
675
|
+
this.loginOrSignupWithWallet = async (params) => {
|
|
676
|
+
const { walletProvider, createSubOrgParams, sessionKey = SessionKey.DefaultSessionkey, } = params;
|
|
677
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
678
|
+
const { signedRequest, publicKey } = await this.buildWalletLoginRequest(params);
|
|
679
|
+
// here we check if the subOrg exists and create one
|
|
680
|
+
// then we send off the stamped request to ZeroXKey
|
|
681
|
+
const accountRes = await this.httpClient.proxyGetAccount({
|
|
682
|
+
filterType: FilterType.PublicKey,
|
|
683
|
+
filterValue: publicKey,
|
|
684
|
+
});
|
|
685
|
+
if (!accountRes) {
|
|
686
|
+
throw new ZeroXKeyError(`Account fetch failed`, ZeroXKeyErrorCodes.ACCOUNT_FETCH_ERROR);
|
|
687
|
+
}
|
|
688
|
+
const subOrganizationId = accountRes.organizationId;
|
|
689
|
+
// if there is no subOrganizationId, we create one
|
|
690
|
+
let signupRes;
|
|
691
|
+
if (!subOrganizationId) {
|
|
692
|
+
const signUpBody = buildSignUpBody({
|
|
693
|
+
createSubOrgParams: {
|
|
694
|
+
...createSubOrgParams,
|
|
695
|
+
apiKeys: [
|
|
696
|
+
{
|
|
697
|
+
apiKeyName: `wallet-auth:${publicKey}`,
|
|
698
|
+
publicKey: publicKey,
|
|
699
|
+
curveType: getCurveTypeFromProvider(walletProvider),
|
|
700
|
+
},
|
|
701
|
+
],
|
|
702
|
+
},
|
|
703
|
+
});
|
|
704
|
+
signupRes = await this.httpClient.proxySignup(signUpBody);
|
|
705
|
+
if (!signupRes) {
|
|
706
|
+
throw new ZeroXKeyError(`Sign up failed`, ZeroXKeyErrorCodes.WALLET_SIGNUP_AUTH_ERROR);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
// now we can send the stamped request to ZeroXKey
|
|
710
|
+
const sessionResponse = await this.httpClient.sendSignedRequest(signedRequest);
|
|
711
|
+
const sessionToken = sessionResponse.session;
|
|
712
|
+
if (!sessionToken) {
|
|
713
|
+
throw new ZeroXKeyError("Session token not found in the response", ZeroXKeyErrorCodes.BAD_RESPONSE);
|
|
714
|
+
}
|
|
715
|
+
await this.storeSession({
|
|
716
|
+
sessionToken: sessionToken,
|
|
717
|
+
sessionKey,
|
|
718
|
+
});
|
|
719
|
+
return {
|
|
720
|
+
sessionToken: sessionToken,
|
|
721
|
+
appProofs: signupRes?.appProofs,
|
|
722
|
+
address: addressFromPublicKey(walletProvider.chainInfo.namespace, publicKey),
|
|
723
|
+
// if the subOrganizationId exists, it means the user is logging in
|
|
724
|
+
action: subOrganizationId ? AuthAction.LOGIN : AuthAction.SIGNUP,
|
|
725
|
+
};
|
|
726
|
+
}, {
|
|
727
|
+
errorCode: ZeroXKeyErrorCodes.WALLET_LOGIN_OR_SIGNUP_ERROR,
|
|
728
|
+
errorMessage: "Failed to log in or sign up with wallet",
|
|
729
|
+
});
|
|
730
|
+
};
|
|
731
|
+
/**
|
|
732
|
+
* Initializes the OTP process by sending an OTP code to the provided contact.
|
|
733
|
+
*
|
|
734
|
+
* - This function initiates the OTP flow by sending a one-time password (OTP) code to the user's contact information (email address or phone number) via the auth proxy.
|
|
735
|
+
* - Supports both email and SMS OTP types.
|
|
736
|
+
* - Returns an OTP ID that is required for subsequent OTP verification.
|
|
737
|
+
*
|
|
738
|
+
* @param params.otpType - type of OTP to initialize (OtpType.Email or OtpType.Sms).
|
|
739
|
+
* @param params.contact - contact information for the user (e.g., email address or phone number).
|
|
740
|
+
* @param params.organizationId - optional organization ID to target (defaults to the session's organization ID or the parent organization ID).
|
|
741
|
+
* @returns A promise that resolves to the OTP ID required for verification.
|
|
742
|
+
* @throws {ZeroXKeyError} If there is an error during the OTP initialization process or if the maximum number of OTPs has been reached.
|
|
743
|
+
*/
|
|
744
|
+
this.initOtp = async (params) => {
|
|
745
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
746
|
+
const initOtpRes = await this.httpClient.proxyInitOtp(params);
|
|
747
|
+
if (!initOtpRes ||
|
|
748
|
+
!initOtpRes.otpId ||
|
|
749
|
+
!initOtpRes.otpEncryptionTargetBundle) {
|
|
750
|
+
throw new ZeroXKeyError("Failed to initialize OTP: otpId or otpEncryptionTargetBundle is missing", ZeroXKeyErrorCodes.INIT_OTP_ERROR);
|
|
751
|
+
}
|
|
752
|
+
return {
|
|
753
|
+
otpId: initOtpRes.otpId,
|
|
754
|
+
otpEncryptionTargetBundle: initOtpRes.otpEncryptionTargetBundle,
|
|
755
|
+
};
|
|
756
|
+
}, {
|
|
757
|
+
errorMessage: "Failed to initialize OTP",
|
|
758
|
+
errorCode: ZeroXKeyErrorCodes.INIT_OTP_ERROR,
|
|
759
|
+
customErrorsByMessages: {
|
|
760
|
+
"Max number of OTPs have been initiated": {
|
|
761
|
+
message: "Maximum number of OTPs has been reached for this contact.",
|
|
762
|
+
code: ZeroXKeyErrorCodes.MAX_OTP_INITIATED_ERROR,
|
|
763
|
+
},
|
|
764
|
+
},
|
|
765
|
+
});
|
|
766
|
+
};
|
|
767
|
+
/**
|
|
768
|
+
* Verifies the OTP code sent to the user.
|
|
769
|
+
*
|
|
770
|
+
* - This function verifies the OTP code entered by the user against the OTP sent to their contact information (email or phone) using the auth proxy.
|
|
771
|
+
* - If verification is successful, it returns the sub-organization ID associated with the contact (if it exists) and a verification token.
|
|
772
|
+
* - The verification token can be used for subsequent login or sign-up flows.
|
|
773
|
+
* - Handles both email and SMS OTP types.
|
|
774
|
+
*
|
|
775
|
+
* @param params.otpId - ID of the OTP to verify (returned from `initOtp`).
|
|
776
|
+
* @param params.otpCode - OTP code entered by the user.
|
|
777
|
+
* @param params.contact - contact information for the user (e.g., email address or phone number).
|
|
778
|
+
* @param params.otpType - type of OTP being verified (OtpType.Email or OtpType.Sms).
|
|
779
|
+
* @param params.publicKey - public key the verification token is bound to for ownership verification (client signature verification during login/signup). This public key is optional; if not provided, a new key pair will be generated.
|
|
780
|
+
* @returns A promise that resolves to an object containing:
|
|
781
|
+
* - subOrganizationId: sub-organization ID if the contact is already associated with a sub-organization, or an empty string if not.
|
|
782
|
+
* - verificationToken: verification token to be used for login or sign-up.
|
|
783
|
+
* @throws {ZeroXKeyError} If there is an error during the OTP verification process, such as an invalid code or network failure.
|
|
784
|
+
*/
|
|
785
|
+
this.verifyOtp = async (params) => {
|
|
786
|
+
const { otpId, otpCode, otpEncryptionTargetBundle, contact, otpType } = params;
|
|
787
|
+
const resolvedPublicKey = params.publicKey ?? (await this.apiKeyStamper?.createKeyPair());
|
|
788
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
789
|
+
const encryptedOtpBundle = encryptOtpAttemptBundle({
|
|
790
|
+
otpCode,
|
|
791
|
+
publicKey: resolvedPublicKey,
|
|
792
|
+
otpEncryptionTargetBundle,
|
|
793
|
+
});
|
|
794
|
+
const verifyOtpRes = await this.httpClient.proxyVerifyOtp({
|
|
795
|
+
otpId,
|
|
796
|
+
encryptedOtpBundle,
|
|
797
|
+
});
|
|
798
|
+
if (!verifyOtpRes) {
|
|
799
|
+
throw new ZeroXKeyError(`OTP verification failed`, ZeroXKeyErrorCodes.INTERNAL_ERROR);
|
|
800
|
+
}
|
|
801
|
+
const accountRes = await this.httpClient.proxyGetAccount({
|
|
802
|
+
filterType: OtpTypeToFilterTypeMap[otpType],
|
|
803
|
+
filterValue: contact,
|
|
804
|
+
verificationToken: verifyOtpRes.verificationToken,
|
|
805
|
+
});
|
|
806
|
+
if (!accountRes) {
|
|
807
|
+
throw new ZeroXKeyError(`Account fetch failed`, ZeroXKeyErrorCodes.ACCOUNT_FETCH_ERROR);
|
|
808
|
+
}
|
|
809
|
+
const subOrganizationId = accountRes.organizationId;
|
|
810
|
+
return {
|
|
811
|
+
subOrganizationId: subOrganizationId,
|
|
812
|
+
verificationToken: verifyOtpRes.verificationToken,
|
|
813
|
+
};
|
|
814
|
+
}, {
|
|
815
|
+
errorMessage: "Failed to verify OTP",
|
|
816
|
+
errorCode: ZeroXKeyErrorCodes.VERIFY_OTP_ERROR,
|
|
817
|
+
customErrorsByMessages: {
|
|
818
|
+
"Invalid OTP code": {
|
|
819
|
+
message: "The provided OTP code is invalid.",
|
|
820
|
+
code: ZeroXKeyErrorCodes.INVALID_OTP_CODE,
|
|
821
|
+
},
|
|
822
|
+
},
|
|
823
|
+
});
|
|
824
|
+
};
|
|
825
|
+
/**
|
|
826
|
+
* Logs in a user using an OTP verification token.
|
|
827
|
+
*
|
|
828
|
+
* - This function logs in a user using the verification token received after OTP verification (from email or SMS).
|
|
829
|
+
* - If a public key is not provided, a new API key pair will be generated for authentication.
|
|
830
|
+
* - Optionally invalidates any existing sessions for the user if `invalidateExisting` is set to true.
|
|
831
|
+
* - Stores the resulting session token under the specified session key, or the default session key if not provided.
|
|
832
|
+
* - Handles cleanup of unused key pairs if login fails.
|
|
833
|
+
*
|
|
834
|
+
* @param params.verificationToken - verification token received after OTP verification.
|
|
835
|
+
* @param params.publicKey - public key to use for authentication. If not provided, a new key pair will be generated.
|
|
836
|
+
* @param params.organizationId - optional organization ID to target (defaults to the verified subOrg ID linked to the verification token contact).
|
|
837
|
+
* @param params.invalidateExisting - flag to invalidate existing session for the user.
|
|
838
|
+
* @param params.sessionKey - session key to use for session creation (defaults to the default session key).
|
|
839
|
+
* @returns A promise that resolves to a {@link BaseAuthResult}, which includes:
|
|
840
|
+
* - `sessionToken`: the signed JWT session token.
|
|
841
|
+
* @throws {ZeroXKeyError} If there is an error during the OTP login process or if key pair cleanup fails.
|
|
842
|
+
*/
|
|
843
|
+
this.loginWithOtp = async (params) => {
|
|
844
|
+
const { verificationToken, invalidateExisting = false, publicKey = await this.apiKeyStamper?.createKeyPair(), organizationId, sessionKey = SessionKey.DefaultSessionkey, } = params;
|
|
845
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
846
|
+
const { message, publicKey: clientSignaturePublicKey } = getClientSignatureMessageForLogin({
|
|
847
|
+
verificationToken,
|
|
848
|
+
sessionPublicKey: publicKey,
|
|
849
|
+
});
|
|
850
|
+
this.apiKeyStamper?.setTemporaryPublicKey(publicKey);
|
|
851
|
+
const signature = await this.apiKeyStamper?.sign(message, SignatureFormat.Raw);
|
|
852
|
+
if (!signature) {
|
|
853
|
+
throw new ZeroXKeyError(`Failed to sign client signature for OTP login`, ZeroXKeyErrorCodes.INTERNAL_ERROR);
|
|
854
|
+
}
|
|
855
|
+
const clientSignature = {
|
|
856
|
+
message: message,
|
|
857
|
+
publicKey: clientSignaturePublicKey,
|
|
858
|
+
scheme: "CLIENT_SIGNATURE_SCHEME_API_P256",
|
|
859
|
+
signature: signature,
|
|
860
|
+
};
|
|
861
|
+
const res = await this.httpClient.proxyOtpLogin({
|
|
862
|
+
verificationToken,
|
|
863
|
+
publicKey: publicKey,
|
|
864
|
+
invalidateExisting,
|
|
865
|
+
clientSignature,
|
|
866
|
+
...(organizationId && { organizationId }),
|
|
867
|
+
});
|
|
868
|
+
if (!res) {
|
|
869
|
+
throw new ZeroXKeyError(`Auth proxy OTP login failed`, ZeroXKeyErrorCodes.OTP_LOGIN_ERROR);
|
|
870
|
+
}
|
|
871
|
+
const loginRes = await res;
|
|
872
|
+
if (!loginRes.session) {
|
|
873
|
+
throw new ZeroXKeyError("No session returned from OTP login", ZeroXKeyErrorCodes.OTP_LOGIN_ERROR);
|
|
874
|
+
}
|
|
875
|
+
await this.storeSession({
|
|
876
|
+
sessionToken: loginRes.session,
|
|
877
|
+
sessionKey,
|
|
878
|
+
});
|
|
879
|
+
return {
|
|
880
|
+
sessionToken: loginRes.session,
|
|
881
|
+
};
|
|
882
|
+
}, {
|
|
883
|
+
errorMessage: "Failed to log in with OTP",
|
|
884
|
+
errorCode: ZeroXKeyErrorCodes.OTP_LOGIN_ERROR,
|
|
885
|
+
catchFn: async () => {
|
|
886
|
+
// Clean up the generated key pair if it wasn't successfully used
|
|
887
|
+
if (publicKey) {
|
|
888
|
+
try {
|
|
889
|
+
await this.apiKeyStamper?.deleteKeyPair(publicKey);
|
|
890
|
+
}
|
|
891
|
+
catch (cleanupError) {
|
|
892
|
+
throw new ZeroXKeyError(`Failed to clean up generated key pair`, ZeroXKeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
},
|
|
896
|
+
}, {
|
|
897
|
+
finallyFn: async () => {
|
|
898
|
+
this.apiKeyStamper?.clearTemporaryPublicKey();
|
|
899
|
+
},
|
|
900
|
+
});
|
|
901
|
+
};
|
|
902
|
+
/**
|
|
903
|
+
* Signs up a user using an OTP verification token.
|
|
904
|
+
*
|
|
905
|
+
* - This function signs up a user using the verification token received after OTP verification (from email or SMS).
|
|
906
|
+
* - Creates a new sub-organization for the user with the provided parameters and associates the contact (email or phone) with the sub-organization.
|
|
907
|
+
* - Automatically generates a new API key pair for authentication and session management.
|
|
908
|
+
* - Stores the resulting session token under the specified session key, or the default session key if not provided.
|
|
909
|
+
* - Handles both email and SMS OTP types, and supports additional sub-organization creation parameters.
|
|
910
|
+
*
|
|
911
|
+
* @param params.verificationToken - verification token received after OTP verification.
|
|
912
|
+
* @param params.contact - contact information for the user (e.g., email address or phone number).
|
|
913
|
+
* @param params.otpType - type of OTP being used (OtpType.Email or OtpType.Sms).
|
|
914
|
+
* @param params.createSubOrgParams - parameters for creating a sub-organization (e.g., authenticators, user metadata).
|
|
915
|
+
* @param params.invalidateExisting - flag to invalidate existing session for the user.
|
|
916
|
+
* @param params.sessionKey - session key to use for session creation (defaults to the default session key).
|
|
917
|
+
* @returns A promise that resolves to a {@link BaseAuthResult}, which includes:
|
|
918
|
+
* - `sessionToken`: the signed JWT session token.
|
|
919
|
+
* @throws {ZeroXKeyError} If there is an error during the OTP sign-up process or session storage.
|
|
920
|
+
*/
|
|
921
|
+
this.signUpWithOtp = async (params) => {
|
|
922
|
+
const { verificationToken, contact, otpType, createSubOrgParams, invalidateExisting, sessionKey, publicKey = await this.apiKeyStamper?.createKeyPair(), } = params;
|
|
923
|
+
// build sign up body without client signature first
|
|
924
|
+
const signUpBody = buildSignUpBody({
|
|
925
|
+
createSubOrgParams: {
|
|
926
|
+
...createSubOrgParams,
|
|
927
|
+
...(otpType === OtpType.Email
|
|
928
|
+
? { userEmail: contact }
|
|
929
|
+
: { userPhoneNumber: contact }),
|
|
930
|
+
verificationToken,
|
|
931
|
+
},
|
|
932
|
+
});
|
|
933
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
934
|
+
const { message, publicKey: clientSignaturePublicKey } = getClientSignatureMessageForSignup({
|
|
935
|
+
verificationToken,
|
|
936
|
+
...(signUpBody.userEmail && { email: signUpBody.userEmail }),
|
|
937
|
+
...(signUpBody.userPhoneNumber && {
|
|
938
|
+
phoneNumber: signUpBody.userPhoneNumber,
|
|
939
|
+
}),
|
|
940
|
+
apiKeys: signUpBody.apiKeys,
|
|
941
|
+
authenticators: signUpBody.authenticators,
|
|
942
|
+
oauthProviders: signUpBody.oauthProviders,
|
|
943
|
+
});
|
|
944
|
+
this.apiKeyStamper?.setTemporaryPublicKey(publicKey);
|
|
945
|
+
const signature = await this.apiKeyStamper?.sign(message, SignatureFormat.Raw);
|
|
946
|
+
if (!signature) {
|
|
947
|
+
throw new ZeroXKeyError(`Failed to sign client signature for OTP sign up`, ZeroXKeyErrorCodes.INTERNAL_ERROR);
|
|
948
|
+
}
|
|
949
|
+
const clientSignature = {
|
|
950
|
+
message: message,
|
|
951
|
+
publicKey: clientSignaturePublicKey,
|
|
952
|
+
scheme: "CLIENT_SIGNATURE_SCHEME_API_P256",
|
|
953
|
+
signature: signature,
|
|
954
|
+
};
|
|
955
|
+
const signupRes = await this.httpClient.proxySignup({
|
|
956
|
+
...signUpBody,
|
|
957
|
+
clientSignature,
|
|
958
|
+
});
|
|
959
|
+
if (!signupRes) {
|
|
960
|
+
throw new ZeroXKeyError(`Auth proxy OTP sign up failed`, ZeroXKeyErrorCodes.OTP_SIGNUP_ERROR);
|
|
961
|
+
}
|
|
962
|
+
const otpRes = await this.loginWithOtp({
|
|
963
|
+
verificationToken,
|
|
964
|
+
publicKey: publicKey,
|
|
965
|
+
...(invalidateExisting && { invalidateExisting }),
|
|
966
|
+
...(sessionKey && { sessionKey }),
|
|
967
|
+
});
|
|
968
|
+
return {
|
|
969
|
+
...otpRes,
|
|
970
|
+
appProofs: signupRes.appProofs,
|
|
971
|
+
};
|
|
972
|
+
}, {
|
|
973
|
+
catchFn: async () => {
|
|
974
|
+
// Clean up the generated key pair if it wasn't successfully used
|
|
975
|
+
if (publicKey) {
|
|
976
|
+
try {
|
|
977
|
+
await this.apiKeyStamper?.deleteKeyPair(publicKey);
|
|
978
|
+
}
|
|
979
|
+
catch (cleanupError) {
|
|
980
|
+
throw new ZeroXKeyError(`Failed to clean up generated key pair`, ZeroXKeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
},
|
|
984
|
+
errorCode: ZeroXKeyErrorCodes.OTP_SIGNUP_ERROR,
|
|
985
|
+
errorMessage: "Failed to sign up with OTP",
|
|
986
|
+
}, {
|
|
987
|
+
finallyFn: async () => {
|
|
988
|
+
this.apiKeyStamper?.clearTemporaryPublicKey();
|
|
989
|
+
},
|
|
990
|
+
});
|
|
991
|
+
};
|
|
992
|
+
/**
|
|
993
|
+
* Completes the OTP authentication flow by verifying the OTP code and then either signing up or logging in the user.
|
|
994
|
+
*
|
|
995
|
+
* - This function first verifies the OTP code for the provided contact and OTP type.
|
|
996
|
+
* - If the contact is not associated with an existing sub-organization, it will automatically create a new sub-organization and complete the sign-up flow.
|
|
997
|
+
* - If the contact is already associated with a sub-organization, it will complete the login flow.
|
|
998
|
+
* - Supports passing a custom public key for authentication, invalidating existing session, specifying a session key, and providing additional sub-organization creation parameters.
|
|
999
|
+
* - Handles both email and SMS OTP types.
|
|
1000
|
+
*
|
|
1001
|
+
* @param params.otpId - ID of the OTP to complete (returned from `initOtp`).
|
|
1002
|
+
* @param params.otpCode - OTP code entered by the user.
|
|
1003
|
+
* @param params.contact - contact information for the user (e.g., email address or phone number).
|
|
1004
|
+
* @param params.otpType - type of OTP being completed (OtpType.Email or OtpType.Sms).
|
|
1005
|
+
* @param params.publicKey - public key to use for authentication. If not provided, a new key pair may be generated.
|
|
1006
|
+
* @param params.invalidateExisting - flag to invalidate existing sessions for the user.
|
|
1007
|
+
* @param params.sessionKey - session key to use for session creation (defaults to the default session key).
|
|
1008
|
+
* @param params.createSubOrgParams - parameters for sub-organization creation (e.g., authenticators, user metadata).
|
|
1009
|
+
* @returns A promise that resolves to an object containing:
|
|
1010
|
+
* - `sessionToken`: the signed JWT session token.
|
|
1011
|
+
* - `verificationToken`: the OTP verification token.
|
|
1012
|
+
* - `action`: whether the flow resulted in a login or signup ({@link AuthAction}).
|
|
1013
|
+
* @throws {ZeroXKeyError} If there is an error during OTP verification, sign-up, or login.
|
|
1014
|
+
*/
|
|
1015
|
+
this.completeOtp = async (params) => {
|
|
1016
|
+
const { otpId, otpCode, otpEncryptionTargetBundle, contact, otpType, publicKey = await this.apiKeyStamper?.createKeyPair(), invalidateExisting = false, sessionKey, createSubOrgParams, } = params;
|
|
1017
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
1018
|
+
const { subOrganizationId, verificationToken } = await this.verifyOtp({
|
|
1019
|
+
otpId: otpId,
|
|
1020
|
+
otpCode: otpCode,
|
|
1021
|
+
otpEncryptionTargetBundle,
|
|
1022
|
+
contact: contact,
|
|
1023
|
+
otpType: otpType,
|
|
1024
|
+
publicKey: publicKey,
|
|
1025
|
+
});
|
|
1026
|
+
if (!verificationToken) {
|
|
1027
|
+
throw new ZeroXKeyError("No verification token returned from OTP verification", ZeroXKeyErrorCodes.VERIFY_OTP_ERROR);
|
|
1028
|
+
}
|
|
1029
|
+
if (!subOrganizationId) {
|
|
1030
|
+
const signUpRes = await this.signUpWithOtp({
|
|
1031
|
+
verificationToken,
|
|
1032
|
+
contact,
|
|
1033
|
+
otpType,
|
|
1034
|
+
...(createSubOrgParams && { createSubOrgParams }),
|
|
1035
|
+
...(invalidateExisting && { invalidateExisting }),
|
|
1036
|
+
...(sessionKey && { sessionKey }),
|
|
1037
|
+
publicKey: publicKey,
|
|
1038
|
+
});
|
|
1039
|
+
return {
|
|
1040
|
+
...signUpRes,
|
|
1041
|
+
verificationToken,
|
|
1042
|
+
action: AuthAction.SIGNUP,
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
else {
|
|
1046
|
+
const loginRes = await this.loginWithOtp({
|
|
1047
|
+
verificationToken,
|
|
1048
|
+
publicKey: publicKey,
|
|
1049
|
+
...(invalidateExisting && { invalidateExisting }),
|
|
1050
|
+
...(sessionKey && { sessionKey }),
|
|
1051
|
+
});
|
|
1052
|
+
return {
|
|
1053
|
+
...loginRes,
|
|
1054
|
+
verificationToken,
|
|
1055
|
+
action: AuthAction.LOGIN,
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
}, {
|
|
1059
|
+
errorMessage: "Failed to complete OTP process",
|
|
1060
|
+
errorCode: ZeroXKeyErrorCodes.OTP_COMPLETION_ERROR,
|
|
1061
|
+
});
|
|
1062
|
+
};
|
|
1063
|
+
/**
|
|
1064
|
+
* Completes the OAuth authentication flow by either signing up or logging in the user, depending on whether a sub-organization already exists for the provided OIDC token.
|
|
1065
|
+
*
|
|
1066
|
+
* - This function first checks if there is an existing sub-organization associated with the OIDC token.
|
|
1067
|
+
* - If a sub-organization exists, it proceeds with the OAuth login flow.
|
|
1068
|
+
* - If no sub-organization exists, it creates a new sub-organization and completes the sign-up flow.
|
|
1069
|
+
* - Optionally accepts a custom OAuth provider name, session key, and additional sub-organization creation parameters.
|
|
1070
|
+
* - Handles session storage and management, and supports invalidating existing sessions if specified.
|
|
1071
|
+
*
|
|
1072
|
+
* @param params.oidcToken - OIDC token received after successful authentication with the OAuth provider.
|
|
1073
|
+
* @param params.publicKey - public key to use for authentication. Must be generated prior to calling this function, this is because the OIDC nonce has to be set to `sha256(publicKey)`.
|
|
1074
|
+
* @param params.providerName - name of the OAuth provider (defaults to a generated name with a timestamp).
|
|
1075
|
+
* @param params.createSubOrgParams - parameters for sub-organization creation (e.g., authenticators, user metadata).
|
|
1076
|
+
* @param params.invalidateExisting - flag to invalidate existing sessions for the user.
|
|
1077
|
+
* @param params.sessionKey - session key to use for session creation (defaults to the default session key).
|
|
1078
|
+
*
|
|
1079
|
+
* @returns A promise that resolves to an object containing:
|
|
1080
|
+
* - `sessionToken`: the signed JWT session token.
|
|
1081
|
+
* - `action`: whether the flow resulted in a login or signup ({@link AuthAction}).
|
|
1082
|
+
* @throws {ZeroXKeyError} If there is an error during the OAuth completion process, such as account lookup, sign-up, or login.
|
|
1083
|
+
*/
|
|
1084
|
+
this.completeOauth = async (params) => {
|
|
1085
|
+
const { oidcToken, publicKey, providerName, createSubOrgParams, invalidateExisting, sessionKey, } = params;
|
|
1086
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
1087
|
+
const accountRes = await this.httpClient.proxyGetAccount({
|
|
1088
|
+
filterType: "OIDC_TOKEN",
|
|
1089
|
+
filterValue: oidcToken,
|
|
1090
|
+
});
|
|
1091
|
+
if (!accountRes) {
|
|
1092
|
+
throw new ZeroXKeyError(`Account fetch failed`, ZeroXKeyErrorCodes.ACCOUNT_FETCH_ERROR);
|
|
1093
|
+
}
|
|
1094
|
+
const subOrganizationId = accountRes.organizationId;
|
|
1095
|
+
if (subOrganizationId) {
|
|
1096
|
+
const loginRes = await this.loginWithOauth({
|
|
1097
|
+
oidcToken,
|
|
1098
|
+
publicKey,
|
|
1099
|
+
...(invalidateExisting && { invalidateExisting }),
|
|
1100
|
+
...(sessionKey && { sessionKey }),
|
|
1101
|
+
});
|
|
1102
|
+
return {
|
|
1103
|
+
...loginRes,
|
|
1104
|
+
action: AuthAction.LOGIN,
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
else {
|
|
1108
|
+
const signUpRes = await this.signUpWithOauth({
|
|
1109
|
+
oidcToken,
|
|
1110
|
+
publicKey,
|
|
1111
|
+
...(providerName && {
|
|
1112
|
+
providerName,
|
|
1113
|
+
}),
|
|
1114
|
+
...(createSubOrgParams && {
|
|
1115
|
+
createSubOrgParams,
|
|
1116
|
+
}),
|
|
1117
|
+
...(invalidateExisting && { invalidateExisting }),
|
|
1118
|
+
...(sessionKey && { sessionKey }),
|
|
1119
|
+
});
|
|
1120
|
+
return {
|
|
1121
|
+
...signUpRes,
|
|
1122
|
+
action: AuthAction.SIGNUP,
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
}, {
|
|
1126
|
+
errorMessage: "Failed to complete OAuth process",
|
|
1127
|
+
errorCode: ZeroXKeyErrorCodes.OAUTH_LOGIN_ERROR,
|
|
1128
|
+
});
|
|
1129
|
+
};
|
|
1130
|
+
/**
|
|
1131
|
+
* Logs in a user using OAuth authentication.
|
|
1132
|
+
*
|
|
1133
|
+
* - This function logs in a user using the provided OIDC token and public key.
|
|
1134
|
+
* - Optionally invalidates any existing sessions for the user if `invalidateExisting` is set to true.
|
|
1135
|
+
* - Stores the resulting session token under the specified session key, or the default session key if not provided.
|
|
1136
|
+
* - Handles cleanup of unused key pairs if login fails.
|
|
1137
|
+
*
|
|
1138
|
+
* @param params.oidcToken - OIDC token received after successful authentication with the OAuth provider.
|
|
1139
|
+
* @param params.publicKey - The public key bound to the login session. This key is required because it is directly
|
|
1140
|
+
* tied to the nonce used during OIDC token generation and must match the value
|
|
1141
|
+
* encoded in the token.
|
|
1142
|
+
* @param params.organizationId - ID of the organization to target when creating the session.
|
|
1143
|
+
* @param params.invalidateExisting - flag to invalidate existing sessions for the user.
|
|
1144
|
+
* @param params.sessionKey - session key to use for session creation (defaults to the default session key).
|
|
1145
|
+
* @returns A promise that resolves to a {@link BaseAuthResult}, which includes:
|
|
1146
|
+
* - `sessionToken`: the signed JWT session token.
|
|
1147
|
+
* @throws {ZeroXKeyError} If there is an error during the OAuth login process or if key pair cleanup fails.
|
|
1148
|
+
*/
|
|
1149
|
+
this.loginWithOauth = async (params) => {
|
|
1150
|
+
const { oidcToken, publicKey, organizationId, invalidateExisting = false, sessionKey = SessionKey.DefaultSessionkey, } = params;
|
|
1151
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
1152
|
+
if (!publicKey) {
|
|
1153
|
+
throw new ZeroXKeyError("Public key must be provided to log in with OAuth. Please create a key pair first.", ZeroXKeyErrorCodes.MISSING_PARAMS);
|
|
1154
|
+
}
|
|
1155
|
+
const loginRes = await this.httpClient.proxyOAuthLogin({
|
|
1156
|
+
oidcToken,
|
|
1157
|
+
publicKey,
|
|
1158
|
+
invalidateExisting,
|
|
1159
|
+
...(organizationId && { organizationId }),
|
|
1160
|
+
});
|
|
1161
|
+
if (!loginRes) {
|
|
1162
|
+
throw new ZeroXKeyError(`Auth proxy OAuth login failed`, ZeroXKeyErrorCodes.OAUTH_LOGIN_ERROR);
|
|
1163
|
+
}
|
|
1164
|
+
if (!loginRes.session) {
|
|
1165
|
+
throw new ZeroXKeyError("No session returned from oauth login", ZeroXKeyErrorCodes.OAUTH_LOGIN_ERROR);
|
|
1166
|
+
}
|
|
1167
|
+
await this.storeSession({
|
|
1168
|
+
sessionToken: loginRes.session,
|
|
1169
|
+
sessionKey,
|
|
1170
|
+
});
|
|
1171
|
+
return { sessionToken: loginRes.session };
|
|
1172
|
+
}, {
|
|
1173
|
+
errorMessage: "Failed to complete OAuth login",
|
|
1174
|
+
errorCode: ZeroXKeyErrorCodes.OAUTH_LOGIN_ERROR,
|
|
1175
|
+
customErrorsByMessages: {
|
|
1176
|
+
"OAUTH disallowed": {
|
|
1177
|
+
message: "OAuth is disabled on the dashboard for this organization.",
|
|
1178
|
+
code: ZeroXKeyErrorCodes.AUTH_METHOD_NOT_ENABLED,
|
|
1179
|
+
},
|
|
1180
|
+
},
|
|
1181
|
+
catchFn: async () => {
|
|
1182
|
+
// Clean up the generated key pair if it wasn't successfully used
|
|
1183
|
+
if (publicKey) {
|
|
1184
|
+
try {
|
|
1185
|
+
await this.apiKeyStamper?.deleteKeyPair(publicKey);
|
|
1186
|
+
}
|
|
1187
|
+
catch (cleanupError) {
|
|
1188
|
+
throw new ZeroXKeyError(`Failed to clean up generated key pair`, ZeroXKeyErrorCodes.KEY_PAIR_CLEANUP_ERROR, cleanupError);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
},
|
|
1192
|
+
});
|
|
1193
|
+
};
|
|
1194
|
+
/**
|
|
1195
|
+
* Signs up a user using OAuth authentication.
|
|
1196
|
+
*
|
|
1197
|
+
* - This function creates a new sub-organization for the user using the provided OIDC token, public key, and provider name.
|
|
1198
|
+
* - Handles the full OAuth sign-up flow, including sub-organization creation and session management.
|
|
1199
|
+
* - Optionally accepts additional sub-organization creation parameters and a custom session key.
|
|
1200
|
+
* - After successful sign-up, automatically logs in the user and returns a signed JWT session token.
|
|
1201
|
+
*
|
|
1202
|
+
* @param params.oidcToken - OIDC token received after successful authentication with the OAuth provider.
|
|
1203
|
+
* @param params.publicKey - public key to associate with the new sub-organization.
|
|
1204
|
+
* @param params.providerName - name of the OAuth provider (e.g., "Google", "Apple").
|
|
1205
|
+
* @param params.createSubOrgParams - parameters for sub-organization creation (e.g., authenticators, user metadata).
|
|
1206
|
+
* @param params.sessionKey - session key to use for session creation (defaults to the default session key).
|
|
1207
|
+
* @returns A promise that resolves to a {@link BaseAuthResult}, which includes:
|
|
1208
|
+
* - `sessionToken`: the signed JWT session token.
|
|
1209
|
+
* @throws {ZeroXKeyError} If there is an error during the OAuth sign-up or login process.
|
|
1210
|
+
*/
|
|
1211
|
+
this.signUpWithOauth = async (params) => {
|
|
1212
|
+
const { oidcToken, publicKey, providerName = "OpenID Connect Provider" + " " + Date.now(), createSubOrgParams, sessionKey, } = params;
|
|
1213
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
1214
|
+
const signUpBody = buildSignUpBody({
|
|
1215
|
+
createSubOrgParams: {
|
|
1216
|
+
...createSubOrgParams,
|
|
1217
|
+
oauthProviders: [
|
|
1218
|
+
{
|
|
1219
|
+
providerName,
|
|
1220
|
+
oidcToken,
|
|
1221
|
+
},
|
|
1222
|
+
],
|
|
1223
|
+
},
|
|
1224
|
+
});
|
|
1225
|
+
const signupRes = await this.httpClient.proxySignup(signUpBody);
|
|
1226
|
+
if (!signupRes) {
|
|
1227
|
+
throw new ZeroXKeyError(`Auth proxy OAuth signup failed`, ZeroXKeyErrorCodes.OAUTH_SIGNUP_ERROR);
|
|
1228
|
+
}
|
|
1229
|
+
const oauthRes = await this.loginWithOauth({
|
|
1230
|
+
oidcToken,
|
|
1231
|
+
publicKey: publicKey,
|
|
1232
|
+
...(sessionKey && { sessionKey }),
|
|
1233
|
+
});
|
|
1234
|
+
return {
|
|
1235
|
+
...oauthRes,
|
|
1236
|
+
appProofs: signupRes.appProofs,
|
|
1237
|
+
};
|
|
1238
|
+
}, {
|
|
1239
|
+
errorMessage: "Failed to sign up with OAuth",
|
|
1240
|
+
errorCode: ZeroXKeyErrorCodes.OAUTH_SIGNUP_ERROR,
|
|
1241
|
+
});
|
|
1242
|
+
};
|
|
1243
|
+
/**
|
|
1244
|
+
* Fetches all wallets for the current user, including both embedded and connected wallets.
|
|
1245
|
+
*
|
|
1246
|
+
* - Retrieves all wallets associated with the organizationId from the current active session.
|
|
1247
|
+
* - For each embedded wallet, automatically fetches and attaches all associated wallet accounts.
|
|
1248
|
+
* - For connected wallets (e.g., browser extensions or external providers), groups providers by wallet name and attaches all connected accounts.
|
|
1249
|
+
* - Returns both embedded and connected wallets in a single array, each with their respective accounts populated.
|
|
1250
|
+
* - Optionally allows stamping the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
1251
|
+
*
|
|
1252
|
+
* @param params.walletProviders - array of wallet providers to use for fetching wallets.
|
|
1253
|
+
* @param params.organizationId - organization ID to target (defaults to the session's organization ID).
|
|
1254
|
+
* @param params.userId - user ID to target (defaults to the session's user ID).
|
|
1255
|
+
* @param params.connectedOnly - if true, fetches only connected wallets; if false or undefined, fetches both embedded and connected wallets.
|
|
1256
|
+
* @param params.stampWith - parameter to stamp the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
1257
|
+
* @returns A promise that resolves to an array of `Wallet` objects.
|
|
1258
|
+
* @throws {ZeroXKeyError} If no active session is found or if there is an error fetching wallets.
|
|
1259
|
+
*/
|
|
1260
|
+
this.fetchWallets = async (params) => {
|
|
1261
|
+
const { walletProviders, organizationId: organizationIdFromParams, userId: userIdFromParams, connectedOnly, stampWith = this.config.defaultStamperType, } = params || {};
|
|
1262
|
+
const session = await this.storageManager.getActiveSession();
|
|
1263
|
+
if (!session && !connectedOnly) {
|
|
1264
|
+
throw new ZeroXKeyError("No active session found. Fetching embedded wallets requires a valid session. If you only need connected wallets, set the 'connectedOnly' parameter to true.", ZeroXKeyErrorCodes.NO_SESSION_FOUND);
|
|
1265
|
+
}
|
|
1266
|
+
// if `connectedOnly` is true, we need to make sure the walletManager is initialized
|
|
1267
|
+
// or else we can't fetch connected wallets, and we throw an error
|
|
1268
|
+
if (connectedOnly && !this.walletManager?.connector) {
|
|
1269
|
+
throw new ZeroXKeyError("Wallet connector is not initialized", ZeroXKeyErrorCodes.WALLET_MANAGER_COMPONENT_NOT_INITIALIZED);
|
|
1270
|
+
}
|
|
1271
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
1272
|
+
let embedded = [];
|
|
1273
|
+
const organizationId = organizationIdFromParams || session?.organizationId;
|
|
1274
|
+
const userId = userIdFromParams || session?.userId;
|
|
1275
|
+
// we start fetching user early if we have the required params (needed for connected wallets)
|
|
1276
|
+
// this runs in parallel with the embedded wallet fetching below
|
|
1277
|
+
let userPromise;
|
|
1278
|
+
if (organizationId && userId && this.walletManager?.connector) {
|
|
1279
|
+
const signedUserRequest = await this.httpClient.stampGetUser({
|
|
1280
|
+
userId,
|
|
1281
|
+
organizationId,
|
|
1282
|
+
}, stampWith);
|
|
1283
|
+
if (!signedUserRequest) {
|
|
1284
|
+
throw new ZeroXKeyError("Failed to stamp user request", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
1285
|
+
}
|
|
1286
|
+
userPromise = this.httpClient
|
|
1287
|
+
.sendSignedRequest(signedUserRequest)
|
|
1288
|
+
.then((response) => getAuthenticatorAddresses(response.user));
|
|
1289
|
+
}
|
|
1290
|
+
// if connectedOnly is true, we skip fetching embedded wallets
|
|
1291
|
+
if (!connectedOnly) {
|
|
1292
|
+
if (!organizationId) {
|
|
1293
|
+
throw new ZeroXKeyError("No organization ID provided and no active session found. Please log in first or pass in an organization ID.", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
1294
|
+
}
|
|
1295
|
+
if (!userId) {
|
|
1296
|
+
throw new ZeroXKeyError("No user ID provided and no active session found. Please log in first or pass in a user ID.", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
1297
|
+
}
|
|
1298
|
+
// we stamp the wallet request first
|
|
1299
|
+
// this is done to avoid concurrent passkey prompts
|
|
1300
|
+
const signedWalletsRequest = await this.httpClient.stampGetWallets({
|
|
1301
|
+
organizationId,
|
|
1302
|
+
}, stampWith);
|
|
1303
|
+
if (!signedWalletsRequest) {
|
|
1304
|
+
throw new ZeroXKeyError("Failed to stamp wallet request", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
1305
|
+
}
|
|
1306
|
+
const [accounts, walletsRes] = await Promise.all([
|
|
1307
|
+
fetchAllWalletAccountsWithCursor(this.httpClient, organizationId, stampWith),
|
|
1308
|
+
this.httpClient.sendSignedRequest(signedWalletsRequest),
|
|
1309
|
+
]);
|
|
1310
|
+
// create a map of walletId to EmbeddedWallet for easy lookup
|
|
1311
|
+
const walletMap = new Map(walletsRes.wallets.map((wallet) => [
|
|
1312
|
+
wallet.walletId,
|
|
1313
|
+
{
|
|
1314
|
+
...wallet,
|
|
1315
|
+
source: WalletSource.Embedded,
|
|
1316
|
+
accounts: [],
|
|
1317
|
+
},
|
|
1318
|
+
]));
|
|
1319
|
+
// map the accounts to their respective wallets
|
|
1320
|
+
embedded = mapAccountsToWallet(accounts, walletMap);
|
|
1321
|
+
}
|
|
1322
|
+
// if wallet connecting is disabled we return only embedded wallets
|
|
1323
|
+
// this will never be hit if `connectedOnly` is true because of the check above
|
|
1324
|
+
if (!this.walletManager?.connector)
|
|
1325
|
+
return embedded;
|
|
1326
|
+
const providers = walletProviders ?? (await this.fetchWalletProviders());
|
|
1327
|
+
const groupedProviders = new Map();
|
|
1328
|
+
for (const provider of providers) {
|
|
1329
|
+
// connected wallets don't all have some uuid we can use for the walletId
|
|
1330
|
+
// so what we do is we use a normalized version of the name for the wallet, like "metamask" or "phantom-wallet"
|
|
1331
|
+
const walletId = provider.info?.name?.toLowerCase().replace(/\s+/g, "-") ||
|
|
1332
|
+
"unknown";
|
|
1333
|
+
const group = groupedProviders.get(walletId) || [];
|
|
1334
|
+
group.push(provider);
|
|
1335
|
+
groupedProviders.set(walletId, group);
|
|
1336
|
+
}
|
|
1337
|
+
// we fetch user once for all connected wallets to avoid duplicate `fetchUser` calls
|
|
1338
|
+
// this is only done if we have `organizationId` and `userId`
|
|
1339
|
+
// Note: this was started earlier in parallel with embedded wallet fetching for performance
|
|
1340
|
+
let authenticatorAddresses;
|
|
1341
|
+
if (userPromise) {
|
|
1342
|
+
authenticatorAddresses = await userPromise;
|
|
1343
|
+
}
|
|
1344
|
+
// has to be done in a for of loop so we can await each fetchWalletAccounts call individually
|
|
1345
|
+
// otherwise await Promise.all would cause them all to fire at once breaking passkey only set ups
|
|
1346
|
+
// (multiple wallet fetches at once causing "OperationError: A request is already pending.")
|
|
1347
|
+
let connected = [];
|
|
1348
|
+
for (const [walletId, grouped] of groupedProviders.entries()) {
|
|
1349
|
+
const timestamp = toExternalTimestamp();
|
|
1350
|
+
const wallet = {
|
|
1351
|
+
source: WalletSource.Connected,
|
|
1352
|
+
walletId,
|
|
1353
|
+
walletName: grouped[0]?.info?.name ?? "Unknown",
|
|
1354
|
+
createdAt: timestamp,
|
|
1355
|
+
updatedAt: timestamp,
|
|
1356
|
+
exported: false,
|
|
1357
|
+
imported: false,
|
|
1358
|
+
accounts: [],
|
|
1359
|
+
};
|
|
1360
|
+
const accounts = await this.fetchWalletAccounts({
|
|
1361
|
+
wallet,
|
|
1362
|
+
walletProviders: grouped,
|
|
1363
|
+
...(stampWith !== undefined && { stampWith }),
|
|
1364
|
+
...(organizationIdFromParams !== undefined && {
|
|
1365
|
+
organizationId: organizationIdFromParams,
|
|
1366
|
+
}),
|
|
1367
|
+
...(userIdFromParams !== undefined && { userId: userIdFromParams }),
|
|
1368
|
+
...(authenticatorAddresses && { authenticatorAddresses }),
|
|
1369
|
+
});
|
|
1370
|
+
wallet.accounts = accounts;
|
|
1371
|
+
if (wallet.accounts.length > 0) {
|
|
1372
|
+
connected.push(wallet);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
return [...embedded, ...connected];
|
|
1376
|
+
}, {
|
|
1377
|
+
errorMessage: "Failed to fetch wallets",
|
|
1378
|
+
errorCode: ZeroXKeyErrorCodes.FETCH_WALLETS_ERROR,
|
|
1379
|
+
});
|
|
1380
|
+
};
|
|
1381
|
+
/**
|
|
1382
|
+
* Fetches all accounts for a specific wallet, including both embedded and connected wallet accounts.
|
|
1383
|
+
*
|
|
1384
|
+
* - For embedded wallets, retrieves accounts from the ZeroXKey API, supporting pagination (defaults to the first page with a limit of 100 accounts).
|
|
1385
|
+
* - For connected wallets (e.g., browser extensions or external providers), constructs account objects for each connected address from the provided or discovered wallet providers.
|
|
1386
|
+
* - Automatically determines the account type and populates relevant fields such as address, curve, and signing capability.
|
|
1387
|
+
* - Optionally allows filtering by a specific set of wallet providers and supports custom pagination options.
|
|
1388
|
+
* - Supports stamping the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
1389
|
+
*
|
|
1390
|
+
* @param params.wallet - wallet for which to fetch accounts.
|
|
1391
|
+
* @param params.walletProviders - list of wallet providers to filter by (used for connected wallets).
|
|
1392
|
+
* @param params.paginationOptions - pagination options for embedded wallets.
|
|
1393
|
+
* @param params.stampWith - parameter to stamp the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
1394
|
+
* @param params.organizationId - organization ID to target (defaults to the session's organization ID).
|
|
1395
|
+
* @param params.userId - user ID to target (defaults to the session's user ID).
|
|
1396
|
+
* @param params.authenticatorAddresses - optional authenticator addresses to avoid redundant user fetches (this is used for connected wallets to determine if a connected wallet is an authenticator)
|
|
1397
|
+
*
|
|
1398
|
+
* @returns A promise that resolves to an array of `v1WalletAccount` objects.
|
|
1399
|
+
* @throws {ZeroXKeyError} If no active session is found or if there is an error fetching wallet accounts.
|
|
1400
|
+
*/
|
|
1401
|
+
this.fetchWalletAccounts = async (params) => {
|
|
1402
|
+
const { wallet, stampWith = this.config.defaultStamperType, walletProviders, paginationOptions, } = params;
|
|
1403
|
+
const session = await this.storageManager.getActiveSession();
|
|
1404
|
+
const organizationId = params?.organizationId || session?.organizationId;
|
|
1405
|
+
const userId = params?.userId || session?.userId;
|
|
1406
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
1407
|
+
// this is an embedded wallet so we fetch accounts from ZeroXKey
|
|
1408
|
+
if (wallet.source === WalletSource.Embedded) {
|
|
1409
|
+
const embedded = [];
|
|
1410
|
+
if (!organizationId) {
|
|
1411
|
+
throw new ZeroXKeyError("No organization ID provided and no active session found. Please log in first or pass in an organization ID.", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
1412
|
+
}
|
|
1413
|
+
if (!userId) {
|
|
1414
|
+
throw new ZeroXKeyError("No user ID provided and no active session found. Please log in first or pass in a user ID.", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
1415
|
+
}
|
|
1416
|
+
const res = await this.httpClient.getWalletAccounts({
|
|
1417
|
+
walletId: wallet.walletId,
|
|
1418
|
+
organizationId,
|
|
1419
|
+
paginationOptions: paginationOptions || { limit: "100" },
|
|
1420
|
+
}, stampWith);
|
|
1421
|
+
if (!res || !res.accounts) {
|
|
1422
|
+
throw new ZeroXKeyError("No wallet accounts found in the response", ZeroXKeyErrorCodes.BAD_RESPONSE);
|
|
1423
|
+
}
|
|
1424
|
+
for (const account of res.accounts) {
|
|
1425
|
+
embedded.push({
|
|
1426
|
+
...account,
|
|
1427
|
+
source: WalletSource.Embedded,
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
return embedded;
|
|
1431
|
+
}
|
|
1432
|
+
// this is an external wallet so we fetch accounts from the connected wallet provider
|
|
1433
|
+
// if wallet connecting is disabled we return only embedded wallets
|
|
1434
|
+
// we should never reach this point if wallet connecting is disabled
|
|
1435
|
+
if (!this.walletManager?.connector)
|
|
1436
|
+
return [];
|
|
1437
|
+
const connected = [];
|
|
1438
|
+
const providers = walletProviders ?? (await this.fetchWalletProviders());
|
|
1439
|
+
// Context: connected wallets don't all have some uuid we can use for the walletId so what
|
|
1440
|
+
// we do is we use a normalized version of the name for the wallet, like "metamask"
|
|
1441
|
+
// or "phantom-wallet"
|
|
1442
|
+
//
|
|
1443
|
+
// when fetching accounts, we select all providers with this normalized walletId.
|
|
1444
|
+
// A single wallet can map to multiple providers if it supports multiple chains
|
|
1445
|
+
// (e.g. MetaMask for Ethereum and MetaMask for Solana)
|
|
1446
|
+
const matching = providers.filter((p) => p.info?.name?.toLowerCase().replace(/\s+/g, "-") ===
|
|
1447
|
+
wallet.walletId && p.connectedAddresses.length > 0);
|
|
1448
|
+
const sign = this.walletManager.connector.sign.bind(this.walletManager.connector);
|
|
1449
|
+
let ethereumAddresses = [];
|
|
1450
|
+
let solanaAddresses = [];
|
|
1451
|
+
if (params.authenticatorAddresses) {
|
|
1452
|
+
({ ethereum: ethereumAddresses, solana: solanaAddresses } =
|
|
1453
|
+
params.authenticatorAddresses);
|
|
1454
|
+
}
|
|
1455
|
+
else if (organizationId && userId) {
|
|
1456
|
+
// we only fetch the user if authenticator addresses aren't provided and we have the organizationId and userId
|
|
1457
|
+
// if not, then that means `isAuthenticator` will always be false
|
|
1458
|
+
const user = await this.fetchUser({
|
|
1459
|
+
userId,
|
|
1460
|
+
organizationId,
|
|
1461
|
+
stampWith,
|
|
1462
|
+
});
|
|
1463
|
+
({ ethereum: ethereumAddresses, solana: solanaAddresses } =
|
|
1464
|
+
getAuthenticatorAddresses(user));
|
|
1465
|
+
}
|
|
1466
|
+
for (const provider of matching) {
|
|
1467
|
+
const timestamp = toExternalTimestamp();
|
|
1468
|
+
for (const address of provider.connectedAddresses) {
|
|
1469
|
+
if (isEthereumProvider(provider)) {
|
|
1470
|
+
const evmAccount = {
|
|
1471
|
+
walletAccountId: `${wallet.walletId}-${provider.interfaceType}-${address}`,
|
|
1472
|
+
organizationId: organizationId ?? "",
|
|
1473
|
+
walletId: wallet.walletId,
|
|
1474
|
+
pathFormat: "PATH_FORMAT_BIP32",
|
|
1475
|
+
path: WalletSource.Connected,
|
|
1476
|
+
source: WalletSource.Connected,
|
|
1477
|
+
address,
|
|
1478
|
+
createdAt: timestamp,
|
|
1479
|
+
updatedAt: timestamp,
|
|
1480
|
+
// ethereum specific
|
|
1481
|
+
curve: Curve.SECP256K1,
|
|
1482
|
+
addressFormat: "ADDRESS_FORMAT_ETHEREUM",
|
|
1483
|
+
chainInfo: provider.chainInfo,
|
|
1484
|
+
isAuthenticator: ethereumAddresses.includes(address.toLowerCase()),
|
|
1485
|
+
signMessage: (msg) => sign(msg, provider, SignIntent.SignMessage),
|
|
1486
|
+
signAndSendTransaction: (tx) => sign(tx, provider, SignIntent.SignAndSendTransaction),
|
|
1487
|
+
};
|
|
1488
|
+
connected.push(evmAccount);
|
|
1489
|
+
continue;
|
|
1490
|
+
}
|
|
1491
|
+
if (isSolanaProvider(provider)) {
|
|
1492
|
+
const solAccount = {
|
|
1493
|
+
walletAccountId: `${wallet.walletId}-${provider.interfaceType}-${address}`,
|
|
1494
|
+
organizationId: organizationId ?? "",
|
|
1495
|
+
walletId: wallet.walletId,
|
|
1496
|
+
pathFormat: "PATH_FORMAT_BIP32",
|
|
1497
|
+
path: WalletSource.Connected,
|
|
1498
|
+
source: WalletSource.Connected,
|
|
1499
|
+
address,
|
|
1500
|
+
createdAt: timestamp,
|
|
1501
|
+
updatedAt: timestamp,
|
|
1502
|
+
// solana specific
|
|
1503
|
+
publicKey: address,
|
|
1504
|
+
curve: Curve.ED25519,
|
|
1505
|
+
addressFormat: "ADDRESS_FORMAT_SOLANA",
|
|
1506
|
+
chainInfo: provider.chainInfo,
|
|
1507
|
+
isAuthenticator: solanaAddresses.includes(address),
|
|
1508
|
+
signMessage: (msg) => sign(msg, provider, SignIntent.SignMessage),
|
|
1509
|
+
signTransaction: (tx) => sign(tx, provider, SignIntent.SignTransaction),
|
|
1510
|
+
};
|
|
1511
|
+
connected.push(solAccount);
|
|
1512
|
+
continue;
|
|
1513
|
+
}
|
|
1514
|
+
throw new Error(`Unsupported wallet chain: ${provider.chainInfo}. Supported chains are Ethereum and Solana.`);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
return connected;
|
|
1518
|
+
}, {
|
|
1519
|
+
errorMessage: "Failed to fetch wallet accounts",
|
|
1520
|
+
errorCode: ZeroXKeyErrorCodes.FETCH_WALLET_ACCOUNTS_ERROR,
|
|
1521
|
+
});
|
|
1522
|
+
};
|
|
1523
|
+
/**
|
|
1524
|
+
* Fetches all private keys for the current user.
|
|
1525
|
+
*
|
|
1526
|
+
* - Retrieves private keys from the ZeroXKey API.
|
|
1527
|
+
* - Supports stamping the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
1528
|
+
*
|
|
1529
|
+
* @param params.stampWith - parameter to stamp the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
1530
|
+
* @param params.organizationId - organization ID to target (defaults to the session's organization ID).
|
|
1531
|
+
* @returns A promise that resolves to an array of `v1PrivateKey` objects.
|
|
1532
|
+
* @throws {ZeroXKeyError} If no active session is found or if there is an error fetching private keys.
|
|
1533
|
+
*/
|
|
1534
|
+
this.fetchPrivateKeys = async (params) => {
|
|
1535
|
+
const { stampWith = this.config.defaultStamperType } = params || {};
|
|
1536
|
+
const session = await getActiveSessionOrThrowIfRequired(stampWith, this.storageManager.getActiveSession);
|
|
1537
|
+
const organizationId = params?.organizationId || session?.organizationId;
|
|
1538
|
+
if (!organizationId) {
|
|
1539
|
+
throw new ZeroXKeyError("No organization ID provided and no active session found. Please log in first or pass in an organization ID.", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
1540
|
+
}
|
|
1541
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
1542
|
+
const res = await this.httpClient.getPrivateKeys({ organizationId }, stampWith);
|
|
1543
|
+
if (!res) {
|
|
1544
|
+
throw new ZeroXKeyError("Failed to fetch private keys", ZeroXKeyErrorCodes.BAD_RESPONSE);
|
|
1545
|
+
}
|
|
1546
|
+
return res.privateKeys;
|
|
1547
|
+
}, {
|
|
1548
|
+
errorMessage: "Failed to fetch private keys",
|
|
1549
|
+
errorCode: ZeroXKeyErrorCodes.FETCH_PRIVATE_KEYS_ERROR,
|
|
1550
|
+
});
|
|
1551
|
+
};
|
|
1552
|
+
/**
|
|
1553
|
+
* Signs a message using the specified wallet account.
|
|
1554
|
+
*
|
|
1555
|
+
* Behavior differs depending on the wallet type:
|
|
1556
|
+
*
|
|
1557
|
+
* - **Connected wallets**
|
|
1558
|
+
* - Delegates signing to the wallet provider’s native signing method.
|
|
1559
|
+
* - *Ethereum*: signatures always follow [EIP-191](https://eips.ethereum.org/EIPS/eip-191).
|
|
1560
|
+
* - The wallet automatically prefixes messages with
|
|
1561
|
+
* `"\x19Ethereum Signed Message:\n" + message length` before signing.
|
|
1562
|
+
* - As a result, these signatures cannot be used as raw transaction signatures or broadcast on-chain.
|
|
1563
|
+
* - If `addEthereumPrefix` is set to `false`, an error is thrown because connected Ethereum wallets always prefix.
|
|
1564
|
+
* - *Other chains*: follows the native connected wallet behavior.
|
|
1565
|
+
*
|
|
1566
|
+
* - **Embedded wallets**
|
|
1567
|
+
* - Uses the ZeroXKey API to sign the message directly.
|
|
1568
|
+
* - Supports optional `addEthereumPrefix`:
|
|
1569
|
+
* - If `true` (default for Ethereum), the message is prefixed before signing.
|
|
1570
|
+
* - If `false`, the raw message is signed without any prefix.
|
|
1571
|
+
*
|
|
1572
|
+
* Additional details:
|
|
1573
|
+
* - Automatically handles encoding and hashing based on the wallet account’s address format,
|
|
1574
|
+
* unless explicitly overridden.
|
|
1575
|
+
* - Optionally allows stamping with a specific stamper
|
|
1576
|
+
* (`StamperType.Passkey`, `StamperType.ApiKey`, or `StamperType.Wallet`).
|
|
1577
|
+
*
|
|
1578
|
+
* @param params.message - plaintext (UTF-8) message to sign.
|
|
1579
|
+
* @param params.walletAccount - wallet account to use for signing.
|
|
1580
|
+
* @param params.encoding - override for payload encoding (defaults to the encoding appropriate for the address format).
|
|
1581
|
+
* @param params.hashFunction - override for hash function (defaults to the function appropriate for the address format).
|
|
1582
|
+
* @param params.stampWith - optional stamper for the signing request.
|
|
1583
|
+
* @param params.addEthereumPrefix - whether to prefix the message with Ethereum’s
|
|
1584
|
+
* `"\x19Ethereum Signed Message:\n"` string (default: `true` for Ethereum).
|
|
1585
|
+
* @param params.organizationId - organization ID to target (defaults to the session's organization ID).
|
|
1586
|
+
*
|
|
1587
|
+
* @returns A promise that resolves to a `v1SignRawPayloadResult` containing the signature and metadata.
|
|
1588
|
+
* @throws {ZeroXKeyError} If signing fails, the wallet type does not support message signing, or the response is invalid.
|
|
1589
|
+
*/
|
|
1590
|
+
this.signMessage = async (params) => {
|
|
1591
|
+
const { message, walletAccount, stampWith = this.config.defaultStamperType, addEthereumPrefix, organizationId, } = params;
|
|
1592
|
+
const hashFunction = params.hashFunction || getHashFunction(walletAccount.addressFormat);
|
|
1593
|
+
const payloadEncoding = params.encoding || getEncodingType(walletAccount.addressFormat);
|
|
1594
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
1595
|
+
const isEthereum = walletAccount.addressFormat === "ADDRESS_FORMAT_ETHEREUM";
|
|
1596
|
+
if (walletAccount.source === WalletSource.Connected) {
|
|
1597
|
+
// this is a connected wallet
|
|
1598
|
+
if (!addEthereumPrefix && isEthereum) {
|
|
1599
|
+
throw new ZeroXKeyError("Connected Ethereum wallets automatically prefix messages. Use `addEthereumPrefix: true`.", ZeroXKeyErrorCodes.SIGN_MESSAGE_ERROR);
|
|
1600
|
+
}
|
|
1601
|
+
let encodedMessage = message;
|
|
1602
|
+
if (isEthereum) {
|
|
1603
|
+
const msgBytes = toUtf8Bytes(message);
|
|
1604
|
+
encodedMessage = getEncodedMessage(payloadEncoding, msgBytes);
|
|
1605
|
+
}
|
|
1606
|
+
const sigHex = await walletAccount.signMessage(encodedMessage);
|
|
1607
|
+
return splitSignature(sigHex, walletAccount.addressFormat);
|
|
1608
|
+
}
|
|
1609
|
+
// this is an embedded wallet
|
|
1610
|
+
let msgBytes = toUtf8Bytes(message);
|
|
1611
|
+
if (addEthereumPrefix && isEthereum) {
|
|
1612
|
+
const prefix = `\x19Ethereum Signed Message:\n${msgBytes.length}`;
|
|
1613
|
+
const prefixBytes = toUtf8Bytes(prefix);
|
|
1614
|
+
const combined = new Uint8Array(prefixBytes.length + msgBytes.length);
|
|
1615
|
+
combined.set(prefixBytes, 0);
|
|
1616
|
+
combined.set(msgBytes, prefixBytes.length);
|
|
1617
|
+
msgBytes = combined;
|
|
1618
|
+
}
|
|
1619
|
+
const encodedMessage = getEncodedMessage(payloadEncoding, msgBytes);
|
|
1620
|
+
const response = await this.httpClient.signRawPayload({
|
|
1621
|
+
signWith: walletAccount.address,
|
|
1622
|
+
payload: encodedMessage,
|
|
1623
|
+
encoding: payloadEncoding,
|
|
1624
|
+
hashFunction,
|
|
1625
|
+
...(organizationId && { organizationId }),
|
|
1626
|
+
}, stampWith);
|
|
1627
|
+
if (response.activity.failure) {
|
|
1628
|
+
throw new ZeroXKeyError("Failed to sign message, no signed payload returned", ZeroXKeyErrorCodes.SIGN_MESSAGE_ERROR);
|
|
1629
|
+
}
|
|
1630
|
+
return response.activity.result
|
|
1631
|
+
.signRawPayloadResult;
|
|
1632
|
+
}, {
|
|
1633
|
+
errorMessage: "Failed to sign message",
|
|
1634
|
+
errorCode: ZeroXKeyErrorCodes.SIGN_MESSAGE_ERROR,
|
|
1635
|
+
});
|
|
1636
|
+
};
|
|
1637
|
+
/**
|
|
1638
|
+
* Signs a transaction using the specified wallet account.
|
|
1639
|
+
*
|
|
1640
|
+
* Behavior differs depending on the type of wallet:
|
|
1641
|
+
*
|
|
1642
|
+
* - **Connected wallets**
|
|
1643
|
+
* - Ethereum: does not support raw transaction signing. Calling this function will throw an error instructing you to use `signAndSendTransaction` instead.
|
|
1644
|
+
* - Solana: supports raw transaction signing via the connected wallet provider.
|
|
1645
|
+
* - Other chains: not supported; will throw an error.
|
|
1646
|
+
*
|
|
1647
|
+
* - **Embedded wallets**
|
|
1648
|
+
* - Delegates signing to the ZeroXKey API, which returns the signed transaction.
|
|
1649
|
+
* - Supports all ZeroXKey-supported transaction types (e.g., Ethereum/EVM, Solana, Tron).
|
|
1650
|
+
* - BSC and other EVM-compatible chains use `TRANSACTION_TYPE_ETHEREUM`; the chain is identified by the unsigned transaction's `chainId`.
|
|
1651
|
+
* - Optionally allows stamping with a specific stamper (`StamperType.Passkey`, `StamperType.ApiKey`, or `StamperType.Wallet`).
|
|
1652
|
+
* - Note: For embedded Ethereum wallets, the returned signature doesn’t include the `0x` prefix. You should add `0x` before
|
|
1653
|
+
* broadcasting if it’s missing. It’s a good idea to check whether the signature already starts with `0x` before adding it,
|
|
1654
|
+
* since we plan to include the prefix by default in a future breaking change.
|
|
1655
|
+
*
|
|
1656
|
+
* @param params.walletAccount - wallet account to use for signing.
|
|
1657
|
+
* @param params.unsignedTransaction - unsigned transaction data as a serialized
|
|
1658
|
+
* string in the canonical encoding for the given `transactionType`.
|
|
1659
|
+
* @param params.transactionType - type of transaction (e.g., "TRANSACTION_TYPE_ETHEREUM", "TRANSACTION_TYPE_SOLANA", "TRANSACTION_TYPE_TRON"). Use "TRANSACTION_TYPE_ETHEREUM" for BSC.
|
|
1660
|
+
* @param params.stampWith - stamper to use for signing (`StamperType.Passkey`, `StamperType.ApiKey`, or `StamperType.Wallet`).
|
|
1661
|
+
* @param params.organizationId - organization ID to target (defaults to the session's organization ID).
|
|
1662
|
+
* @returns A promise that resolves to the signed transaction string.
|
|
1663
|
+
* @throws {ZeroXKeyError} If the wallet type is unsupported, signing fails, or the response is invalid.
|
|
1664
|
+
*/
|
|
1665
|
+
this.signTransaction = async (params) => {
|
|
1666
|
+
const { walletAccount, unsignedTransaction, transactionType, stampWith = this.config.defaultStamperType, organizationId, } = params;
|
|
1667
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
1668
|
+
if (walletAccount.source === WalletSource.Connected) {
|
|
1669
|
+
switch (walletAccount.chainInfo.namespace) {
|
|
1670
|
+
case Chain.Ethereum:
|
|
1671
|
+
throw new ZeroXKeyError("Ethereum connected wallets do not support raw transaction signing. Use signAndSendTransaction instead.", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
1672
|
+
case Chain.Solana:
|
|
1673
|
+
// not sure why typescript isn't inferring the type here
|
|
1674
|
+
// if namespace is Chain.Solana, then it must be a ConnectedSolanaWalletAccount
|
|
1675
|
+
return walletAccount.signTransaction(unsignedTransaction);
|
|
1676
|
+
default:
|
|
1677
|
+
throw new ZeroXKeyError("Unsupported connected wallet type.", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
// this is an embedded wallet account
|
|
1681
|
+
const signTransaction = await this.httpClient.signTransaction({
|
|
1682
|
+
signWith: walletAccount.address,
|
|
1683
|
+
unsignedTransaction,
|
|
1684
|
+
type: transactionType,
|
|
1685
|
+
...(organizationId && { organizationId }),
|
|
1686
|
+
}, stampWith);
|
|
1687
|
+
// TODO (breaking change): eventually we should append a `0x` prefix for ethereum signatures here
|
|
1688
|
+
// then we should remove the note in the comment header
|
|
1689
|
+
return signTransaction.signedTransaction;
|
|
1690
|
+
}, {
|
|
1691
|
+
errorMessage: "Failed to sign transaction",
|
|
1692
|
+
errorCode: ZeroXKeyErrorCodes.SIGN_TRANSACTION_ERROR,
|
|
1693
|
+
});
|
|
1694
|
+
};
|
|
1695
|
+
/**
|
|
1696
|
+
* Signs and broadcasts a transaction using the specified wallet account.
|
|
1697
|
+
*
|
|
1698
|
+
* Behavior differs depending on the type of wallet:
|
|
1699
|
+
*
|
|
1700
|
+
* - **Connected wallets**
|
|
1701
|
+
* - *Ethereum*: delegates to the wallet’s native `signAndSendTransaction` method.
|
|
1702
|
+
* - Does **not** require an `rpcUrl` (the wallet handles broadcasting).
|
|
1703
|
+
* - *Solana*: signs the transaction locally with the connected wallet, but requires an `rpcUrl` to broadcast it.
|
|
1704
|
+
* - Other chains: not supported; will throw an error.
|
|
1705
|
+
*
|
|
1706
|
+
* - **Embedded wallets**
|
|
1707
|
+
* - Signs the transaction using the ZeroXKey API.
|
|
1708
|
+
* - Requires an `rpcUrl` to broadcast the signed transaction, since ZeroXKey does not broadcast directly.
|
|
1709
|
+
* - Broadcasts the transaction using a JSON-RPC client and returns the resulting transaction hash/signature.
|
|
1710
|
+
* - Optionally allows stamping with a specific stamper (`StamperType.Passkey`, `StamperType.ApiKey`, or `StamperType.Wallet`).
|
|
1711
|
+
*
|
|
1712
|
+
* @param params.walletAccount - wallet account to use for signing and broadcasting.
|
|
1713
|
+
* @param params.unsignedTransaction - unsigned transaction data as a serialized
|
|
1714
|
+
* string in the canonical encoding for the given `transactionType`.
|
|
1715
|
+
* @param params.transactionType - type of transaction (e.g., `"TRANSACTION_TYPE_SOLANA"`, `"TRANSACTION_TYPE_ETHEREUM"`).
|
|
1716
|
+
* @param params.rpcUrl - JSON-RPC endpoint used for broadcasting (required for Solana connected wallets and all embedded wallets).
|
|
1717
|
+
* @param params.stampWith - optional stamper to use when signing (`StamperType.Passkey`, `StamperType.ApiKey`, or `StamperType.Wallet`).
|
|
1718
|
+
* @param params.organizationId - **Only for ZeroXKey embedded wallets**: organization ID to target (defaults to the session's organization ID).
|
|
1719
|
+
* @returns A promise that resolves to a transaction signature or hash.
|
|
1720
|
+
* @throws {ZeroXKeyError} If the wallet type is unsupported, or if signing/broadcasting fails.
|
|
1721
|
+
*/
|
|
1722
|
+
this.signAndSendTransaction = async (params) => {
|
|
1723
|
+
const { walletAccount, unsignedTransaction, transactionType, rpcUrl, stampWith = this.config.defaultStamperType, organizationId, } = params;
|
|
1724
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
1725
|
+
if (walletAccount.source === WalletSource.Connected) {
|
|
1726
|
+
// this is a connected wallet account
|
|
1727
|
+
switch (walletAccount.chainInfo.namespace) {
|
|
1728
|
+
case Chain.Ethereum:
|
|
1729
|
+
// not sure why typescript isn't inferring the type here
|
|
1730
|
+
// if namespace is Chain.Ethereum, then it must be a ConnectedEthereumWalletAccount
|
|
1731
|
+
return await walletAccount.signAndSendTransaction(unsignedTransaction);
|
|
1732
|
+
case Chain.Solana:
|
|
1733
|
+
if (!rpcUrl) {
|
|
1734
|
+
throw new ZeroXKeyError("Missing rpcUrl: connected Solana wallets require an RPC URL to broadcast transactions.", ZeroXKeyErrorCodes.SIGN_AND_SEND_TRANSACTION_ERROR);
|
|
1735
|
+
}
|
|
1736
|
+
// not sure why typescript isn't inferring the type here
|
|
1737
|
+
// if namespace is Chain.Solana, then it must be a ConnectedSolanaWalletAccount
|
|
1738
|
+
const signature = await walletAccount.signTransaction(unsignedTransaction);
|
|
1739
|
+
return await broadcastTransaction({
|
|
1740
|
+
signedTransaction: signature,
|
|
1741
|
+
rpcUrl,
|
|
1742
|
+
transactionType,
|
|
1743
|
+
});
|
|
1744
|
+
default:
|
|
1745
|
+
throw new ZeroXKeyError("Connected wallets do not support signAndSendTransaction for this transaction type.", ZeroXKeyErrorCodes.SIGN_AND_SEND_TRANSACTION_ERROR);
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
// this is an embedded wallet account
|
|
1749
|
+
// embedded wallet requires an RPC URL to broadcast
|
|
1750
|
+
// since ZeroXKey does not broadcast transactions directly
|
|
1751
|
+
if (!rpcUrl) {
|
|
1752
|
+
throw new ZeroXKeyError("Missing rpcUrl: embedded wallets require an RPC URL to broadcast transactions.", ZeroXKeyErrorCodes.SIGN_AND_SEND_TRANSACTION_ERROR);
|
|
1753
|
+
}
|
|
1754
|
+
const signTransactionResponse = await this.httpClient.signTransaction({
|
|
1755
|
+
signWith: walletAccount.address,
|
|
1756
|
+
unsignedTransaction,
|
|
1757
|
+
type: transactionType,
|
|
1758
|
+
...(organizationId && { organizationId }),
|
|
1759
|
+
}, stampWith);
|
|
1760
|
+
const signedTx = transactionType === "TRANSACTION_TYPE_ETHEREUM"
|
|
1761
|
+
? `0x${signTransactionResponse.signedTransaction}`
|
|
1762
|
+
: signTransactionResponse.signedTransaction;
|
|
1763
|
+
const txHash = await broadcastTransaction({
|
|
1764
|
+
signedTransaction: signedTx,
|
|
1765
|
+
rpcUrl,
|
|
1766
|
+
transactionType,
|
|
1767
|
+
});
|
|
1768
|
+
return txHash;
|
|
1769
|
+
}, {
|
|
1770
|
+
errorMessage: "Failed to sign and send transaction",
|
|
1771
|
+
errorCode: ZeroXKeyErrorCodes.SIGN_AND_SEND_TRANSACTION_ERROR,
|
|
1772
|
+
});
|
|
1773
|
+
};
|
|
1774
|
+
/**
|
|
1775
|
+
* @beta
|
|
1776
|
+
* * **API subject to change**
|
|
1777
|
+
*
|
|
1778
|
+
* Signs and submits an ERC20 `transfer(address,uint256)` as an Ethereum transaction
|
|
1779
|
+
* using a ZeroXKey-managed (embedded) wallet.
|
|
1780
|
+
*
|
|
1781
|
+
* This is a convenience wrapper around `ethSendTransaction`:
|
|
1782
|
+
* - Encodes ERC20 transfer calldata.
|
|
1783
|
+
* - Sends a transaction to the token contract.
|
|
1784
|
+
* - Returns a `sendTransactionStatusId` for polling with `pollTransactionStatus`.
|
|
1785
|
+
*
|
|
1786
|
+
* @param params.organizationId - Organization ID to execute the transaction under.
|
|
1787
|
+
* Defaults to the active session's organization.
|
|
1788
|
+
* @param params.stampWith - Optional stamper to authorize signing (e.g., passkey).
|
|
1789
|
+
* @param params.transfer - ERC20 transfer parameters.
|
|
1790
|
+
* @returns A promise resolving to the `sendTransactionStatusId`.
|
|
1791
|
+
* @throws {ZeroXKeyError} If amount encoding fails or ZeroXKey rejects the transaction.
|
|
1792
|
+
*/
|
|
1793
|
+
this.ethSendErc20Transfer = async (params) => {
|
|
1794
|
+
const { organizationId, stampWith = this.config.defaultStamperType, transfer, } = params;
|
|
1795
|
+
const { from, to, tokenAddress, amount, caip2, nonce, gasLimit, maxFeePerGas, maxPriorityFeePerGas, sponsor, } = transfer;
|
|
1796
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
1797
|
+
let parsedAmount;
|
|
1798
|
+
try {
|
|
1799
|
+
parsedAmount = BigInt(amount);
|
|
1800
|
+
}
|
|
1801
|
+
catch {
|
|
1802
|
+
throw new ZeroXKeyError("Invalid ERC20 amount. Use a base-unit integer string.", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
1803
|
+
}
|
|
1804
|
+
const data = encodeFunctionData({
|
|
1805
|
+
abi: ERC20_TRANSFER_ABI,
|
|
1806
|
+
functionName: "transfer",
|
|
1807
|
+
args: [to, parsedAmount],
|
|
1808
|
+
});
|
|
1809
|
+
return this.ethSendTransaction({
|
|
1810
|
+
...(organizationId !== undefined ? { organizationId } : {}),
|
|
1811
|
+
...(stampWith !== undefined ? { stampWith } : {}),
|
|
1812
|
+
transaction: {
|
|
1813
|
+
from,
|
|
1814
|
+
to: tokenAddress,
|
|
1815
|
+
caip2,
|
|
1816
|
+
data,
|
|
1817
|
+
...(sponsor !== undefined ? { sponsor } : {}),
|
|
1818
|
+
...(nonce ? { nonce } : {}),
|
|
1819
|
+
...(gasLimit ? { gasLimit } : {}),
|
|
1820
|
+
...(maxFeePerGas ? { maxFeePerGas } : {}),
|
|
1821
|
+
...(maxPriorityFeePerGas ? { maxPriorityFeePerGas } : {}),
|
|
1822
|
+
},
|
|
1823
|
+
});
|
|
1824
|
+
}, {
|
|
1825
|
+
errorMessage: "Failed to sign and send ERC20 transfer",
|
|
1826
|
+
errorCode: ZeroXKeyErrorCodes.ETH_SEND_TRANSACTION_ERROR,
|
|
1827
|
+
});
|
|
1828
|
+
};
|
|
1829
|
+
/**
|
|
1830
|
+
* @beta
|
|
1831
|
+
* * **API subject to change**
|
|
1832
|
+
*
|
|
1833
|
+
* Signs and submits an Ethereum transaction using a ZeroXKey-managed (embedded) wallet.
|
|
1834
|
+
*
|
|
1835
|
+
* This method performs **authorization and signing**, and submits the transaction
|
|
1836
|
+
* to ZeroXKey’s coordinator. It **does not perform any polling** — callers must use
|
|
1837
|
+
* `pollTransactionStatus` to obtain the final on-chain result.
|
|
1838
|
+
*
|
|
1839
|
+
* Behavior:
|
|
1840
|
+
*
|
|
1841
|
+
* - **Connected wallets**
|
|
1842
|
+
* - Connected wallets are **not supported** by this method.
|
|
1843
|
+
* - They must instead use `signAndSendTransaction`.
|
|
1844
|
+
*
|
|
1845
|
+
* - **Embedded wallets**
|
|
1846
|
+
* - Constructs the payload for ZeroXKey's `eth_send_transaction` endpoint.
|
|
1847
|
+
* - Forwards transaction fields directly to ZeroXKey's coordinator.
|
|
1848
|
+
* - Signs and submits the transaction through ZeroXKey.
|
|
1849
|
+
* - Returns a `sendTransactionStatusId`, which the caller must pass to
|
|
1850
|
+
* `pollTransactionStatus` to obtain the final result (tx hash + status).
|
|
1851
|
+
*
|
|
1852
|
+
* @param params.organizationId - Organization ID to execute the transaction under.
|
|
1853
|
+
* Defaults to the active session's organization.
|
|
1854
|
+
*
|
|
1855
|
+
* @param params.stampWith - Optional stamper to authorize signing (e.g., passkey).
|
|
1856
|
+
*
|
|
1857
|
+
* @param params.transaction - The Ethereum transaction details.
|
|
1858
|
+
* @returns A promise resolving to the `sendTransactionStatusId`.
|
|
1859
|
+
* This ID must be passed to `pollTransactionStatus`.
|
|
1860
|
+
*
|
|
1861
|
+
* @throws {ZeroXKeyError} If the transaction is invalid or ZeroXKey rejects it.
|
|
1862
|
+
*/
|
|
1863
|
+
this.ethSendTransaction = async (params) => {
|
|
1864
|
+
const { organizationId: organizationIdFromParams, stampWith = this.config.defaultStamperType, transaction, } = params;
|
|
1865
|
+
const { from, to, caip2, value, data, nonce, gasLimit, maxFeePerGas, maxPriorityFeePerGas, sponsor, gasStationNonce, } = transaction;
|
|
1866
|
+
const session = await getActiveSessionOrThrowIfRequired(stampWith, this.storageManager.getActiveSession);
|
|
1867
|
+
const organizationId = organizationIdFromParams || session?.organizationId;
|
|
1868
|
+
if (!organizationId) {
|
|
1869
|
+
throw new ZeroXKeyError("Organization ID must be provided to send a transaction", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
1870
|
+
}
|
|
1871
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
1872
|
+
//
|
|
1873
|
+
// Build ZeroXKey intent
|
|
1874
|
+
//
|
|
1875
|
+
const intent = {
|
|
1876
|
+
from,
|
|
1877
|
+
to,
|
|
1878
|
+
caip2,
|
|
1879
|
+
...(value ? { value } : {}),
|
|
1880
|
+
...(data ? { data } : {}),
|
|
1881
|
+
...(nonce !== undefined ? { nonce } : {}),
|
|
1882
|
+
};
|
|
1883
|
+
if (sponsor) {
|
|
1884
|
+
intent.sponsor = true;
|
|
1885
|
+
if (gasStationNonce !== undefined)
|
|
1886
|
+
intent.gasStationNonce = gasStationNonce;
|
|
1887
|
+
}
|
|
1888
|
+
else {
|
|
1889
|
+
if (gasLimit)
|
|
1890
|
+
intent.gasLimit = gasLimit;
|
|
1891
|
+
if (maxFeePerGas)
|
|
1892
|
+
intent.maxFeePerGas = maxFeePerGas;
|
|
1893
|
+
if (maxPriorityFeePerGas)
|
|
1894
|
+
intent.maxPriorityFeePerGas = maxPriorityFeePerGas;
|
|
1895
|
+
}
|
|
1896
|
+
//
|
|
1897
|
+
// Submit to ZeroXKey
|
|
1898
|
+
//
|
|
1899
|
+
const resp = await this.httpClient.ethSendTransaction({
|
|
1900
|
+
...intent,
|
|
1901
|
+
organizationId,
|
|
1902
|
+
});
|
|
1903
|
+
const id = resp.sendTransactionStatusId;
|
|
1904
|
+
if (!id) {
|
|
1905
|
+
throw new ZeroXKeyError("Missing sendTransactionStatusId", ZeroXKeyErrorCodes.ETH_SEND_TRANSACTION_ERROR);
|
|
1906
|
+
}
|
|
1907
|
+
return id;
|
|
1908
|
+
}, {
|
|
1909
|
+
errorMessage: "Failed to sign and send Ethereum transaction",
|
|
1910
|
+
errorCode: ZeroXKeyErrorCodes.ETH_SEND_TRANSACTION_ERROR,
|
|
1911
|
+
});
|
|
1912
|
+
};
|
|
1913
|
+
/**
|
|
1914
|
+
* @beta
|
|
1915
|
+
* * **API subject to change**
|
|
1916
|
+
*
|
|
1917
|
+
* Signs and submits a Solana transaction using a ZeroXKey-managed (embedded) wallet.
|
|
1918
|
+
*
|
|
1919
|
+
* This method performs **authorization and signing**, and submits the transaction
|
|
1920
|
+
* to ZeroXKey’s coordinator. It **does not perform any polling** — callers must use
|
|
1921
|
+
* `pollTransactionStatus` to obtain the final on-chain result.
|
|
1922
|
+
*
|
|
1923
|
+
* Behavior:
|
|
1924
|
+
*
|
|
1925
|
+
* - **Connected wallets**
|
|
1926
|
+
* - Connected wallets are **not supported** by this method.
|
|
1927
|
+
* - They must instead use `signAndSendTransaction`.
|
|
1928
|
+
*
|
|
1929
|
+
* - **Embedded wallets**
|
|
1930
|
+
* - Constructs the payload for ZeroXKey's `sol_send_transaction` endpoint.
|
|
1931
|
+
* - Signs and submits the transaction through ZeroXKey.
|
|
1932
|
+
* - Returns a `sendTransactionStatusId`, which the caller must pass to
|
|
1933
|
+
* `pollTransactionStatus` to obtain the final result (signature + status).
|
|
1934
|
+
*
|
|
1935
|
+
* @param params.organizationId - Organization ID to execute the transaction under.
|
|
1936
|
+
* Defaults to the active session's organization.
|
|
1937
|
+
* @param params.stampWith - Optional stamper to authorize signing (e.g., passkey).
|
|
1938
|
+
* @param params.transaction - The Solana transaction details.
|
|
1939
|
+
* @returns A promise resolving to the `sendTransactionStatusId`.
|
|
1940
|
+
* This ID must be passed to `pollTransactionStatus`.
|
|
1941
|
+
* @throws {ZeroXKeyError} If the transaction is invalid or ZeroXKey rejects it.
|
|
1942
|
+
*/
|
|
1943
|
+
this.solSendTransaction = async (params) => {
|
|
1944
|
+
const { organizationId: organizationIdFromParams, stampWith = this.config.defaultStamperType, transaction, } = params;
|
|
1945
|
+
const session = await getActiveSessionOrThrowIfRequired(stampWith, this.storageManager.getActiveSession);
|
|
1946
|
+
const organizationId = organizationIdFromParams || session?.organizationId;
|
|
1947
|
+
if (!organizationId) {
|
|
1948
|
+
throw new ZeroXKeyError("Organization ID must be provided to send a transaction", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
1949
|
+
}
|
|
1950
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
1951
|
+
const intent = {
|
|
1952
|
+
unsignedTransaction: transaction.unsignedTransaction,
|
|
1953
|
+
signWith: transaction.signWith,
|
|
1954
|
+
caip2: transaction.caip2,
|
|
1955
|
+
...(transaction.sponsor !== undefined
|
|
1956
|
+
? { sponsor: transaction.sponsor }
|
|
1957
|
+
: {}),
|
|
1958
|
+
...(transaction.recentBlockhash
|
|
1959
|
+
? { recentBlockhash: transaction.recentBlockhash }
|
|
1960
|
+
: {}),
|
|
1961
|
+
};
|
|
1962
|
+
const resp = await this.httpClient.solSendTransaction({
|
|
1963
|
+
...intent,
|
|
1964
|
+
organizationId,
|
|
1965
|
+
}, stampWith);
|
|
1966
|
+
const id = resp.sendTransactionStatusId;
|
|
1967
|
+
if (!id) {
|
|
1968
|
+
throw new ZeroXKeyError("Missing sendTransactionStatusId", ZeroXKeyErrorCodes.SOL_SEND_TRANSACTION_ERROR);
|
|
1969
|
+
}
|
|
1970
|
+
return id;
|
|
1971
|
+
}, {
|
|
1972
|
+
errorMessage: "Failed to sign and send Solana transaction",
|
|
1973
|
+
errorCode: ZeroXKeyErrorCodes.SOL_SEND_TRANSACTION_ERROR,
|
|
1974
|
+
});
|
|
1975
|
+
};
|
|
1976
|
+
/**
|
|
1977
|
+
* Fetches the user details for the current session or a specified user.
|
|
1978
|
+
*
|
|
1979
|
+
* - Retrieves user details from the ZeroXKey API using the provided userId and organizationId, or defaults to those from the active session.
|
|
1980
|
+
* - If no userId is provided, the userId from the current session is used.
|
|
1981
|
+
* - If no organizationId is provided, the organizationId from the current session is used.
|
|
1982
|
+
* - Optionally allows stamping the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
1983
|
+
* - Ensures that an active session exists before making the request.
|
|
1984
|
+
*
|
|
1985
|
+
* @param params.organizationId - organization ID to specify the sub-organization (defaults to the current session's organizationId).
|
|
1986
|
+
* @param params.userId - user ID to fetch specific user details (defaults to the current session's userId).
|
|
1987
|
+
* @param params.stampWith - parameter to stamp the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
1988
|
+
* @returns A promise that resolves to a `v1User` object containing the user details.
|
|
1989
|
+
* @throws {ZeroXKeyError} If there is no active session, if there is no userId, or if there is an error fetching user details.
|
|
1990
|
+
*/
|
|
1991
|
+
this.fetchUser = async (params) => {
|
|
1992
|
+
const { organizationId: organizationIdFromParams, userId: userIdFromParams, stampWith = this.config.defaultStamperType, } = params || {};
|
|
1993
|
+
const session = await getActiveSessionOrThrowIfRequired(stampWith, this.storageManager.getActiveSession);
|
|
1994
|
+
const userId = userIdFromParams || session?.userId;
|
|
1995
|
+
if (!userId) {
|
|
1996
|
+
throw new ZeroXKeyError("User ID must be provided to fetch user", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
1997
|
+
}
|
|
1998
|
+
const organizationId = organizationIdFromParams || session?.organizationId;
|
|
1999
|
+
if (!organizationId) {
|
|
2000
|
+
throw new ZeroXKeyError("Organization ID must be provided to fetch user", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
2001
|
+
}
|
|
2002
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
2003
|
+
const userResponse = await this.httpClient.getUser({ organizationId, userId }, stampWith);
|
|
2004
|
+
if (!userResponse || !userResponse.user) {
|
|
2005
|
+
throw new ZeroXKeyError("No user found in the response", ZeroXKeyErrorCodes.BAD_RESPONSE);
|
|
2006
|
+
}
|
|
2007
|
+
return userResponse.user;
|
|
2008
|
+
}, {
|
|
2009
|
+
errorMessage: "Failed to fetch user",
|
|
2010
|
+
errorCode: ZeroXKeyErrorCodes.FETCH_USER_ERROR,
|
|
2011
|
+
});
|
|
2012
|
+
};
|
|
2013
|
+
/**
|
|
2014
|
+
* Fetches an existing user by P-256 API key public key, or creates a new one if none exists.
|
|
2015
|
+
*
|
|
2016
|
+
* - This function is idempotent: multiple calls with the same `publicKey` will always return the same user.
|
|
2017
|
+
* - Attempts to find a user whose API keys include the given P-256 public key.
|
|
2018
|
+
* - If a matching user is found, it is returned as-is.
|
|
2019
|
+
* - If no matching user is found, a new user is created with the given public key as a P-256 API key.
|
|
2020
|
+
*
|
|
2021
|
+
* @param params.publicKey - the P-256 public key to use for lookup and creation.
|
|
2022
|
+
* @param params.createParams.userName - optional username to assign if creating a new user (defaults to `"Public Key User"`).
|
|
2023
|
+
* @param params.createParams.apiKeyName - optional API key name to assign if creating a new API key (defaults to `public-key-user-${publicKey}`).
|
|
2024
|
+
* @param params.organizationId - organization ID to specify the sub-organization (defaults to the current session's organizationId).
|
|
2025
|
+
* @param params.stampWith - parameter to stamp the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2026
|
+
* @returns A promise that resolves to the existing or newly created {@link v1User}.
|
|
2027
|
+
* @throws {ZeroXKeyError} If there is no active session, if the input is invalid, if user retrieval fails, or if user creation fails.
|
|
2028
|
+
*/
|
|
2029
|
+
this.fetchOrCreateP256ApiKeyUser = async (params) => {
|
|
2030
|
+
const { publicKey, createParams, stampWith = this.config.defaultStamperType, organizationId: organizationIdFromParams, } = params;
|
|
2031
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
2032
|
+
const session = await getActiveSessionOrThrowIfRequired(stampWith, this.storageManager.getActiveSession);
|
|
2033
|
+
const organizationId = organizationIdFromParams || session?.organizationId;
|
|
2034
|
+
if (!organizationId) {
|
|
2035
|
+
throw new ZeroXKeyError("Organization ID is required to fetch or create P-256 API key user.", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
2036
|
+
}
|
|
2037
|
+
// we validate their input
|
|
2038
|
+
if (!publicKey?.trim()) {
|
|
2039
|
+
throw new ZeroXKeyError("'publicKey' is required and cannot be empty.", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
2040
|
+
}
|
|
2041
|
+
const usersResponse = await this.httpClient.getUsers({
|
|
2042
|
+
organizationId,
|
|
2043
|
+
}, stampWith);
|
|
2044
|
+
if (!usersResponse || !usersResponse.users) {
|
|
2045
|
+
throw new ZeroXKeyError("No users found in the response", ZeroXKeyErrorCodes.BAD_RESPONSE);
|
|
2046
|
+
}
|
|
2047
|
+
const userWithPublicKey = usersResponse.users.find((user) => user.apiKeys.some((apiKey) => apiKey.credential.publicKey === publicKey &&
|
|
2048
|
+
apiKey.credential.type === "CREDENTIAL_TYPE_API_KEY_P256"));
|
|
2049
|
+
// the user already exists, so we return it
|
|
2050
|
+
if (userWithPublicKey) {
|
|
2051
|
+
return userWithPublicKey;
|
|
2052
|
+
}
|
|
2053
|
+
// at this point we know the user doesn't exist, so we create it
|
|
2054
|
+
const userName = createParams?.userName?.trim() || "Public Key User";
|
|
2055
|
+
const apiKeyName = createParams?.apiKeyName?.trim() || `public-key-user-${publicKey}`;
|
|
2056
|
+
const createUserResp = await this.httpClient.createUsers({
|
|
2057
|
+
organizationId,
|
|
2058
|
+
users: [
|
|
2059
|
+
{
|
|
2060
|
+
userName: userName,
|
|
2061
|
+
userTags: [],
|
|
2062
|
+
apiKeys: [
|
|
2063
|
+
{
|
|
2064
|
+
apiKeyName: apiKeyName,
|
|
2065
|
+
curveType: "API_KEY_CURVE_P256",
|
|
2066
|
+
publicKey,
|
|
2067
|
+
},
|
|
2068
|
+
],
|
|
2069
|
+
authenticators: [],
|
|
2070
|
+
oauthProviders: [],
|
|
2071
|
+
},
|
|
2072
|
+
],
|
|
2073
|
+
}, stampWith);
|
|
2074
|
+
if (!createUserResp?.userIds ||
|
|
2075
|
+
createUserResp.userIds.length === 0 ||
|
|
2076
|
+
!createUserResp.userIds[0]) {
|
|
2077
|
+
throw new ZeroXKeyError("Failed to create P-256 API key user", ZeroXKeyErrorCodes.CREATE_USERS_ERROR);
|
|
2078
|
+
}
|
|
2079
|
+
const newUserId = createUserResp.userIds[0];
|
|
2080
|
+
return await this.fetchUser({
|
|
2081
|
+
organizationId,
|
|
2082
|
+
userId: newUserId,
|
|
2083
|
+
stampWith,
|
|
2084
|
+
});
|
|
2085
|
+
}, {
|
|
2086
|
+
errorMessage: "Failed to get or create P-256 API key user",
|
|
2087
|
+
errorCode: ZeroXKeyErrorCodes.CREATE_USERS_ERROR,
|
|
2088
|
+
});
|
|
2089
|
+
};
|
|
2090
|
+
/**
|
|
2091
|
+
* Fetches each requested policy if it exists, or creates it if it does not.
|
|
2092
|
+
*
|
|
2093
|
+
* - This function is idempotent: multiple calls with the same policies will not create duplicates.
|
|
2094
|
+
* - For every policy in the request:
|
|
2095
|
+
* - If it already exists, it is returned with its `policyId`.
|
|
2096
|
+
* - If it does not exist, it is created and returned with its new `policyId`.
|
|
2097
|
+
*
|
|
2098
|
+
* @param params.policies - the list of policies to fetch or create.
|
|
2099
|
+
* @param params.stampWith - parameter to stamp the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2100
|
+
* @param params.organizationId - organization ID to specify the sub-organization (defaults to the current session's organizationId).
|
|
2101
|
+
* @returns A promise that resolves to an array of objects, each containing:
|
|
2102
|
+
* - `policyId`: the unique identifier of the policy.
|
|
2103
|
+
* - `policyName`: human-readable name of the policy.
|
|
2104
|
+
* - `effect`: the instruction to DENY or ALLOW an activity.
|
|
2105
|
+
* - `condition`: (optional) the condition expression that triggers the effect.
|
|
2106
|
+
* - `consensus`: (optional) the consensus expression that triggers the effect.
|
|
2107
|
+
* - `notes`: (optional) developer notes or description for the policy.
|
|
2108
|
+
* @throws {ZeroXKeyError} If there is no active session, if the input is invalid,
|
|
2109
|
+
* if fetching policies fails, or if creating policies fails.
|
|
2110
|
+
*/
|
|
2111
|
+
this.fetchOrCreatePolicies = async (params) => {
|
|
2112
|
+
const { policies, stampWith = this.config.defaultStamperType } = params;
|
|
2113
|
+
return await withZeroXKeyErrorHandling(async () => {
|
|
2114
|
+
const session = await getActiveSessionOrThrowIfRequired(stampWith, this.storageManager.getActiveSession);
|
|
2115
|
+
if (!Array.isArray(policies) || policies.length === 0) {
|
|
2116
|
+
throw new ZeroXKeyError("'policies' must be a non-empty array of policy definitions.", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
2117
|
+
}
|
|
2118
|
+
const organizationId = params?.organizationId ?? session?.organizationId;
|
|
2119
|
+
if (!organizationId) {
|
|
2120
|
+
throw new ZeroXKeyError("Organization ID is required to fetch or create policies.", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
2121
|
+
}
|
|
2122
|
+
// we first fetch existing policies
|
|
2123
|
+
const existingPoliciesResponse = await this.httpClient.getPolicies({
|
|
2124
|
+
organizationId,
|
|
2125
|
+
}, stampWith);
|
|
2126
|
+
const existingPolicies = existingPoliciesResponse.policies || [];
|
|
2127
|
+
// we create a map of existing policies by their signature
|
|
2128
|
+
// where the policySignature maps to its policyId
|
|
2129
|
+
const existingPoliciesSignatureMap = {};
|
|
2130
|
+
for (const existingPolicy of existingPolicies) {
|
|
2131
|
+
const signature = getPolicySignature(existingPolicy);
|
|
2132
|
+
existingPoliciesSignatureMap[signature] = existingPolicy.policyId;
|
|
2133
|
+
}
|
|
2134
|
+
// we go through each requested policy and check if it already exists
|
|
2135
|
+
// if it exists, we add it to the alreadyExistingPolicies list
|
|
2136
|
+
// if it doesn't exist, we add it to the missingPolicies list
|
|
2137
|
+
const alreadyExistingPolicies = [];
|
|
2138
|
+
const missingPolicies = [];
|
|
2139
|
+
for (const policy of policies) {
|
|
2140
|
+
const existingId = existingPoliciesSignatureMap[getPolicySignature(policy)];
|
|
2141
|
+
if (existingId) {
|
|
2142
|
+
alreadyExistingPolicies.push({ ...policy, policyId: existingId });
|
|
2143
|
+
}
|
|
2144
|
+
else {
|
|
2145
|
+
missingPolicies.push(policy);
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
// if there are no missing policies, that means we're done
|
|
2149
|
+
// so we return them with their respective IDs
|
|
2150
|
+
if (missingPolicies.length === 0) {
|
|
2151
|
+
return alreadyExistingPolicies;
|
|
2152
|
+
}
|
|
2153
|
+
// at this point we know there is at least one missing policy.
|
|
2154
|
+
// so we create the missing policies and then return the full list
|
|
2155
|
+
const createPoliciesResponse = await this.httpClient.createPolicies({
|
|
2156
|
+
organizationId,
|
|
2157
|
+
policies: missingPolicies,
|
|
2158
|
+
}, stampWith);
|
|
2159
|
+
// assign returned IDs back to the missing ones in order
|
|
2160
|
+
if (!createPoliciesResponse || !createPoliciesResponse.policyIds) {
|
|
2161
|
+
throw new ZeroXKeyError("Failed to create missing policies", ZeroXKeyErrorCodes.CREATE_POLICY_ERROR);
|
|
2162
|
+
}
|
|
2163
|
+
const newlyCreatedPolicies = missingPolicies.map((p, idx) => ({
|
|
2164
|
+
...p,
|
|
2165
|
+
// we can safely assert the ID exists because we know ZeroXKey's api
|
|
2166
|
+
// will return one ID for each created policy or throw an error
|
|
2167
|
+
policyId: createPoliciesResponse.policyIds[idx],
|
|
2168
|
+
}));
|
|
2169
|
+
// we return the full list of policies, both existing and the newly created
|
|
2170
|
+
// which includes each of their respective IDs
|
|
2171
|
+
return [...alreadyExistingPolicies, ...newlyCreatedPolicies];
|
|
2172
|
+
}, {
|
|
2173
|
+
errorMessage: "Failed to get or create policies",
|
|
2174
|
+
errorCode: ZeroXKeyErrorCodes.CREATE_USERS_ERROR,
|
|
2175
|
+
});
|
|
2176
|
+
};
|
|
2177
|
+
/**
|
|
2178
|
+
* Updates the user's email address.
|
|
2179
|
+
*
|
|
2180
|
+
* - This function updates the user's email address and, if provided, verifies it using a verification token (typically from an OTP flow).
|
|
2181
|
+
* - If a userId is provided, it updates the email for that specific user; otherwise, it uses the current session's userId.
|
|
2182
|
+
* - If a verificationToken is not provided, the email will be updated but will not be marked as verified.
|
|
2183
|
+
* - Automatically ensures an active session exists before making the request.
|
|
2184
|
+
* - Handles session management and error reporting for both update and verification flows.
|
|
2185
|
+
*
|
|
2186
|
+
* @param params.email - new email address to set for the user.
|
|
2187
|
+
* @param params.verificationToken - verification token from OTP email verification (required if verifying the email).
|
|
2188
|
+
* @param params.userId - user ID to update a specific user's email (defaults to the current session's userId).
|
|
2189
|
+
* @param params.stampWith - parameter to stamp the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2190
|
+
* @param params.organizationId - organization ID to specify the sub-organization (defaults to the current session's organizationId).
|
|
2191
|
+
* @returns A promise that resolves to the userId of the updated user.
|
|
2192
|
+
* @throws {ZeroXKeyError} If there is no active session, if the userId is missing, or if there is an error updating or verifying the user email.
|
|
2193
|
+
*/
|
|
2194
|
+
this.updateUserEmail = async (params) => {
|
|
2195
|
+
const { verificationToken, email, stampWith = this.config.defaultStamperType, organizationId, } = params;
|
|
2196
|
+
const session = await getActiveSessionOrThrowIfRequired(stampWith, this.storageManager.getActiveSession);
|
|
2197
|
+
const userId = params?.userId || session?.userId;
|
|
2198
|
+
if (!userId) {
|
|
2199
|
+
throw new ZeroXKeyError("User ID must be provided to update user email", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
2200
|
+
}
|
|
2201
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
2202
|
+
if (verificationToken) {
|
|
2203
|
+
const existingUser = await this.httpClient.proxyGetAccount({
|
|
2204
|
+
filterType: FilterType.Email,
|
|
2205
|
+
filterValue: email,
|
|
2206
|
+
verificationToken,
|
|
2207
|
+
});
|
|
2208
|
+
if (existingUser.organizationId) {
|
|
2209
|
+
throw new ZeroXKeyError(`Email ${email} is already associated with another user.`, ZeroXKeyErrorCodes.ACCOUNT_ALREADY_EXISTS);
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
const res = await this.httpClient.updateUserEmail({
|
|
2213
|
+
userId: userId,
|
|
2214
|
+
userEmail: email,
|
|
2215
|
+
...(verificationToken && { verificationToken }),
|
|
2216
|
+
...(organizationId && { organizationId }),
|
|
2217
|
+
}, stampWith);
|
|
2218
|
+
if (!res || !res.userId) {
|
|
2219
|
+
throw new ZeroXKeyError("No user ID found in the update user email response", ZeroXKeyErrorCodes.BAD_RESPONSE);
|
|
2220
|
+
}
|
|
2221
|
+
return res.userId;
|
|
2222
|
+
}, {
|
|
2223
|
+
errorMessage: "Failed to update user email",
|
|
2224
|
+
errorCode: ZeroXKeyErrorCodes.UPDATE_USER_EMAIL_ERROR,
|
|
2225
|
+
});
|
|
2226
|
+
};
|
|
2227
|
+
/**
|
|
2228
|
+
* Removes the user's email address.
|
|
2229
|
+
*
|
|
2230
|
+
* - This function removes the user's email address by setting it to an empty string.
|
|
2231
|
+
* - If a userId is provided, it removes the email for that specific user; otherwise, it uses the current session's userId.
|
|
2232
|
+
* - Automatically ensures an active session exists before making the request.
|
|
2233
|
+
* - Optionally allows stamping the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2234
|
+
*
|
|
2235
|
+
* @param params.userId - user ID to remove a specific user's email address (defaults to the current session's userId).
|
|
2236
|
+
* @param params.stampWith - parameter to stamp the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2237
|
+
* @param params.organizationId - organization ID to specify the sub-organization (defaults to the current session's organizationId).
|
|
2238
|
+
* @returns A promise that resolves to the userId of the user whose email was removed.
|
|
2239
|
+
* @throws {ZeroXKeyError} If there is no active session, if the userId is missing, or if there is an error removing the user email.
|
|
2240
|
+
*/
|
|
2241
|
+
this.removeUserEmail = async (params) => {
|
|
2242
|
+
const { stampWith = this.config.defaultStamperType, organizationId } = params || {};
|
|
2243
|
+
const session = await getActiveSessionOrThrowIfRequired(stampWith, this.storageManager.getActiveSession);
|
|
2244
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
2245
|
+
const userId = params?.userId || session?.userId;
|
|
2246
|
+
if (!userId) {
|
|
2247
|
+
throw new ZeroXKeyError("User ID must be provided to remove user email", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
2248
|
+
}
|
|
2249
|
+
const res = await this.httpClient.updateUserEmail({
|
|
2250
|
+
userId: userId,
|
|
2251
|
+
userEmail: "",
|
|
2252
|
+
...(organizationId && { organizationId }),
|
|
2253
|
+
}, stampWith);
|
|
2254
|
+
if (!res || !res.userId) {
|
|
2255
|
+
throw new ZeroXKeyError("No user ID found in the remove user email response", ZeroXKeyErrorCodes.BAD_RESPONSE);
|
|
2256
|
+
}
|
|
2257
|
+
return res.userId;
|
|
2258
|
+
}, {
|
|
2259
|
+
errorMessage: "Failed to remove user email",
|
|
2260
|
+
errorCode: ZeroXKeyErrorCodes.UPDATE_USER_EMAIL_ERROR,
|
|
2261
|
+
});
|
|
2262
|
+
};
|
|
2263
|
+
/**
|
|
2264
|
+
* Updates the user's phone number.
|
|
2265
|
+
*
|
|
2266
|
+
* - This function updates the user's phone number and, if provided, verifies it using a verification token (from an OTP flow).
|
|
2267
|
+
* - If a userId is provided, it updates the phone number for that specific user; otherwise, it uses the current session's userId.
|
|
2268
|
+
* - If a verificationToken is not provided, the phone number will be updated but will not be marked as verified.
|
|
2269
|
+
* - Automatically ensures an active session exists before making the request.
|
|
2270
|
+
* - Handles session management and error reporting for both update and verification flows.
|
|
2271
|
+
*
|
|
2272
|
+
* @param params.phoneNumber - new phone number to set for the user.
|
|
2273
|
+
* @param params.verificationToken - verification token from OTP phone verification (required if verifying the phone number).
|
|
2274
|
+
* @param params.userId - user ID to update a specific user's phone number (defaults to the current session's userId).
|
|
2275
|
+
* @param params.stampWith - parameter to stamp the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2276
|
+
* @param params.organizationId - organization ID to specify the sub-organization (defaults to the current session's organizationId).
|
|
2277
|
+
* @returns A promise that resolves to the userId of the updated user.
|
|
2278
|
+
* @throws {ZeroXKeyError} If there is no active session, if the userId is missing, or if there is an error updating or verifying the user phone number.
|
|
2279
|
+
*/
|
|
2280
|
+
this.updateUserPhoneNumber = async (params) => {
|
|
2281
|
+
const { verificationToken, phoneNumber, stampWith = this.config.defaultStamperType, organizationId, } = params;
|
|
2282
|
+
const session = await getActiveSessionOrThrowIfRequired(stampWith, this.storageManager.getActiveSession);
|
|
2283
|
+
const userId = params?.userId || session?.userId;
|
|
2284
|
+
if (!userId) {
|
|
2285
|
+
throw new ZeroXKeyError("User ID must be provided to update user phone number", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
2286
|
+
}
|
|
2287
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
2288
|
+
if (verificationToken) {
|
|
2289
|
+
const existingUser = await this.httpClient.proxyGetAccount({
|
|
2290
|
+
filterType: FilterType.Sms,
|
|
2291
|
+
filterValue: phoneNumber,
|
|
2292
|
+
verificationToken,
|
|
2293
|
+
});
|
|
2294
|
+
if (existingUser.organizationId) {
|
|
2295
|
+
throw new ZeroXKeyError(`Phone number ${phoneNumber} is already associated with another user.`, ZeroXKeyErrorCodes.ACCOUNT_ALREADY_EXISTS);
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
const res = await this.httpClient.updateUserPhoneNumber({
|
|
2299
|
+
userId,
|
|
2300
|
+
userPhoneNumber: phoneNumber,
|
|
2301
|
+
...(verificationToken && { verificationToken }),
|
|
2302
|
+
...(organizationId && { organizationId }),
|
|
2303
|
+
}, stampWith);
|
|
2304
|
+
if (!res || !res.userId) {
|
|
2305
|
+
throw new ZeroXKeyError("Failed to update user phone number", ZeroXKeyErrorCodes.UPDATE_USER_PHONE_NUMBER_ERROR);
|
|
2306
|
+
}
|
|
2307
|
+
return res.userId;
|
|
2308
|
+
}, {
|
|
2309
|
+
errorMessage: "Failed to update user phone number",
|
|
2310
|
+
errorCode: ZeroXKeyErrorCodes.UPDATE_USER_PHONE_NUMBER_ERROR,
|
|
2311
|
+
});
|
|
2312
|
+
};
|
|
2313
|
+
/**
|
|
2314
|
+
* Removes the user's phone number.
|
|
2315
|
+
*
|
|
2316
|
+
* - This function removes the user's phone number by setting it to an empty string.
|
|
2317
|
+
* - If a userId is provided, it removes the phone number for that specific user; otherwise, it uses the current session's userId.
|
|
2318
|
+
* - Automatically ensures an active session exists before making the request.
|
|
2319
|
+
* - Optionally allows stamping the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2320
|
+
*
|
|
2321
|
+
* @param params.userId - user ID to remove a specific user's phone number (defaults to the current session's userId).
|
|
2322
|
+
* @param params.stampWith - parameter to stamp the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2323
|
+
* @param params.organizationId - organization ID to specify the sub-organization (defaults to the current session's organizationId).
|
|
2324
|
+
* @returns A promise that resolves to the userId of the user whose phone number was removed.
|
|
2325
|
+
* @throws {ZeroXKeyError} If there is no active session, if the userId is missing, or if there is an error removing the user phone number.
|
|
2326
|
+
*/
|
|
2327
|
+
this.removeUserPhoneNumber = async (params) => {
|
|
2328
|
+
const { stampWith = this.config.defaultStamperType, organizationId } = params || {};
|
|
2329
|
+
const session = await getActiveSessionOrThrowIfRequired(stampWith, this.storageManager.getActiveSession);
|
|
2330
|
+
const userId = params?.userId || session?.userId;
|
|
2331
|
+
if (!userId) {
|
|
2332
|
+
throw new ZeroXKeyError("User ID must be provided to remove user phone number", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
2333
|
+
}
|
|
2334
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
2335
|
+
const res = await this.httpClient.updateUserPhoneNumber({
|
|
2336
|
+
userId,
|
|
2337
|
+
userPhoneNumber: "",
|
|
2338
|
+
...(organizationId && { organizationId }),
|
|
2339
|
+
}, stampWith);
|
|
2340
|
+
if (!res || !res.userId) {
|
|
2341
|
+
throw new ZeroXKeyError("Failed to remove user phone number", ZeroXKeyErrorCodes.UPDATE_USER_PHONE_NUMBER_ERROR);
|
|
2342
|
+
}
|
|
2343
|
+
return res.userId;
|
|
2344
|
+
}, {
|
|
2345
|
+
errorMessage: "Failed to remove user phone number",
|
|
2346
|
+
errorCode: ZeroXKeyErrorCodes.UPDATE_USER_PHONE_NUMBER_ERROR,
|
|
2347
|
+
});
|
|
2348
|
+
};
|
|
2349
|
+
/**
|
|
2350
|
+
* Updates the user's name.
|
|
2351
|
+
*
|
|
2352
|
+
* - This function updates the user's display name.
|
|
2353
|
+
* - If a userId is provided, it updates the name for that specific user; otherwise, it uses the current session's userId.
|
|
2354
|
+
* - Automatically ensures an active session exists before making the request.
|
|
2355
|
+
* - Optionally allows stamping the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2356
|
+
* - Handles session management and error reporting for the update flow.
|
|
2357
|
+
*
|
|
2358
|
+
* @param params.userName - new name to set for the user.
|
|
2359
|
+
* @param params.userId - user ID to update a specific user's name (defaults to the current session's userId).
|
|
2360
|
+
* @param params.stampWith - parameter to stamp the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2361
|
+
* @param params.organizationId - organization ID to specify the sub-organization (defaults to the current session's organizationId).
|
|
2362
|
+
* @returns A promise that resolves to the userId of the updated user.
|
|
2363
|
+
* @throws {ZeroXKeyError} If there is no active session, if the userId is missing, or if there is an error updating the user name.
|
|
2364
|
+
*/
|
|
2365
|
+
this.updateUserName = async (params) => {
|
|
2366
|
+
const { userName, stampWith = this.config.defaultStamperType, organizationId, } = params;
|
|
2367
|
+
const session = await getActiveSessionOrThrowIfRequired(stampWith, this.storageManager.getActiveSession);
|
|
2368
|
+
const userId = params?.userId || session?.userId;
|
|
2369
|
+
if (!userId) {
|
|
2370
|
+
throw new ZeroXKeyError("User ID must be provided to update user name", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
2371
|
+
}
|
|
2372
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
2373
|
+
const res = await this.httpClient.updateUserName({
|
|
2374
|
+
userId,
|
|
2375
|
+
userName,
|
|
2376
|
+
...(organizationId && { organizationId }),
|
|
2377
|
+
}, stampWith);
|
|
2378
|
+
if (!res || !res.userId) {
|
|
2379
|
+
throw new ZeroXKeyError("No user ID found in the update user name response", ZeroXKeyErrorCodes.BAD_RESPONSE);
|
|
2380
|
+
}
|
|
2381
|
+
return res.userId;
|
|
2382
|
+
}, {
|
|
2383
|
+
errorMessage: "Failed to update user name",
|
|
2384
|
+
errorCode: ZeroXKeyErrorCodes.UPDATE_USER_NAME_ERROR,
|
|
2385
|
+
});
|
|
2386
|
+
};
|
|
2387
|
+
/**
|
|
2388
|
+
* Adds an OAuth provider to the user.
|
|
2389
|
+
*
|
|
2390
|
+
* - This function adds an OAuth provider (e.g., Google, Apple) to the user account.
|
|
2391
|
+
* - If a userId is provided, it adds the provider for that specific user; otherwise, it uses the current session's userId.
|
|
2392
|
+
* - Automatically checks if an account already exists for the provided OIDC token and prevents duplicate associations.
|
|
2393
|
+
* - If the user's email is not set or not verified, attempts to update and verify the email using the email from the OIDC token.
|
|
2394
|
+
* - Handles session management and error reporting for the add provider flow.
|
|
2395
|
+
* - Optionally allows stamping the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2396
|
+
*
|
|
2397
|
+
* @param params.providerName - name of the OAuth provider to add (e.g., "Google", "Apple").
|
|
2398
|
+
* @param params.oidcToken - OIDC token for the OAuth provider.
|
|
2399
|
+
* @param params.organizationId - organization ID to specify the sub-organization (defaults to the current session's organizationId).
|
|
2400
|
+
* @param params.userId - user ID to add the provider for a specific user (defaults to current session's userId).
|
|
2401
|
+
* @param params.stampWith - parameter to stamp the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2402
|
+
* @returns A promise that resolves to an array of provider IDs associated with the user.
|
|
2403
|
+
* @throws {ZeroXKeyError} If there is no active session, if the account already exists, or if there is an error adding the OAuth provider.
|
|
2404
|
+
*/
|
|
2405
|
+
this.addOauthProvider = async (params) => {
|
|
2406
|
+
const { providerName, oidcToken, stampWith = this.config.defaultStamperType, } = params;
|
|
2407
|
+
const session = await getActiveSessionOrThrowIfRequired(stampWith, this.storageManager.getActiveSession);
|
|
2408
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
2409
|
+
const accountRes = await this.httpClient.proxyGetAccount({
|
|
2410
|
+
filterType: "OIDC_TOKEN",
|
|
2411
|
+
filterValue: oidcToken,
|
|
2412
|
+
});
|
|
2413
|
+
if (!accountRes) {
|
|
2414
|
+
throw new ZeroXKeyError(`Account fetch failed`, ZeroXKeyErrorCodes.ACCOUNT_FETCH_ERROR);
|
|
2415
|
+
}
|
|
2416
|
+
if (accountRes.organizationId &&
|
|
2417
|
+
accountRes.organizationId !== session?.organizationId) {
|
|
2418
|
+
throw new ZeroXKeyError("Account already exists with this OIDC token", ZeroXKeyErrorCodes.ACCOUNT_ALREADY_EXISTS);
|
|
2419
|
+
}
|
|
2420
|
+
const userId = params?.userId || session?.userId;
|
|
2421
|
+
if (!userId) {
|
|
2422
|
+
throw new ZeroXKeyError("User ID must be provided to add OAuth provider", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
2423
|
+
}
|
|
2424
|
+
const organizationId = params?.organizationId ?? session?.organizationId;
|
|
2425
|
+
if (!organizationId) {
|
|
2426
|
+
throw new ZeroXKeyError("Organization ID is required to add OAuth provider", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
2427
|
+
}
|
|
2428
|
+
// parse the oidc token so we can get the email. Pass it in to updateUser then call createOauthProviders. This will be verified by ZeroXKey.
|
|
2429
|
+
const { email: oidcEmail, iss } = jwtDecode(oidcToken) || {};
|
|
2430
|
+
if (iss === googleISS) {
|
|
2431
|
+
const verifiedSuborg = await this.httpClient.proxyGetAccount({
|
|
2432
|
+
filterType: "EMAIL",
|
|
2433
|
+
filterValue: oidcEmail,
|
|
2434
|
+
oidcToken: oidcToken,
|
|
2435
|
+
});
|
|
2436
|
+
const isVerified = verifiedSuborg.organizationId === organizationId;
|
|
2437
|
+
const user = await this.fetchUser({
|
|
2438
|
+
userId,
|
|
2439
|
+
stampWith,
|
|
2440
|
+
});
|
|
2441
|
+
// this block's pupose is for social linking, it's important that we update the email BEFORE we call createOauthProviders
|
|
2442
|
+
// since for social linking createOauthProviders will mark the email as verified as long as it's the same one that lives in the
|
|
2443
|
+
// OIDC token.
|
|
2444
|
+
if (!user?.userEmail && !isVerified) {
|
|
2445
|
+
// we cannot use our sugared updateUserEmail here since we need to pass in the oidcToken in case Require Verification Token On Account Lookup is enabled
|
|
2446
|
+
// in the dashboard
|
|
2447
|
+
const existingUser = await this.httpClient.proxyGetAccount({
|
|
2448
|
+
filterType: FilterType.Email,
|
|
2449
|
+
filterValue: oidcEmail,
|
|
2450
|
+
oidcToken: oidcToken,
|
|
2451
|
+
});
|
|
2452
|
+
if (existingUser.organizationId) {
|
|
2453
|
+
throw new ZeroXKeyError(`Email ${oidcEmail} is already associated with another user.`, ZeroXKeyErrorCodes.ACCOUNT_ALREADY_EXISTS);
|
|
2454
|
+
}
|
|
2455
|
+
// update and verify their email since we got it from a verified OIDC token
|
|
2456
|
+
await this.httpClient.updateUserEmail({
|
|
2457
|
+
userId,
|
|
2458
|
+
userEmail: oidcEmail,
|
|
2459
|
+
organizationId,
|
|
2460
|
+
}, stampWith);
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
const createProviderRes = await this.httpClient.createOauthProviders({
|
|
2464
|
+
userId,
|
|
2465
|
+
oauthProviders: [
|
|
2466
|
+
{
|
|
2467
|
+
providerName,
|
|
2468
|
+
oidcToken,
|
|
2469
|
+
},
|
|
2470
|
+
],
|
|
2471
|
+
}, stampWith);
|
|
2472
|
+
if (!createProviderRes) {
|
|
2473
|
+
throw new ZeroXKeyError("Failed to create OAuth provider", ZeroXKeyErrorCodes.ADD_OAUTH_PROVIDER_ERROR);
|
|
2474
|
+
}
|
|
2475
|
+
return createProviderRes?.providerIds || [];
|
|
2476
|
+
}, {
|
|
2477
|
+
errorMessage: "Failed to add OAuth provider",
|
|
2478
|
+
errorCode: ZeroXKeyErrorCodes.ADD_OAUTH_PROVIDER_ERROR,
|
|
2479
|
+
});
|
|
2480
|
+
};
|
|
2481
|
+
/**
|
|
2482
|
+
* Removes a list of OAuth providers from the user.
|
|
2483
|
+
*
|
|
2484
|
+
* - This function removes OAuth providers (e.g., Google, Apple) from the user's account.
|
|
2485
|
+
* - If a userId is provided, it removes the providers for that specific user; otherwise, it uses the current session's userId.
|
|
2486
|
+
* - Automatically ensures an active session exists before making the request.
|
|
2487
|
+
* - Optionally allows stamping the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2488
|
+
* - Returns an array of remaining provider IDs associated with the user after removal.
|
|
2489
|
+
*
|
|
2490
|
+
* @param params.providerIds - IDs of the OAuth providers to remove.
|
|
2491
|
+
* @param params.userId - user ID to remove the provider for a specific user (defaults to the current session's userId).
|
|
2492
|
+
* @param params.stampWith - parameter to stamp the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2493
|
+
* @param params.organizationId - organization ID to specify the sub-organization (defaults to the current session's organizationId).
|
|
2494
|
+
* @returns A promise that resolves to an array of provider IDs that were removed.
|
|
2495
|
+
* @throws {ZeroXKeyError} If there is no active session, if the userId is missing, or if there is an error removing the OAuth provider.
|
|
2496
|
+
*/
|
|
2497
|
+
this.removeOauthProviders = async (params) => {
|
|
2498
|
+
const { providerIds, stampWith = this.config.defaultStamperType, organizationId, } = params;
|
|
2499
|
+
const session = await getActiveSessionOrThrowIfRequired(stampWith, this.storageManager.getActiveSession);
|
|
2500
|
+
const userId = params?.userId || session?.userId;
|
|
2501
|
+
if (!userId) {
|
|
2502
|
+
throw new ZeroXKeyError("User ID must be provided to remove OAuth provider", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
2503
|
+
}
|
|
2504
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
2505
|
+
const res = await this.httpClient.deleteOauthProviders({
|
|
2506
|
+
userId,
|
|
2507
|
+
providerIds,
|
|
2508
|
+
...(organizationId && { organizationId }),
|
|
2509
|
+
}, stampWith);
|
|
2510
|
+
if (!res) {
|
|
2511
|
+
throw new ZeroXKeyError("Failed to remove OAuth provider", ZeroXKeyErrorCodes.REMOVE_OAUTH_PROVIDER_ERROR);
|
|
2512
|
+
}
|
|
2513
|
+
return res.providerIds;
|
|
2514
|
+
}, {
|
|
2515
|
+
errorMessage: "Failed to remove OAuth provider",
|
|
2516
|
+
errorCode: ZeroXKeyErrorCodes.REMOVE_OAUTH_PROVIDER_ERROR,
|
|
2517
|
+
});
|
|
2518
|
+
};
|
|
2519
|
+
/**
|
|
2520
|
+
* Adds a new passkey authenticator for the user.
|
|
2521
|
+
*
|
|
2522
|
+
* - This function prompts the user to create a new passkey (WebAuthn/FIDO2) and adds it as an authenticator for the user.
|
|
2523
|
+
* - Handles both web and React Native environments, automatically selecting the appropriate passkey creation flow.
|
|
2524
|
+
* - If a userId is provided, the passkey is added for that specific user; otherwise, it uses the current session's userId.
|
|
2525
|
+
* - The passkey's name and display name can be customized; if not provided, defaults are generated.
|
|
2526
|
+
* - The resulting passkey attestation and challenge are registered with ZeroXKey as a new authenticator.
|
|
2527
|
+
*
|
|
2528
|
+
* @param params.name - name of the passkey (defaults to "ZeroXKey Passkey-`timestamp`").
|
|
2529
|
+
* @param params.displayName - display name of the passkey (defaults to the value of `name`).
|
|
2530
|
+
* @param params.userId - user ID to add the passkey for a specific user (defaults to the current session's userId).
|
|
2531
|
+
* @param params.stampWith - parameter to stamp the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2532
|
+
* @param params.organizationId - organization ID to specify the sub-organization (defaults to the current session's organizationId).
|
|
2533
|
+
* @returns A promise that resolves to an array of authenticator IDs for the newly added passkey(s).
|
|
2534
|
+
* @throws {ZeroXKeyError} If there is no active session, if passkey creation fails, or if there is an error adding the passkey.
|
|
2535
|
+
*/
|
|
2536
|
+
this.addPasskey = async (params) => {
|
|
2537
|
+
const { stampWith = this.config.defaultStamperType, organizationId } = params || {};
|
|
2538
|
+
const name = params?.name || `ZeroXKey Passkey-${Date.now()}`;
|
|
2539
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
2540
|
+
const session = await getActiveSessionOrThrowIfRequired(stampWith, this.storageManager.getActiveSession);
|
|
2541
|
+
const userId = params?.userId || session?.userId;
|
|
2542
|
+
if (!userId) {
|
|
2543
|
+
throw new ZeroXKeyError("User ID must be provided to add passkey", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
2544
|
+
}
|
|
2545
|
+
const { encodedChallenge, attestation } = await this.createPasskey({
|
|
2546
|
+
name,
|
|
2547
|
+
});
|
|
2548
|
+
if (!attestation || !encodedChallenge) {
|
|
2549
|
+
throw new ZeroXKeyError("Failed to create passkey challenge and attestation", ZeroXKeyErrorCodes.CREATE_PASSKEY_ERROR);
|
|
2550
|
+
}
|
|
2551
|
+
const res = await this.httpClient.createAuthenticators({
|
|
2552
|
+
userId,
|
|
2553
|
+
authenticators: [
|
|
2554
|
+
{
|
|
2555
|
+
authenticatorName: name,
|
|
2556
|
+
challenge: encodedChallenge,
|
|
2557
|
+
attestation,
|
|
2558
|
+
},
|
|
2559
|
+
],
|
|
2560
|
+
...(organizationId && { organizationId }),
|
|
2561
|
+
}, stampWith);
|
|
2562
|
+
return res?.authenticatorIds || [];
|
|
2563
|
+
}, {
|
|
2564
|
+
errorMessage: "Failed to add passkey",
|
|
2565
|
+
errorCode: ZeroXKeyErrorCodes.ADD_PASSKEY_ERROR,
|
|
2566
|
+
});
|
|
2567
|
+
};
|
|
2568
|
+
/**
|
|
2569
|
+
* Removes passkeys (authenticator) from the user.
|
|
2570
|
+
*
|
|
2571
|
+
* - This function removes passkeys (WebAuthn/FIDO2 authenticators) from the user's account.
|
|
2572
|
+
* - If a userId is provided, it removes the passkeys for that specific user; otherwise, it uses the current session's userId.
|
|
2573
|
+
* - Automatically ensures an active session exists before making the request.
|
|
2574
|
+
* - Optionally allows stamping the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2575
|
+
* - Returns an array of remaining authenticator IDs for the user after removal.
|
|
2576
|
+
*
|
|
2577
|
+
* @param params.authenticatorIds - IDs of the authenticators (passkeys) to remove.
|
|
2578
|
+
* @param params.userId - user ID to remove the passkeys for a specific user (defaults to the current session's userId).
|
|
2579
|
+
* @param params.stampWith - parameter to stamp the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2580
|
+
* @param params.organizationId - organization ID to specify the sub-organization (defaults to the current session's organizationId).
|
|
2581
|
+
* @returns A promise that resolves to an array of authenticator IDs that were removed.
|
|
2582
|
+
* @throws {ZeroXKeyError} If there is no active session, if the userId is missing, or if there is an error removing the passkeys.
|
|
2583
|
+
*/
|
|
2584
|
+
this.removePasskeys = async (params) => {
|
|
2585
|
+
const { authenticatorIds, stampWith = this.config.defaultStamperType, organizationId, } = params;
|
|
2586
|
+
const session = await getActiveSessionOrThrowIfRequired(stampWith, this.storageManager.getActiveSession);
|
|
2587
|
+
const userId = params?.userId || session?.userId;
|
|
2588
|
+
if (!userId) {
|
|
2589
|
+
throw new ZeroXKeyError("User ID must be provided to remove passkey", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
2590
|
+
}
|
|
2591
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
2592
|
+
const res = await this.httpClient.deleteAuthenticators({
|
|
2593
|
+
userId,
|
|
2594
|
+
authenticatorIds,
|
|
2595
|
+
...(organizationId && { organizationId }),
|
|
2596
|
+
}, stampWith);
|
|
2597
|
+
if (!res) {
|
|
2598
|
+
throw new ZeroXKeyError("No response found in the remove passkey response", ZeroXKeyErrorCodes.REMOVE_PASSKEY_ERROR);
|
|
2599
|
+
}
|
|
2600
|
+
return res.authenticatorIds;
|
|
2601
|
+
}, {
|
|
2602
|
+
errorMessage: "Failed to remove passkey",
|
|
2603
|
+
errorCode: ZeroXKeyErrorCodes.REMOVE_PASSKEY_ERROR,
|
|
2604
|
+
});
|
|
2605
|
+
};
|
|
2606
|
+
/**
|
|
2607
|
+
* Creates a new wallet for sub-organization.
|
|
2608
|
+
*
|
|
2609
|
+
* - This function creates a new wallet for the current sub-organization.
|
|
2610
|
+
* - If an organizationId is provided, the wallet will be created under that specific sub-organization; otherwise, it uses the current session's organizationId.
|
|
2611
|
+
* - If a list of address formats is provided, accounts will be created in the wallet based on those formats (starting from path index 0).
|
|
2612
|
+
* - If a list of account parameters is provided, those accounts will be created in the wallet.
|
|
2613
|
+
* - If no accounts or address formats are provided, default Ethereum and Solana accounts will be created.
|
|
2614
|
+
* - Optionally allows specifying the mnemonic length for the wallet seed phrase (defaults to 12).
|
|
2615
|
+
* - Optionally allows stamping the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2616
|
+
*
|
|
2617
|
+
* @param params.walletName - name of the wallet to create.
|
|
2618
|
+
* @param params.accounts - array of account parameters or address formats to create in the wallet.
|
|
2619
|
+
* @param params.organizationId - organization ID to create the wallet under a specific sub-organization (defaults to the current session's organizationId).
|
|
2620
|
+
* @param params.mnemonicLength - mnemonic length for the wallet seed phrase (defaults to 12).
|
|
2621
|
+
* @param params.stampWith - parameter to stamp the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2622
|
+
* @returns A promise that resolves to the ID of the newly created wallet.
|
|
2623
|
+
* @throws {ZeroXKeyError} If there is no active session or if there is an error creating the wallet.
|
|
2624
|
+
*/
|
|
2625
|
+
this.createWallet = async (params) => {
|
|
2626
|
+
const { walletName, accounts, organizationId: organizationIdFromParams, mnemonicLength, stampWith = this.config.defaultStamperType, } = params;
|
|
2627
|
+
const session = await getActiveSessionOrThrowIfRequired(stampWith, this.storageManager.getActiveSession);
|
|
2628
|
+
const organizationId = organizationIdFromParams || session?.organizationId;
|
|
2629
|
+
if (!organizationId) {
|
|
2630
|
+
throw new ZeroXKeyError("Organization ID must be provided to create wallet", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
2631
|
+
}
|
|
2632
|
+
let walletAccounts = [];
|
|
2633
|
+
if (accounts && !isWalletAccountArray(accounts)) {
|
|
2634
|
+
walletAccounts = generateWalletAccountsFromAddressFormat({
|
|
2635
|
+
addresses: accounts,
|
|
2636
|
+
});
|
|
2637
|
+
}
|
|
2638
|
+
else {
|
|
2639
|
+
walletAccounts = accounts || [
|
|
2640
|
+
...DEFAULT_ETHEREUM_ACCOUNTS,
|
|
2641
|
+
...DEFAULT_SOLANA_ACCOUNTS,
|
|
2642
|
+
];
|
|
2643
|
+
}
|
|
2644
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
2645
|
+
const res = await this.httpClient.createWallet({
|
|
2646
|
+
organizationId: organizationId,
|
|
2647
|
+
walletName,
|
|
2648
|
+
accounts: walletAccounts,
|
|
2649
|
+
mnemonicLength: mnemonicLength || 12,
|
|
2650
|
+
}, stampWith);
|
|
2651
|
+
if (!res || !res.walletId) {
|
|
2652
|
+
throw new ZeroXKeyError("No wallet found in the create wallet response", ZeroXKeyErrorCodes.BAD_RESPONSE);
|
|
2653
|
+
}
|
|
2654
|
+
return res.walletId;
|
|
2655
|
+
}, {
|
|
2656
|
+
errorMessage: "Failed to create wallet",
|
|
2657
|
+
errorCode: ZeroXKeyErrorCodes.CREATE_WALLET_ERROR,
|
|
2658
|
+
});
|
|
2659
|
+
};
|
|
2660
|
+
/**
|
|
2661
|
+
* Creates new accounts in the specified wallet.
|
|
2662
|
+
*
|
|
2663
|
+
* - This function creates new wallet accounts based on the provided account parameters or address formats.
|
|
2664
|
+
* - If a walletId is provided, it creates the accounts in that specific wallet; otherwise, it uses the current session's wallet.
|
|
2665
|
+
* - If a list of address formats is provided, it will create accounts in the wallet based on those formats, automatically determining the next available path indexes to avoid duplicates with existing accounts.
|
|
2666
|
+
* - If account parameters are provided, they are used directly for account creation.
|
|
2667
|
+
* - Automatically queries existing wallet accounts to prevent duplicate account creation for the same address format and path.
|
|
2668
|
+
* - Supports stamping the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2669
|
+
*
|
|
2670
|
+
* @param params.accounts - An array of account parameters or address formats to create in the wallet.
|
|
2671
|
+
* @param params.walletId - ID of the wallet to create accounts in.
|
|
2672
|
+
* @param params.organizationId - organization ID to create the accounts under a specific organization (walletId must be associated with the sub-organization).
|
|
2673
|
+
* @param params.stampWith - parameter to stamp the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2674
|
+
* @returns A promise that resolves to an array of addresses for the newly created accounts.
|
|
2675
|
+
* @throws {ZeroXKeyError} If there is no active session, if the wallet does not exist, or if there is an error creating the wallet accounts.
|
|
2676
|
+
*/
|
|
2677
|
+
this.createWalletAccounts = async (params) => {
|
|
2678
|
+
const { accounts, walletId, organizationId: organizationIdFromParams, stampWith = this.config.defaultStamperType, } = params;
|
|
2679
|
+
const session = await getActiveSessionOrThrowIfRequired(stampWith, this.storageManager.getActiveSession);
|
|
2680
|
+
const organizationId = organizationIdFromParams || session?.organizationId;
|
|
2681
|
+
if (!organizationId) {
|
|
2682
|
+
throw new ZeroXKeyError("Organization ID must be provided to create wallet accounts", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
2683
|
+
}
|
|
2684
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
2685
|
+
let walletAccounts = [];
|
|
2686
|
+
if (accounts && !isWalletAccountArray(accounts)) {
|
|
2687
|
+
// Query existing wallet accounts to avoid duplicates
|
|
2688
|
+
const existingWalletAccounts = await this.httpClient.getWalletAccounts({
|
|
2689
|
+
walletId,
|
|
2690
|
+
organizationId: organizationId,
|
|
2691
|
+
paginationOptions: { limit: "100" },
|
|
2692
|
+
}, stampWith);
|
|
2693
|
+
walletAccounts = generateWalletAccountsFromAddressFormat({
|
|
2694
|
+
addresses: accounts,
|
|
2695
|
+
existingWalletAccounts: existingWalletAccounts.accounts || [],
|
|
2696
|
+
});
|
|
2697
|
+
}
|
|
2698
|
+
else {
|
|
2699
|
+
walletAccounts = accounts;
|
|
2700
|
+
}
|
|
2701
|
+
const res = await this.httpClient.createWalletAccounts({
|
|
2702
|
+
organizationId: organizationId,
|
|
2703
|
+
walletId,
|
|
2704
|
+
accounts: walletAccounts,
|
|
2705
|
+
}, stampWith);
|
|
2706
|
+
if (!res || !res.addresses) {
|
|
2707
|
+
throw new ZeroXKeyError("No account found in the create wallet account response", ZeroXKeyErrorCodes.BAD_RESPONSE);
|
|
2708
|
+
}
|
|
2709
|
+
return res.addresses;
|
|
2710
|
+
}, {
|
|
2711
|
+
errorMessage: "Failed to create wallet account",
|
|
2712
|
+
errorCode: ZeroXKeyErrorCodes.CREATE_WALLET_ACCOUNT_ERROR,
|
|
2713
|
+
});
|
|
2714
|
+
};
|
|
2715
|
+
/**
|
|
2716
|
+
* Exports a wallet as an encrypted bundle.
|
|
2717
|
+
*
|
|
2718
|
+
* - This function exports the specified wallet and its accounts as an encrypted bundle, suitable for backup or transfer.
|
|
2719
|
+
* - The exported bundle contains the wallet's seed phrase, encrypted to the provided target public key.
|
|
2720
|
+
* - If a targetPublicKey is provided, the bundle will be encrypted to that public key; otherwise, an error will be thrown.
|
|
2721
|
+
* - If an organizationId is provided, the wallet will be exported under that sub-organization; otherwise, the current session's organizationId is used.
|
|
2722
|
+
* - Optionally allows stamping the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2723
|
+
* - The exported bundle can later be imported using the `importWallet` method.
|
|
2724
|
+
*
|
|
2725
|
+
* @param params.walletId - ID of the wallet to export.
|
|
2726
|
+
* @param params.targetPublicKey - public key to encrypt the bundle to (required).
|
|
2727
|
+
* @param params.organizationId - organization ID to export the wallet under a specific sub-organization (walletId must be associated with the sub-organization).
|
|
2728
|
+
* @param params.stampWith - parameter to stamp the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2729
|
+
* @returns A promise that resolves to an `ExportBundle` object containing the encrypted wallet seed phrase and metadata.
|
|
2730
|
+
* @throws {ZeroXKeyError} If there is no active session, if the targetPublicKey is missing, or if there is an error exporting the wallet.
|
|
2731
|
+
*/
|
|
2732
|
+
this.exportWallet = async (params) => {
|
|
2733
|
+
const { walletId, targetPublicKey, stampWith = this.config.defaultStamperType, organizationId: organizationIdFromParams, } = params;
|
|
2734
|
+
const session = await getActiveSessionOrThrowIfRequired(stampWith, this.storageManager.getActiveSession);
|
|
2735
|
+
const organizationId = organizationIdFromParams || session?.organizationId;
|
|
2736
|
+
if (!organizationId) {
|
|
2737
|
+
throw new ZeroXKeyError("Organization ID must be provided to export wallet", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
2738
|
+
}
|
|
2739
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
2740
|
+
const res = await this.httpClient.exportWallet({
|
|
2741
|
+
walletId,
|
|
2742
|
+
targetPublicKey,
|
|
2743
|
+
organizationId: organizationId,
|
|
2744
|
+
}, stampWith);
|
|
2745
|
+
if (!res.exportBundle) {
|
|
2746
|
+
throw new ZeroXKeyError("No export bundle found in the response", ZeroXKeyErrorCodes.BAD_RESPONSE);
|
|
2747
|
+
}
|
|
2748
|
+
return res.exportBundle;
|
|
2749
|
+
}, {
|
|
2750
|
+
errorMessage: "Failed to export wallet",
|
|
2751
|
+
errorCode: ZeroXKeyErrorCodes.EXPORT_WALLET_ERROR,
|
|
2752
|
+
});
|
|
2753
|
+
};
|
|
2754
|
+
/**
|
|
2755
|
+
* Exports a private key as an encrypted bundle.
|
|
2756
|
+
*
|
|
2757
|
+
* - This function exports the specified private key as an encrypted bundle, suitable for backup or transfer.
|
|
2758
|
+
* - The exported bundle contains the private key's key material, encrypted to the provided target public key.
|
|
2759
|
+
* - If a targetPublicKey is provided, the bundle will be encrypted to that public key; otherwise, an error will be thrown.
|
|
2760
|
+
* - If an organizationId is provided, the private key will be exported under that sub-organization; otherwise, the current session's organizationId is used.
|
|
2761
|
+
* - Optionally allows stamping the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2762
|
+
*
|
|
2763
|
+
* @param params.privateKeyId - ID of the private key to export.
|
|
2764
|
+
* @param params.targetPublicKey - public key to encrypt the bundle to (required).
|
|
2765
|
+
* @param params.organizationId - organization ID to export the private key under a specific sub
|
|
2766
|
+
* @param params.stampWith - parameter to stamp the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2767
|
+
* @returns A promise that resolves to an `ExportBundle` object containing the encrypted private key and metadata.
|
|
2768
|
+
* @throws {ZeroXKeyError} If there is no active session, if the targetPublicKey is missing, or if there is an error exporting the private key.
|
|
2769
|
+
*/
|
|
2770
|
+
this.exportPrivateKey = async (params) => {
|
|
2771
|
+
const { privateKeyId, targetPublicKey, stampWith = this.config.defaultStamperType, organizationId: organizationIdFromParams, } = params;
|
|
2772
|
+
const session = await getActiveSessionOrThrowIfRequired(stampWith, this.storageManager.getActiveSession);
|
|
2773
|
+
const organizationId = organizationIdFromParams || session?.organizationId;
|
|
2774
|
+
if (!organizationId) {
|
|
2775
|
+
throw new ZeroXKeyError("Organization ID must be provided to export private key", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
2776
|
+
}
|
|
2777
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
2778
|
+
const res = await this.httpClient.exportPrivateKey({
|
|
2779
|
+
privateKeyId,
|
|
2780
|
+
targetPublicKey,
|
|
2781
|
+
organizationId: organizationId,
|
|
2782
|
+
}, stampWith);
|
|
2783
|
+
if (!res.exportBundle) {
|
|
2784
|
+
throw new ZeroXKeyError("No export bundle found in the response", ZeroXKeyErrorCodes.BAD_RESPONSE);
|
|
2785
|
+
}
|
|
2786
|
+
return res.exportBundle;
|
|
2787
|
+
}, {
|
|
2788
|
+
errorMessage: "Failed to export private key",
|
|
2789
|
+
errorCode: ZeroXKeyErrorCodes.EXPORT_PRIVATE_KEY_ERROR,
|
|
2790
|
+
});
|
|
2791
|
+
};
|
|
2792
|
+
/**
|
|
2793
|
+
* Exports a wallet account as an encrypted bundle.
|
|
2794
|
+
*
|
|
2795
|
+
* - This function exports the specified wallet account as an encrypted bundle, suitable for backup or transfer.
|
|
2796
|
+
* - The exported bundle contains the wallet account's key material, encrypted to the provided target public key.
|
|
2797
|
+
* - If a targetPublicKey is provided, the bundle will be encrypted to that public key; otherwise, an error will be thrown.
|
|
2798
|
+
* - If an organizationId is provided, the wallet account will be exported under that sub-organization; otherwise, the current session's organizationId is used.
|
|
2799
|
+
* - Optionally allows stamping the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2800
|
+
*
|
|
2801
|
+
* @param params.address - address of the wallet account to export.
|
|
2802
|
+
* @param params.targetPublicKey - public key to encrypt the bundle to.
|
|
2803
|
+
* @param params.organizationId - organization ID to export the wallet account under a specific sub-organization.
|
|
2804
|
+
* @param params.stampWith - parameter to stamp the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2805
|
+
* @returns A promise that resolves to an `ExportBundle` object containing the encrypted wallet account and metadata.
|
|
2806
|
+
* @throws {ZeroXKeyError} If there is no active session, if the targetPublicKey is missing, or if there is an error exporting the wallet account.
|
|
2807
|
+
*
|
|
2808
|
+
*/
|
|
2809
|
+
this.exportWalletAccount = async (params) => {
|
|
2810
|
+
const { address, targetPublicKey, stampWith = this.config.defaultStamperType, organizationId: organizationIdFromParams, } = params;
|
|
2811
|
+
const session = await getActiveSessionOrThrowIfRequired(stampWith, this.storageManager.getActiveSession);
|
|
2812
|
+
const organizationId = organizationIdFromParams || session?.organizationId;
|
|
2813
|
+
if (!organizationId) {
|
|
2814
|
+
throw new ZeroXKeyError("Organization ID must be provided to export wallet account", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
2815
|
+
}
|
|
2816
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
2817
|
+
const res = await this.httpClient.exportWalletAccount({
|
|
2818
|
+
address,
|
|
2819
|
+
targetPublicKey,
|
|
2820
|
+
organizationId: organizationId,
|
|
2821
|
+
}, stampWith);
|
|
2822
|
+
if (!res.exportBundle) {
|
|
2823
|
+
throw new ZeroXKeyError("No export bundle found in the response", ZeroXKeyErrorCodes.BAD_RESPONSE);
|
|
2824
|
+
}
|
|
2825
|
+
return res.exportBundle;
|
|
2826
|
+
}, {
|
|
2827
|
+
errorMessage: "Failed to export wallet account",
|
|
2828
|
+
errorCode: ZeroXKeyErrorCodes.EXPORT_WALLET_ACCOUNT_ERROR,
|
|
2829
|
+
});
|
|
2830
|
+
};
|
|
2831
|
+
/**
|
|
2832
|
+
* Imports a wallet from an encrypted bundle.
|
|
2833
|
+
*
|
|
2834
|
+
* - This function imports a wallet using the provided encrypted bundle and creates accounts based on the provided parameters.
|
|
2835
|
+
* - If a userId is provided, the wallet will be imported for that specific user; otherwise, it uses the current session's userId.
|
|
2836
|
+
* - If an accounts array is provided, those accounts will be created in the imported wallet; otherwise, default Ethereum and Solana accounts will be created.
|
|
2837
|
+
* - The encrypted bundle MUST be encrypted to
|
|
2838
|
+
* - Automatically ensures an active session exists before making the request.
|
|
2839
|
+
* - Optionally allows stamping the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2840
|
+
*
|
|
2841
|
+
* @param params.encryptedBundle - encrypted bundle containing the wallet seed phrase and metadata.
|
|
2842
|
+
* @param params.walletName - name of the wallet to create upon import.
|
|
2843
|
+
* @param params.accounts - array of account parameters to create in the imported wallet (defaults to standard Ethereum and Solana accounts).
|
|
2844
|
+
* @param params.organizationId - organization ID to import the wallet under a specific sub-organization (wallet will be associated with the sub-organization).
|
|
2845
|
+
* @param params.userId - user ID to import the wallet for a specific user (defaults to the current session's userId).
|
|
2846
|
+
* @param params.stampWith - parameter to stamp the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2847
|
+
* @returns A promise that resolves to the ID of the imported wallet.
|
|
2848
|
+
* @throws {ZeroXKeyError} If there is no active session, if the encrypted bundle is invalid, or if there is an error importing the wallet.
|
|
2849
|
+
*/
|
|
2850
|
+
this.importWallet = async (params) => {
|
|
2851
|
+
const { encryptedBundle, accounts, walletName, organizationId: organizationIdFromParams, userId: userIdFromParams, stampWith = this.config.defaultStamperType, } = params;
|
|
2852
|
+
const session = await getActiveSessionOrThrowIfRequired(stampWith, this.storageManager.getActiveSession);
|
|
2853
|
+
const organizationId = organizationIdFromParams || session?.organizationId;
|
|
2854
|
+
if (!organizationId) {
|
|
2855
|
+
throw new ZeroXKeyError("Organization ID must be provided to import wallet", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
2856
|
+
}
|
|
2857
|
+
const userId = userIdFromParams || session?.userId;
|
|
2858
|
+
if (!userId) {
|
|
2859
|
+
throw new ZeroXKeyError("User ID must be provided to import wallet", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
2860
|
+
}
|
|
2861
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
2862
|
+
const res = await this.httpClient.importWallet({
|
|
2863
|
+
organizationId: organizationId,
|
|
2864
|
+
userId: userId,
|
|
2865
|
+
encryptedBundle,
|
|
2866
|
+
walletName,
|
|
2867
|
+
accounts: accounts || [
|
|
2868
|
+
...DEFAULT_ETHEREUM_ACCOUNTS,
|
|
2869
|
+
...DEFAULT_SOLANA_ACCOUNTS,
|
|
2870
|
+
],
|
|
2871
|
+
}, stampWith);
|
|
2872
|
+
if (!res || !res.walletId) {
|
|
2873
|
+
throw new ZeroXKeyError("No wallet ID found in the import response", ZeroXKeyErrorCodes.BAD_RESPONSE);
|
|
2874
|
+
}
|
|
2875
|
+
return res.walletId;
|
|
2876
|
+
}, {
|
|
2877
|
+
errorMessage: "Failed to import wallet",
|
|
2878
|
+
errorCode: ZeroXKeyErrorCodes.IMPORT_WALLET_ERROR,
|
|
2879
|
+
customErrorsByMessages: {
|
|
2880
|
+
"invalid mnemonic": {
|
|
2881
|
+
message: "Invalid mnemonic input",
|
|
2882
|
+
code: ZeroXKeyErrorCodes.BAD_REQUEST,
|
|
2883
|
+
},
|
|
2884
|
+
},
|
|
2885
|
+
});
|
|
2886
|
+
};
|
|
2887
|
+
/**
|
|
2888
|
+
* Imports a private key from an encrypted bundle.
|
|
2889
|
+
*
|
|
2890
|
+
* - This function imports a private key using the provided encrypted bundle.
|
|
2891
|
+
* - If a userId is provided, the private key will be imported for that specific user; otherwise, it uses the current session's userId.
|
|
2892
|
+
* - Requires address formats to
|
|
2893
|
+
* - Automatically infers the cryptographic curve used to generate the private key based on the address format (can be optionally overriden if needed).
|
|
2894
|
+
* - The encrypted bundle MUST be encrypted to ensure security.
|
|
2895
|
+
* - Automatically ensures an active session exists before making the request.
|
|
2896
|
+
* - Optionally allows stamping the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2897
|
+
*
|
|
2898
|
+
* @param params.encryptedBundle - encrypted bundle containing the private key key material and metadata.
|
|
2899
|
+
* @param params.privateKeyName - name of the private key to create upon import.
|
|
2900
|
+
* @param params.curve - the cryptographic curve used to generate a given private key
|
|
2901
|
+
* @param params.addressFormat - address format of the private key to import.
|
|
2902
|
+
* @param params.organizationId - organization ID to import the private key under a specific sub-organization (private key will be associated with the sub-organization).
|
|
2903
|
+
* @param params.userId - user ID to import the wallet for a specific user (defaults to the current session's userId).
|
|
2904
|
+
* @param params.stampWith - parameter to stamp the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2905
|
+
* @returns A promise that resolves to the ID of the imported wallet.
|
|
2906
|
+
* @throws {ZeroXKeyError} If there is no active session, if the encrypted bundle is invalid, or if there is an error importing the wallet.
|
|
2907
|
+
*/
|
|
2908
|
+
this.importPrivateKey = async (params) => {
|
|
2909
|
+
const { encryptedBundle, privateKeyName, addressFormats, curve, organizationId: organizationIdFromParams, userId: userIdFromParams, stampWith = this.config.defaultStamperType, } = params;
|
|
2910
|
+
const session = await getActiveSessionOrThrowIfRequired(stampWith, this.storageManager.getActiveSession);
|
|
2911
|
+
const organizationId = organizationIdFromParams || session?.organizationId;
|
|
2912
|
+
if (!organizationId) {
|
|
2913
|
+
throw new ZeroXKeyError("Organization ID must be provided to import private key", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
2914
|
+
}
|
|
2915
|
+
const userId = userIdFromParams || session?.userId;
|
|
2916
|
+
if (!userId) {
|
|
2917
|
+
throw new ZeroXKeyError("User ID must be provided to import private key", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
2918
|
+
}
|
|
2919
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
2920
|
+
const res = await this.httpClient.importPrivateKey({
|
|
2921
|
+
organizationId,
|
|
2922
|
+
userId,
|
|
2923
|
+
encryptedBundle,
|
|
2924
|
+
privateKeyName,
|
|
2925
|
+
curve,
|
|
2926
|
+
addressFormats,
|
|
2927
|
+
}, stampWith);
|
|
2928
|
+
if (!res || !res.privateKeyId) {
|
|
2929
|
+
throw new ZeroXKeyError("No wallet ID found in the import response", ZeroXKeyErrorCodes.BAD_RESPONSE);
|
|
2930
|
+
}
|
|
2931
|
+
return res.privateKeyId;
|
|
2932
|
+
}, {
|
|
2933
|
+
errorMessage: "Failed to import wallet",
|
|
2934
|
+
errorCode: ZeroXKeyErrorCodes.IMPORT_WALLET_ERROR,
|
|
2935
|
+
customErrorsByMessages: {
|
|
2936
|
+
"invalid mnemonic": {
|
|
2937
|
+
message: "Invalid mnemonic input",
|
|
2938
|
+
code: ZeroXKeyErrorCodes.BAD_REQUEST,
|
|
2939
|
+
},
|
|
2940
|
+
},
|
|
2941
|
+
});
|
|
2942
|
+
};
|
|
2943
|
+
/**
|
|
2944
|
+
* Deletes the current sub-organization (sub-org) for the active session.
|
|
2945
|
+
*
|
|
2946
|
+
* - This function deletes the sub-organization associated with the current active session.
|
|
2947
|
+
* - By default, the deletion will fail if any wallets associated with the sub-organization have not been exported.
|
|
2948
|
+
* - If `deleteWithoutExport` is set to true, the sub-organization will be deleted even if its wallets have not been exported (potentially resulting in loss of access to those wallets).
|
|
2949
|
+
* - Requires an active session; otherwise, an error is thrown.
|
|
2950
|
+
* - Optionally allows stamping the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
2951
|
+
*
|
|
2952
|
+
* @param params.deleteWithoutExport - flag to delete the sub-organization without requiring all wallets to be exported first (defaults to false).
|
|
2953
|
+
* @param params.organizationId - organization ID to delete a specific sub-organization (defaults to the current session's organizationId).
|
|
2954
|
+
* @param params.stampWith - parameter to stamp the request with a specific stamper.
|
|
2955
|
+
* @returns A promise that resolves to a `TDeleteSubOrganizationResponse` object containing the result of the deletion.
|
|
2956
|
+
* @throws {ZeroXKeyError} If there is no active session or if there is an error deleting the sub-organization.
|
|
2957
|
+
*/
|
|
2958
|
+
this.deleteSubOrganization = async (params) => {
|
|
2959
|
+
const { deleteWithoutExport = false, organizationId: organizationIdFromParams, stampWith = this.config.defaultStamperType, } = params || {};
|
|
2960
|
+
const session = await getActiveSessionOrThrowIfRequired(stampWith, this.storageManager.getActiveSession);
|
|
2961
|
+
const organizationId = organizationIdFromParams || session?.organizationId;
|
|
2962
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
2963
|
+
return await this.httpClient.deleteSubOrganization({ deleteWithoutExport, ...(organizationId && { organizationId }) }, stampWith);
|
|
2964
|
+
}, {
|
|
2965
|
+
errorMessage: "Failed to delete sub-organization",
|
|
2966
|
+
errorCode: ZeroXKeyErrorCodes.DELETE_SUB_ORGANIZATION_ERROR,
|
|
2967
|
+
});
|
|
2968
|
+
};
|
|
2969
|
+
/**
|
|
2970
|
+
* Stores a session token and updates the session associated with the specified session key, or by default the active session.
|
|
2971
|
+
*
|
|
2972
|
+
* - This function parses and stores a signed JWT session token in local storage, associating it with the given session key.
|
|
2973
|
+
* - If a sessionKey is provided, the session will be stored under that key; otherwise, it will use the default session key.
|
|
2974
|
+
* - If a session already exists for the session key, its associated key pair will be deleted before storing the new session.
|
|
2975
|
+
* - After storing the session, any unused key pairs are automatically cleared from storage.
|
|
2976
|
+
* - Ensures that session management is consistent and prevents orphaned key pairs.
|
|
2977
|
+
*
|
|
2978
|
+
* @param params.sessionToken - JWT session token to store.
|
|
2979
|
+
* @param params.sessionKey - session key to store the session under (defaults to the default session key).
|
|
2980
|
+
* @returns A promise that resolves when the session is successfully stored.
|
|
2981
|
+
* @throws {ZeroXKeyError} If there is an error storing the session or cleaning up key pairs.
|
|
2982
|
+
*/
|
|
2983
|
+
this.storeSession = async (params) => {
|
|
2984
|
+
const { sessionToken, sessionKey = SessionKey.DefaultSessionkey } = params;
|
|
2985
|
+
if (!sessionToken)
|
|
2986
|
+
return;
|
|
2987
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
2988
|
+
await this.storageManager.storeSession(sessionToken, sessionKey);
|
|
2989
|
+
}, {
|
|
2990
|
+
errorMessage: "Failed to store session",
|
|
2991
|
+
errorCode: ZeroXKeyErrorCodes.STORE_SESSION_ERROR,
|
|
2992
|
+
}, {
|
|
2993
|
+
finallyFn: async () => await this.clearUnusedKeyPairs(),
|
|
2994
|
+
});
|
|
2995
|
+
};
|
|
2996
|
+
/**
|
|
2997
|
+
* Clears the session associated with the specified session key, or the active session by default.
|
|
2998
|
+
*
|
|
2999
|
+
* - This function deletes the session and its associated key pair from storage.
|
|
3000
|
+
* - If a sessionKey is provided, it will clear the session under that key; otherwise, it will clear the default (active) session.
|
|
3001
|
+
* - Removes the session data from local storage and deletes the corresponding API key pair from the key store.
|
|
3002
|
+
* - Throws an error if the session does not exist or if there is an error during the clearing process.
|
|
3003
|
+
*
|
|
3004
|
+
* @param params.sessionKey - session key to clear the session under (defaults to the default session key).
|
|
3005
|
+
* @returns A promise that resolves when the session is successfully cleared.
|
|
3006
|
+
* @throws {ZeroXKeyError} If the session does not exist or if there is an error clearing the session.
|
|
3007
|
+
*/
|
|
3008
|
+
this.clearSession = async (params) => {
|
|
3009
|
+
const { sessionKey = SessionKey.DefaultSessionkey } = params || {};
|
|
3010
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
3011
|
+
const session = await this.storageManager.getSession(sessionKey);
|
|
3012
|
+
if (session) {
|
|
3013
|
+
await Promise.all([
|
|
3014
|
+
this.apiKeyStamper?.deleteKeyPair(session.publicKey),
|
|
3015
|
+
this.storageManager.clearSession(sessionKey),
|
|
3016
|
+
]);
|
|
3017
|
+
}
|
|
3018
|
+
else {
|
|
3019
|
+
throw new ZeroXKeyError(`No session found with key: ${sessionKey}`, ZeroXKeyErrorCodes.NOT_FOUND);
|
|
3020
|
+
}
|
|
3021
|
+
}, {
|
|
3022
|
+
errorMessage: "Failed to delete session",
|
|
3023
|
+
errorCode: ZeroXKeyErrorCodes.CLEAR_SESSION_ERROR,
|
|
3024
|
+
});
|
|
3025
|
+
};
|
|
3026
|
+
/**
|
|
3027
|
+
* Clears all sessions and resets the active session state.
|
|
3028
|
+
*
|
|
3029
|
+
* - This function removes all session data from the client and persistent storage, including all associated key pairs.
|
|
3030
|
+
* - Iterates through all stored session keys, clearing each session and deleting its corresponding API key pair.
|
|
3031
|
+
* - After clearing, there will be no active session, and all session-related data will be removed from local storage.
|
|
3032
|
+
* - Throws an error if no sessions exist or if there is an error during the clearing process.
|
|
3033
|
+
*
|
|
3034
|
+
* @returns A promise that resolves when all sessions are successfully cleared.
|
|
3035
|
+
* @throws {ZeroXKeyError} If no sessions exist or if there is an error clearing all sessions.
|
|
3036
|
+
*/
|
|
3037
|
+
this.clearAllSessions = async () => {
|
|
3038
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
3039
|
+
const sessionKeys = await this.storageManager.listSessionKeys();
|
|
3040
|
+
if (sessionKeys.length === 0)
|
|
3041
|
+
return;
|
|
3042
|
+
for (const sessionKey of sessionKeys) {
|
|
3043
|
+
this.clearSession({ sessionKey });
|
|
3044
|
+
}
|
|
3045
|
+
}, {
|
|
3046
|
+
errorMessage: "Failed to clear all sessions",
|
|
3047
|
+
errorCode: ZeroXKeyErrorCodes.CLEAR_ALL_SESSIONS_ERROR,
|
|
3048
|
+
});
|
|
3049
|
+
};
|
|
3050
|
+
/**
|
|
3051
|
+
* Refreshes the session associated with the specified session key, or the active session by default.
|
|
3052
|
+
*
|
|
3053
|
+
* - This function refreshes the session and updates the session token and key pair associated with the given session key.
|
|
3054
|
+
* - If a sessionKey is provided, it will refresh the session under that key; otherwise, it will use the current active session key.
|
|
3055
|
+
* - Optionally allows specifying a new expiration time for the session, a custom public key, and whether to invalidate the existing session after refreshing.
|
|
3056
|
+
* - Makes a request to the ZeroXKey API to stamp a new login and stores the refreshed session token.
|
|
3057
|
+
* - Automatically manages key pair cleanup and session storage to ensure consistency.
|
|
3058
|
+
* - Optionally allows stamping the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
3059
|
+
*
|
|
3060
|
+
* @param params.sessionKey - session key to refresh the session under (defaults to the active session key).
|
|
3061
|
+
* @param params.expirationSeconds - expiration time in seconds for the refreshed session (defaults to the configured default).
|
|
3062
|
+
* @param params.publicKey - public key to use for the refreshed session (if not provided, a new key pair will be generated).
|
|
3063
|
+
* @param params.invalidateExisitng - flag to invalidate the existing session before refreshing (defaults to false).
|
|
3064
|
+
* @param params.stampWith - parameter to stamp the request with a specific stamper.
|
|
3065
|
+
* @returns A promise that resolves to a `TStampLoginResponse` object containing the refreshed session details.
|
|
3066
|
+
* @throws {ZeroXKeyError} If the session key does not exist, if there is no active session, or if there is an error refreshing the session.
|
|
3067
|
+
*/
|
|
3068
|
+
this.refreshSession = async (params) => {
|
|
3069
|
+
const { sessionKey = await this.storageManager.getActiveSessionKey(), expirationSeconds = DEFAULT_SESSION_EXPIRATION_IN_SECONDS, publicKey, stampWith = this.config.defaultStamperType, invalidateExisitng = false, } = params || {};
|
|
3070
|
+
if (!sessionKey) {
|
|
3071
|
+
throw new ZeroXKeyError("No session key provided or active session to refresh session", ZeroXKeyErrorCodes.NO_SESSION_FOUND);
|
|
3072
|
+
}
|
|
3073
|
+
const session = await this.getSession({
|
|
3074
|
+
sessionKey: sessionKey,
|
|
3075
|
+
});
|
|
3076
|
+
if (!session) {
|
|
3077
|
+
throw new ZeroXKeyError(`No active session found: ${sessionKey}`, ZeroXKeyErrorCodes.NO_SESSION_FOUND);
|
|
3078
|
+
}
|
|
3079
|
+
if (!this.httpClient) {
|
|
3080
|
+
throw new ZeroXKeyError("HTTP client is not initialized. Please initialize the client before refreshing the session.", ZeroXKeyErrorCodes.CLIENT_NOT_INITIALIZED);
|
|
3081
|
+
}
|
|
3082
|
+
let keyPair;
|
|
3083
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
3084
|
+
keyPair = publicKey ?? (await this.apiKeyStamper?.createKeyPair());
|
|
3085
|
+
if (!keyPair) {
|
|
3086
|
+
throw new ZeroXKeyError("Failed to create new key pair.", ZeroXKeyErrorCodes.INTERNAL_ERROR);
|
|
3087
|
+
}
|
|
3088
|
+
const res = await this.httpClient.stampLogin({
|
|
3089
|
+
publicKey: keyPair,
|
|
3090
|
+
expirationSeconds,
|
|
3091
|
+
invalidateExisting: invalidateExisitng,
|
|
3092
|
+
}, stampWith);
|
|
3093
|
+
if (!res || !res.session) {
|
|
3094
|
+
throw new ZeroXKeyError("No session found in the refresh response", ZeroXKeyErrorCodes.BAD_RESPONSE);
|
|
3095
|
+
}
|
|
3096
|
+
await this.storeSession({
|
|
3097
|
+
sessionToken: res.session,
|
|
3098
|
+
...(sessionKey && { sessionKey }),
|
|
3099
|
+
});
|
|
3100
|
+
return res;
|
|
3101
|
+
}, {
|
|
3102
|
+
errorMessage: "Failed to refresh session",
|
|
3103
|
+
errorCode: ZeroXKeyErrorCodes.REFRESH_SESSION_ERROR,
|
|
3104
|
+
});
|
|
3105
|
+
};
|
|
3106
|
+
/**
|
|
3107
|
+
* Retrieves the session associated with the specified session key, or the active session by default.
|
|
3108
|
+
*
|
|
3109
|
+
* - This function retrieves the session object from storage, using the provided session key or, if not specified, the current active session key.
|
|
3110
|
+
* - If no session key is provided and there is no active session, it returns undefined.
|
|
3111
|
+
* - Returns the session details, including public key, organization ID, user ID, and expiration.
|
|
3112
|
+
*
|
|
3113
|
+
* @param params.sessionKey - session key to retrieve a specific session (defaults to the current active session key).
|
|
3114
|
+
* @returns A promise that resolves to a `Session` object containing the session details, or undefined if not found.
|
|
3115
|
+
* @throws {ZeroXKeyError} If there is an error retrieving the session from storage.
|
|
3116
|
+
*/
|
|
3117
|
+
this.getSession = async (params) => {
|
|
3118
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
3119
|
+
const { sessionKey = await this.storageManager.getActiveSessionKey() } = params || {};
|
|
3120
|
+
return this.storageManager.getSession(sessionKey);
|
|
3121
|
+
}, {
|
|
3122
|
+
errorMessage: "Failed to get session with key " + params?.sessionKey,
|
|
3123
|
+
errorCode: ZeroXKeyErrorCodes.GET_SESSION_ERROR,
|
|
3124
|
+
});
|
|
3125
|
+
};
|
|
3126
|
+
/**
|
|
3127
|
+
* Retrieves all sessions stored in persistent storage.
|
|
3128
|
+
*
|
|
3129
|
+
* - This function fetches all session objects currently stored by the client, including those that are not active.
|
|
3130
|
+
* - Returns a record mapping each session key to its corresponding `Session` object.
|
|
3131
|
+
* - Useful for session management, auditing, or displaying all available sessions to the user.
|
|
3132
|
+
* - Automatically skips any session keys that do not have a valid session object.
|
|
3133
|
+
*
|
|
3134
|
+
* @returns A promise that resolves to a record of session keys and their corresponding `Session` objects, or `undefined` if no sessions exist.
|
|
3135
|
+
* @throws {ZeroXKeyError} If there is an error retrieving sessions from storage.
|
|
3136
|
+
*/
|
|
3137
|
+
this.getAllSessions = async () => {
|
|
3138
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
3139
|
+
const sessionKeys = await this.storageManager.listSessionKeys();
|
|
3140
|
+
if (!sessionKeys || sessionKeys.length === 0) {
|
|
3141
|
+
return undefined;
|
|
3142
|
+
}
|
|
3143
|
+
const sessions = {};
|
|
3144
|
+
for (const sessionKey of sessionKeys) {
|
|
3145
|
+
const session = await this.storageManager.getSession(sessionKey);
|
|
3146
|
+
if (session) {
|
|
3147
|
+
sessions[sessionKey] = session;
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
return sessions || undefined;
|
|
3151
|
+
}, {
|
|
3152
|
+
errorMessage: "Failed to get all sessions",
|
|
3153
|
+
errorCode: ZeroXKeyErrorCodes.GET_ALL_SESSIONS_ERROR,
|
|
3154
|
+
});
|
|
3155
|
+
};
|
|
3156
|
+
/**
|
|
3157
|
+
* Sets the active session to the specified session key.
|
|
3158
|
+
*
|
|
3159
|
+
* - This function updates the `activeSessionKey` in persistent storage to the specified session key.
|
|
3160
|
+
* - Ensures that subsequent operations use the session associated with this key as the active session.
|
|
3161
|
+
* - Does not validate whether the session key exists or is valid; it simply updates the pointer.
|
|
3162
|
+
* - Useful for switching between multiple stored sessions or restoring a previous session context.
|
|
3163
|
+
*
|
|
3164
|
+
* @param params.sessionKey - session key to set as the active session.
|
|
3165
|
+
* @returns A promise that resolves when the active session key is successfully set.
|
|
3166
|
+
* @throws {ZeroXKeyError} If the client is not initialized or if there is an error setting the active session key.
|
|
3167
|
+
*/
|
|
3168
|
+
this.setActiveSession = async (params) => {
|
|
3169
|
+
const { sessionKey } = params;
|
|
3170
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
3171
|
+
await this.storageManager.setActiveSessionKey(sessionKey);
|
|
3172
|
+
}, {
|
|
3173
|
+
errorMessage: "Failed to set active session",
|
|
3174
|
+
errorCode: ZeroXKeyErrorCodes.SET_ACTIVE_SESSION_ERROR,
|
|
3175
|
+
});
|
|
3176
|
+
};
|
|
3177
|
+
/**
|
|
3178
|
+
* Retrieves the active session key currently set in persistent storage.
|
|
3179
|
+
*
|
|
3180
|
+
* - This function fetches the session key that is currently marked as active in the client's persistent storage.
|
|
3181
|
+
* - The active session key determines which session is used for all session-dependent operations.
|
|
3182
|
+
* - If no active session key is set, returns `undefined`.
|
|
3183
|
+
* - Useful for determining which session is currently in use, especially when managing multiple sessions.
|
|
3184
|
+
*
|
|
3185
|
+
* @returns A promise that resolves to the active session key as a string, or `undefined` if no active session is set.
|
|
3186
|
+
* @throws {ZeroXKeyError} If there is an error retrieving the active session key from storage.
|
|
3187
|
+
*/
|
|
3188
|
+
this.getActiveSessionKey = async () => {
|
|
3189
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
3190
|
+
return await this.storageManager.getActiveSessionKey();
|
|
3191
|
+
}, {
|
|
3192
|
+
errorMessage: "Failed to get active session key",
|
|
3193
|
+
errorCode: ZeroXKeyErrorCodes.GET_ACTIVE_SESSION_KEY_ERROR,
|
|
3194
|
+
});
|
|
3195
|
+
};
|
|
3196
|
+
/**
|
|
3197
|
+
* Clears any unused API key pairs from persistent storage.
|
|
3198
|
+
*
|
|
3199
|
+
* - This function scans all API key pairs stored in indexedDB and removes any key pairs that are not associated with a session in persistent storage.
|
|
3200
|
+
* - Ensures that only key pairs referenced by existing sessions are retained, preventing orphaned or stale key pairs from accumulating.
|
|
3201
|
+
* - Iterates through all stored session keys and builds a map of in-use public keys, then deletes any key pairs not present in this map.
|
|
3202
|
+
* - Intended to be called after session changes (e.g., login, logout, session replacement) to keep key storage clean and secure.
|
|
3203
|
+
*
|
|
3204
|
+
* @returns A promise that resolves when all unused key pairs are successfully cleared.
|
|
3205
|
+
* @throws {ZeroXKeyError} If there is an error listing, checking, or deleting unused key pairs.
|
|
3206
|
+
*/
|
|
3207
|
+
this.clearUnusedKeyPairs = async () => {
|
|
3208
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
3209
|
+
const publicKeys = await this.apiKeyStamper?.listKeyPairs();
|
|
3210
|
+
if (!publicKeys || publicKeys.length === 0) {
|
|
3211
|
+
return;
|
|
3212
|
+
}
|
|
3213
|
+
const sessionKeys = await this.storageManager?.listSessionKeys();
|
|
3214
|
+
const sessionTokensMap = {};
|
|
3215
|
+
for (const sessionKey of sessionKeys) {
|
|
3216
|
+
const session = await this.storageManager.getSession(sessionKey);
|
|
3217
|
+
if (session) {
|
|
3218
|
+
sessionTokensMap[session.publicKey] = sessionKey;
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
for (const publicKey of publicKeys) {
|
|
3222
|
+
if (!sessionTokensMap[publicKey]) {
|
|
3223
|
+
try {
|
|
3224
|
+
await this.apiKeyStamper?.deleteKeyPair(publicKey);
|
|
3225
|
+
}
|
|
3226
|
+
catch (error) {
|
|
3227
|
+
throw new ZeroXKeyError(`Failed to delete unused key pair ${publicKey}`, ZeroXKeyErrorCodes.INTERNAL_ERROR, error);
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
}, {
|
|
3232
|
+
errorMessage: "Failed to clear unused key pairs",
|
|
3233
|
+
errorCode: ZeroXKeyErrorCodes.CLEAR_UNUSED_KEY_PAIRS_ERROR,
|
|
3234
|
+
});
|
|
3235
|
+
};
|
|
3236
|
+
/**
|
|
3237
|
+
* Creates a new API key pair and returns the public key.
|
|
3238
|
+
*
|
|
3239
|
+
* - This function generates a new API key pair and stores it in the underlying key store (IndexedDB).
|
|
3240
|
+
* - If an external key pair is provided, it will use that key pair for creation instead of generating a new one.
|
|
3241
|
+
* - If `storeOverride` is set to true, the generated or provided public key will be set as the override key in the API key stamper, making it the active key for subsequent signing operations.
|
|
3242
|
+
* - Ensures the API key stamper is initialized before proceeding.
|
|
3243
|
+
* - Handles both native CryptoKeyPair objects and raw key material.
|
|
3244
|
+
*
|
|
3245
|
+
* @param params.externalKeyPair - An externally generated key pair (either a CryptoKeyPair or an object with publicKey/privateKey strings) to use instead of generating a new one.
|
|
3246
|
+
* @param params.storeOverride - If true, sets the generated or provided public key as the override key in the API key stamper (defaults to false).
|
|
3247
|
+
* @return A promise that resolves to the public key of the created or provided API key pair as a string.
|
|
3248
|
+
* @throws {ZeroXKeyError} If the API key stamper is not initialized or if there is an error during key pair creation or storage.
|
|
3249
|
+
*/
|
|
3250
|
+
this.createApiKeyPair = async (params) => {
|
|
3251
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
3252
|
+
const externalKeyPair = params?.externalKeyPair;
|
|
3253
|
+
const storeOverride = params?.storeOverride ?? false;
|
|
3254
|
+
if (!this.apiKeyStamper) {
|
|
3255
|
+
throw new ZeroXKeyError("API Key Stamper is not initialized.", ZeroXKeyErrorCodes.INTERNAL_ERROR);
|
|
3256
|
+
}
|
|
3257
|
+
const publicKey = await this.apiKeyStamper.createKeyPair(externalKeyPair ? externalKeyPair : undefined);
|
|
3258
|
+
if (storeOverride && publicKey) {
|
|
3259
|
+
this.apiKeyStamper.setTemporaryPublicKey(publicKey);
|
|
3260
|
+
}
|
|
3261
|
+
return publicKey;
|
|
3262
|
+
}, {
|
|
3263
|
+
errorMessage: "Failed to create API key pair",
|
|
3264
|
+
errorCode: ZeroXKeyErrorCodes.CREATE_API_KEY_PAIR_ERROR,
|
|
3265
|
+
});
|
|
3266
|
+
};
|
|
3267
|
+
/**
|
|
3268
|
+
* Fetches the WalletKit proxy authentication configuration from the auth proxy.
|
|
3269
|
+
*
|
|
3270
|
+
* - This function makes a request to the ZeroXKey auth proxy to retrieve the current WalletKit configuration,
|
|
3271
|
+
* including supported authentication methods, OAuth providers, and any custom proxy settings.
|
|
3272
|
+
* - Useful for dynamically configuring the client UI or authentication flows based on the proxy's capabilities.
|
|
3273
|
+
* - Ensures that the client is aware of the latest proxy-side configuration, which may affect available login/signup options.
|
|
3274
|
+
*
|
|
3275
|
+
* @returns A promise that resolves to a `ProxyTGetWalletKitConfigResponse` object containing the proxy authentication configuration.
|
|
3276
|
+
* @throws {ZeroXKeyError} If there is an error retrieving the proxy authentication configuration from the auth proxy.
|
|
3277
|
+
*/
|
|
3278
|
+
this.getProxyAuthConfig = async () => {
|
|
3279
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
3280
|
+
const res = await this.httpClient.proxyGetWalletKitConfig({});
|
|
3281
|
+
if (!res) {
|
|
3282
|
+
throw new ZeroXKeyError(`Failed to fetch auth proxy config`, ZeroXKeyErrorCodes.GET_PROXY_AUTH_CONFIG_ERROR);
|
|
3283
|
+
}
|
|
3284
|
+
return res;
|
|
3285
|
+
}, {
|
|
3286
|
+
errorMessage: "Failed to get auth proxy config",
|
|
3287
|
+
errorCode: ZeroXKeyErrorCodes.GET_PROXY_AUTH_CONFIG_ERROR,
|
|
3288
|
+
});
|
|
3289
|
+
};
|
|
3290
|
+
/**
|
|
3291
|
+
* Fetches the boot proof for a given app proof.
|
|
3292
|
+
*
|
|
3293
|
+
* - This function is idempotent: multiple calls with the same `app proof` will always return the boot proof.
|
|
3294
|
+
* - Attempts to find the boot proof for the given app proof.
|
|
3295
|
+
* - If a boot proof is found, it is returned as is.
|
|
3296
|
+
* - If no boot proof is found, an error is thrown.
|
|
3297
|
+
*
|
|
3298
|
+
* @param params.appProof - the app proof for which the boot proof is being fetched.
|
|
3299
|
+
* @param params.organizationId - organization ID to specify the sub-organization (defaults to the current session's organizationId).
|
|
3300
|
+
* @param params.stampWith - parameter to stamp the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
3301
|
+
* @returns A promise that resolves to the {@link v1BootProof} associated with the given app proof.
|
|
3302
|
+
* @throws {ZeroXKeyError} If there is no active session, if the input is invalid, or if boot proof retrieval fails.
|
|
3303
|
+
*/
|
|
3304
|
+
this.fetchBootProofForAppProof = async (params) => {
|
|
3305
|
+
const { appProof, stampWith = this.config.defaultStamperType, organizationId: organizationIdFromParams, } = params;
|
|
3306
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
3307
|
+
const session = await getActiveSessionOrThrowIfRequired(stampWith, this.storageManager.getActiveSession);
|
|
3308
|
+
const organizationId = organizationIdFromParams || session?.organizationId;
|
|
3309
|
+
if (!organizationId) {
|
|
3310
|
+
throw new ZeroXKeyError("Organization ID is required to fetch a Boot Proof.", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
3311
|
+
}
|
|
3312
|
+
// validate their input
|
|
3313
|
+
if (appProof === null || appProof?.publicKey === null) {
|
|
3314
|
+
throw new ZeroXKeyError("'appProof' is required and cannot be empty.", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
3315
|
+
}
|
|
3316
|
+
const ephemeralKey = appProof.publicKey;
|
|
3317
|
+
const bootProofResponse = await this.httpClient.getBootProof({
|
|
3318
|
+
organizationId,
|
|
3319
|
+
ephemeralKey,
|
|
3320
|
+
}, stampWith);
|
|
3321
|
+
if (!bootProofResponse || !bootProofResponse.bootProof) {
|
|
3322
|
+
throw new ZeroXKeyError("No boot proof found in the response", ZeroXKeyErrorCodes.BAD_RESPONSE);
|
|
3323
|
+
}
|
|
3324
|
+
return bootProofResponse.bootProof;
|
|
3325
|
+
}, {
|
|
3326
|
+
errorMessage: "Failed to get boot proof for app proof",
|
|
3327
|
+
errorCode: ZeroXKeyErrorCodes.FETCH_BOOT_PROOF_ERROR,
|
|
3328
|
+
});
|
|
3329
|
+
};
|
|
3330
|
+
/**
|
|
3331
|
+
* Verifies a list of app proofs against their corresponding boot proofs.
|
|
3332
|
+
*
|
|
3333
|
+
* - This function iterates through each provided app proof, fetches the corresponding boot proof, and verifies the app proof against the boot proof.
|
|
3334
|
+
* - If any app proof fails verification, an error is thrown.
|
|
3335
|
+
* @param params.appProofs - the app proofs to verify.
|
|
3336
|
+
* @param params.organizationId - organization ID to specify the sub-organization (defaults to the current session's organizationId).
|
|
3337
|
+
* @param params.stampWith - parameter to stamp the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
|
|
3338
|
+
* @returns A promise that resolves when all app proofs have been successfully verified.
|
|
3339
|
+
* @throws {ZeroXKeyError} If there is no active session, if the input is invalid, or if verification fails.
|
|
3340
|
+
*/
|
|
3341
|
+
this.verifyAppProofs = async (params) => {
|
|
3342
|
+
const { appProofs, stampWith = this.config.defaultStamperType, organizationId: organizationIdFromParams, } = params;
|
|
3343
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
3344
|
+
const session = await getActiveSessionOrThrowIfRequired(stampWith, this.storageManager.getActiveSession);
|
|
3345
|
+
const organizationId = organizationIdFromParams || session?.organizationId;
|
|
3346
|
+
if (!organizationId) {
|
|
3347
|
+
throw new ZeroXKeyError("Organization ID is required to verify app proofs.", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
3348
|
+
}
|
|
3349
|
+
if (!appProofs || appProofs.length === 0) {
|
|
3350
|
+
throw new ZeroXKeyError("'appProofs' is required and cannot be empty.", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
3351
|
+
}
|
|
3352
|
+
let lastPublicKey;
|
|
3353
|
+
let lastBootProof;
|
|
3354
|
+
for (const appProof of appProofs) {
|
|
3355
|
+
if (!appProof.publicKey) {
|
|
3356
|
+
throw new ZeroXKeyError("App proof publicKey is missing.", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
3357
|
+
}
|
|
3358
|
+
let bootProof;
|
|
3359
|
+
if (appProof.publicKey === lastPublicKey && lastBootProof) {
|
|
3360
|
+
bootProof = lastBootProof;
|
|
3361
|
+
}
|
|
3362
|
+
else {
|
|
3363
|
+
bootProof = await this.fetchBootProofForAppProof({
|
|
3364
|
+
appProof,
|
|
3365
|
+
organizationId,
|
|
3366
|
+
stampWith,
|
|
3367
|
+
});
|
|
3368
|
+
lastPublicKey = appProof.publicKey;
|
|
3369
|
+
lastBootProof = bootProof;
|
|
3370
|
+
}
|
|
3371
|
+
await verify(appProof, bootProof); // throws if invalid
|
|
3372
|
+
}
|
|
3373
|
+
}, {
|
|
3374
|
+
errorMessage: "Failed to verify app proofs",
|
|
3375
|
+
errorCode: ZeroXKeyErrorCodes.VERIFY_APP_PROOFS_ERROR,
|
|
3376
|
+
});
|
|
3377
|
+
};
|
|
3378
|
+
this.config = config;
|
|
3379
|
+
// Just store any explicitly provided stampers
|
|
3380
|
+
this.apiKeyStamper = apiKeyStamper;
|
|
3381
|
+
this.passkeyStamper = passkeyStamper;
|
|
3382
|
+
this.walletManager = walletManager;
|
|
3383
|
+
// Actual initialization will happen in init()
|
|
3384
|
+
}
|
|
3385
|
+
async init() {
|
|
3386
|
+
// Initialize storage manager
|
|
3387
|
+
// TODO (Amir): StorageManager should be a class that extends StorageBase and has an init method
|
|
3388
|
+
this.storageManager = await createStorageManager();
|
|
3389
|
+
// Initialize the API key stamper
|
|
3390
|
+
this.apiKeyStamper = new CrossPlatformApiKeyStamper(this.storageManager);
|
|
3391
|
+
// we parallelize independent initializations:
|
|
3392
|
+
// - API key stamper init
|
|
3393
|
+
// - Passkey stamper creation and init (if configured)
|
|
3394
|
+
// - Wallet manager creation (if configured)
|
|
3395
|
+
const initTasks = [this.apiKeyStamper.init()];
|
|
3396
|
+
if (this.config.passkeyConfig) {
|
|
3397
|
+
const passkeyStamper = new CrossPlatformPasskeyStamper(this.config.passkeyConfig);
|
|
3398
|
+
initTasks.push(passkeyStamper.init().then(() => {
|
|
3399
|
+
this.passkeyStamper = passkeyStamper;
|
|
3400
|
+
}));
|
|
3401
|
+
}
|
|
3402
|
+
if (this.config.walletConfig?.features?.auth ||
|
|
3403
|
+
this.config.walletConfig?.features?.connecting) {
|
|
3404
|
+
initTasks.push(createWalletManager(this.config.walletConfig).then((manager) => {
|
|
3405
|
+
this.walletManager = manager;
|
|
3406
|
+
}));
|
|
3407
|
+
}
|
|
3408
|
+
await Promise.all(initTasks);
|
|
3409
|
+
// Initialize the HTTP client with the appropriate stampers
|
|
3410
|
+
// Note: not passing anything here since we want to use the configured stampers and this.config
|
|
3411
|
+
this.httpClient = this.createHttpClient();
|
|
3412
|
+
}
|
|
3413
|
+
/**
|
|
3414
|
+
* @beta
|
|
3415
|
+
* **API subject to change**
|
|
3416
|
+
*
|
|
3417
|
+
* Polls ZeroXKey for the final result of a previously submitted transaction.
|
|
3418
|
+
*
|
|
3419
|
+
* This function repeatedly calls `getSendTransactionStatus` until the transaction
|
|
3420
|
+
* reaches a terminal state.
|
|
3421
|
+
*
|
|
3422
|
+
* Terminal states:
|
|
3423
|
+
* - **COMPLETED** or **INCLUDED** → resolves with chain-specific transaction details
|
|
3424
|
+
* - **FAILED** rejects with an error
|
|
3425
|
+
*
|
|
3426
|
+
* Behavior:
|
|
3427
|
+
*
|
|
3428
|
+
* - Queries ZeroXKey every 500ms.
|
|
3429
|
+
* - Stops polling automatically when a terminal state is reached.
|
|
3430
|
+
* - Returns the full status payload from ZeroXKey.
|
|
3431
|
+
* - When available, Ethereum transaction details are exposed at `resp.eth.txHash`.
|
|
3432
|
+
*
|
|
3433
|
+
* @param params.organizationId - Organization ID under which the transaction was submitted.
|
|
3434
|
+
* @param params.sendTransactionStatusId - Status ID returned by `ethSendTransaction` or `solSendTransaction`.
|
|
3435
|
+
* @param params.pollingIntervalMs - Optional polling interval in milliseconds (default: 500ms).
|
|
3436
|
+
* @param params.stampWith - Optional stamper to use for polling.
|
|
3437
|
+
*
|
|
3438
|
+
* @returns A promise resolving to the transaction status payload if successful.
|
|
3439
|
+
* @throws {ZeroXKeyError} If the transaction fails, is cancelled, or times out.
|
|
3440
|
+
*/
|
|
3441
|
+
async pollTransactionStatus(params) {
|
|
3442
|
+
const { organizationId: organizationIdFromParams, stampWith = this.config.defaultStamperType, sendTransactionStatusId, pollingIntervalMs, } = params;
|
|
3443
|
+
const session = await getActiveSessionOrThrowIfRequired(stampWith, this.storageManager.getActiveSession);
|
|
3444
|
+
const organizationId = organizationIdFromParams || session?.organizationId;
|
|
3445
|
+
if (!organizationId) {
|
|
3446
|
+
throw new ZeroXKeyError("Organization ID must be provided to fetch user", ZeroXKeyErrorCodes.INVALID_REQUEST);
|
|
3447
|
+
}
|
|
3448
|
+
return withZeroXKeyErrorHandling(async () => {
|
|
3449
|
+
return new Promise((resolve, reject) => {
|
|
3450
|
+
const interval = pollingIntervalMs ?? 500;
|
|
3451
|
+
const timeoutMs = 60_000; // 1 minute
|
|
3452
|
+
const ref = setInterval(async () => {
|
|
3453
|
+
const resp = await this.httpClient.getSendTransactionStatus({
|
|
3454
|
+
organizationId,
|
|
3455
|
+
sendTransactionStatusId,
|
|
3456
|
+
});
|
|
3457
|
+
const txStatus = resp?.txStatus;
|
|
3458
|
+
if (!txStatus)
|
|
3459
|
+
return;
|
|
3460
|
+
if (txStatus === "FAILED" || txStatus === "CANCELLED") {
|
|
3461
|
+
// TODO: use API enum in the future
|
|
3462
|
+
clearInterval(ref);
|
|
3463
|
+
clearTimeout(timeoutRef);
|
|
3464
|
+
reject(new ZeroXKeyError(resp.error?.message || `Transaction ${resp.txStatus}`, ZeroXKeyErrorCodes.POLL_TRANSACTION_STATUS_ERROR, resp));
|
|
3465
|
+
return;
|
|
3466
|
+
}
|
|
3467
|
+
if (txStatus === "COMPLETED" || txStatus === "INCLUDED") {
|
|
3468
|
+
// TODO: use API enum in the future
|
|
3469
|
+
clearInterval(ref);
|
|
3470
|
+
clearTimeout(timeoutRef);
|
|
3471
|
+
resolve(resp);
|
|
3472
|
+
}
|
|
3473
|
+
}, interval);
|
|
3474
|
+
const timeoutRef = setTimeout(() => {
|
|
3475
|
+
clearInterval(ref);
|
|
3476
|
+
reject(new Error("Polling timed out after 1 minute"));
|
|
3477
|
+
}, timeoutMs);
|
|
3478
|
+
});
|
|
3479
|
+
}, {
|
|
3480
|
+
errorMessage: "Failed to poll transaction status",
|
|
3481
|
+
errorCode: ZeroXKeyErrorCodes.POLL_TRANSACTION_STATUS_ERROR,
|
|
3482
|
+
});
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3486
|
+
export { ZeroXKeyClient };
|
|
3487
|
+
//# sourceMappingURL=core.mjs.map
|