@dynamic-labs-wallet/browser 1.0.92 → 1.0.94

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/index.cjs CHANGED
@@ -9,6 +9,8 @@ var loadArgon2idWasm = require('argon2id');
9
9
  var axios = require('axios');
10
10
  var gte = require('semver/functions/gte');
11
11
  var PQueue = require('p-queue');
12
+ var ed25519 = require('@noble/curves/ed25519');
13
+ var utils = require('@noble/curves/abstract/utils');
12
14
 
13
15
  function _extends() {
14
16
  _extends = Object.assign || function assign(target) {
@@ -78,12 +80,17 @@ function _extends() {
78
80
  return undefined;
79
81
  }
80
82
  };
81
- const getMPCSignatureScheme = ({ signingAlgorithm, baseRelayUrl = core.MPC_RELAY_PROD_API_URL })=>{
83
+ const getMPCSignatureScheme = ({ signingAlgorithm, baseRelayUrl = core.MPC_RELAY_PROD_API_URL, variant = core.DEFAULT_ED25519_VARIANT })=>{
82
84
  switch(signingAlgorithm){
83
85
  case primitives.SigningAlgorithm.ECDSA:
84
86
  return new web.Ecdsa(baseRelayUrl);
85
87
  case primitives.SigningAlgorithm.ED25519:
86
- return new web.ExportableEd25519(baseRelayUrl);
88
+ // `Ed25519` (non-exportable) and `ExportableEd25519` are distinct protocol
89
+ // classes with incompatible keyshare formats. Downstream `instanceof`
90
+ // dispatch (createWalletAccount / importPrivateKey / derive*) always tests
91
+ // `ExportableEd25519` before `Ed25519` — most-specific-first — so it stays
92
+ // correct even if `ExportableEd25519` were a subclass of `Ed25519`.
93
+ return variant === 'ed25519Standard' ? new web.Ed25519(baseRelayUrl) : new web.ExportableEd25519(baseRelayUrl);
87
94
  case primitives.SigningAlgorithm.BIP340:
88
95
  return new web.BIP340(baseRelayUrl);
89
96
  case primitives.SigningAlgorithm.EDBLS12_377:
@@ -92,11 +99,12 @@ const getMPCSignatureScheme = ({ signingAlgorithm, baseRelayUrl = core.MPC_RELAY
92
99
  throw new Error(`Unsupported signing algorithm: ${signingAlgorithm}`);
93
100
  }
94
101
  };
95
- const getMPCSigner = ({ chainName, baseRelayUrl, bitcoinConfig })=>{
102
+ const getMPCSigner = ({ chainName, baseRelayUrl, bitcoinConfig, variant })=>{
96
103
  const chainConfig = core.getMPCChainConfig(chainName, bitcoinConfig);
97
104
  const signatureScheme = getMPCSignatureScheme({
98
105
  signingAlgorithm: chainConfig.signingAlgorithm,
99
- baseRelayUrl
106
+ baseRelayUrl,
107
+ variant
100
108
  });
101
109
  return signatureScheme;
102
110
  };
@@ -1486,6 +1494,37 @@ const NON_RETRYABLE_CEREMONY_ERROR_MESSAGES = [
1486
1494
  ERROR_INCORRECT_PASSWORD,
1487
1495
  ERROR_MULTIPLE_WALLETS_PER_CHAIN
1488
1496
  ];
1497
+ /**
1498
+ * True when the backend rejected a create because the user already has a wallet
1499
+ * on that chain — an expected business-rule outcome, not a system failure.
1500
+ *
1501
+ * The string never appears on `error.message`: the backend returns it in the 400
1502
+ * response body as `{ existingWalletId, error: '<this string>' }`, so an
1503
+ * `AxiosError` carries axios's own "Request failed with status code 400" message
1504
+ * and `handleAxiosError` maps the status to "Invalid request". `WalletApiError`
1505
+ * does preserve the body value, but on `code` (via `extractServerErrorCode`),
1506
+ * not `message`. A `.message` comparison therefore never matches, which is why
1507
+ * the intended downgrade at the keyGen catch site had never fired in production.
1508
+ *
1509
+ * `WalletApiError` is matched on `name` as well as `instanceof`, for the reason
1510
+ * documented on `getWalletApiErrorStatus` in services/logger.ts: core is inlined
1511
+ * into more than one bundle, so a second copy of the class defeats `instanceof`.
1512
+ * A foreign copy has no `response.data` and the generic "Invalid request"
1513
+ * message, so an instanceof-only check misses every branch below.
1514
+ */ const isMultipleWalletsPerChainRejection = (error)=>{
1515
+ var _error_response;
1516
+ const isWalletApiError = error instanceof core.WalletApiError || error instanceof Error && error.name === 'WalletApiError';
1517
+ if (isWalletApiError && error.code === ERROR_MULTIPLE_WALLETS_PER_CHAIN) return true;
1518
+ // Raw AxiosError: the rejection reaches the keyGen catch before
1519
+ // `handleAxiosError` has converted it, so read the response body directly.
1520
+ const body = error == null ? void 0 : (_error_response = error.response) == null ? void 0 : _error_response.data;
1521
+ if (body && typeof body === 'object' && body['error'] === ERROR_MULTIPLE_WALLETS_PER_CHAIN) {
1522
+ return true;
1523
+ }
1524
+ // SSE transport assigns the body onto a plain Error.
1525
+ if ((error == null ? void 0 : error.error) === ERROR_MULTIPLE_WALLETS_PER_CHAIN) return true;
1526
+ return error instanceof Error && error.message === ERROR_MULTIPLE_WALLETS_PER_CHAIN;
1527
+ };
1489
1528
  /**
1490
1529
  * Marks an error as non-retryable in place so `retryPromise` will skip
1491
1530
  * remaining attempts. Used at ceremony retry boundaries.
@@ -2281,14 +2320,33 @@ const classifyOne = (error)=>{
2281
2320
  return fallback;
2282
2321
  };
2283
2322
  const MAX_DESCRIBED_LENGTH = 200;
2323
+ /**
2324
+ * Keys safe to echo when a thrown object exposes no recognised message field.
2325
+ * Deliberately an allowlist: the alternative — serialising the whole object —
2326
+ * cannot be made safe, because a thrown MPC signer rejection or API body may
2327
+ * carry key-share material, a raw transaction body or an auth token.
2328
+ */ const SAFE_DETAIL_KEYS = [
2329
+ 'status',
2330
+ 'statusCode',
2331
+ 'code',
2332
+ 'error_code',
2333
+ 'errorCode',
2334
+ 'name',
2335
+ 'type'
2336
+ ];
2337
+ const isScalar = (value)=>typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
2284
2338
  /**
2285
2339
  * Builds a human-readable message for a thrown value that is not an `Error` —
2286
2340
  * most often an API response body, e.g.
2287
2341
  * `{ existingWalletId, error: 'Multiple wallets per chain not allowed' }`.
2288
2342
  *
2289
2343
  * `String(value)` would collapse that to `[object Object]`, losing the only
2290
- * diagnostic content it had, so prefer a recognisable message field and fall
2291
- * back to bounded JSON.
2344
+ * diagnostic content it had, so a recognised message field is preferred.
2345
+ *
2346
+ * Never serialises an unrecognised object wholesale. Where no message field
2347
+ * exists, only `SAFE_DETAIL_KEYS` scalars are echoed, and failing that a fixed
2348
+ * placeholder — so a value carrying secrets can never reach the logging backend
2349
+ * just because it lacked a `message`.
2292
2350
  */ const describeNonError = (value)=>{
2293
2351
  if (typeof value === 'string') return value.slice(0, MAX_DESCRIBED_LENGTH);
2294
2352
  if (value !== null && typeof value === 'object') {
@@ -2299,11 +2357,8 @@ const MAX_DESCRIBED_LENGTH = 200;
2299
2357
  record['detail']
2300
2358
  ].find((candidate)=>typeof candidate === 'string' && candidate.length > 0);
2301
2359
  if (named) return named.slice(0, MAX_DESCRIBED_LENGTH);
2302
- try {
2303
- return JSON.stringify(value).slice(0, MAX_DESCRIBED_LENGTH);
2304
- } catch (e) {
2305
- // Circular or non-serialisable — fall through to String().
2306
- }
2360
+ const safeDetails = SAFE_DETAIL_KEYS.filter((key)=>isScalar(record[key])).map((key)=>`${key}=${String(record[key])}`);
2361
+ return safeDetails.length > 0 ? safeDetails.join(' ').slice(0, MAX_DESCRIBED_LENGTH) : 'Non-Error value thrown with no recognised message field';
2307
2362
  }
2308
2363
  return String(value).slice(0, MAX_DESCRIBED_LENGTH);
2309
2364
  };
@@ -2319,27 +2374,38 @@ const MAX_DESCRIBED_LENGTH = 200;
2319
2374
  * under `operationError` instead, and a real `Error` is synthesised so the log
2320
2375
  * still gets a stack and an `error.kind`.
2321
2376
  *
2377
+ * A non-`Error` is recorded as its bounded `describeNonError` description only,
2378
+ * never the raw value — the same rule `logError` applies when it logs a cause as
2379
+ * `{ name, message }` — while `operationError` remains its own attribute so "the
2380
+ * thrown value was not an Error" stays queryable.
2381
+ *
2322
2382
  * Returned as a plain object so this is unit-testable without constructing a
2323
2383
  * wallet client (which pulls the MPC WASM bundle).
2324
2384
  */ const buildOperationFailureLog = (error)=>{
2325
2385
  // Classify the original value: an Error's `cause` chain carries the severity,
2326
2386
  // and normalising first would discard it.
2327
2387
  const level = resolveLogLevel(error, 'error');
2328
- return error instanceof Error ? {
2388
+ if (error instanceof Error) return {
2329
2389
  level,
2330
2390
  context: undefined,
2331
2391
  error
2332
- } : {
2392
+ };
2393
+ const described = describeNonError(error);
2394
+ return {
2333
2395
  level,
2334
2396
  context: {
2335
- operationError: error
2397
+ operationError: described
2336
2398
  },
2337
- error: new Error(describeNonError(error))
2399
+ error: new Error(described)
2338
2400
  };
2339
2401
  };
2340
2402
  const logError = ({ message, error, context, level = 'error' })=>{
2341
2403
  if (error instanceof axios.AxiosError) {
2342
- core.handleAxiosError(error, message, context);
2404
+ // Forward the caller's severity hint. It used to be dropped here, so a call
2405
+ // site that knew its failure was expected still got logged at `error`.
2406
+ core.handleAxiosError(error, message, context, {
2407
+ level
2408
+ });
2343
2409
  return;
2344
2410
  }
2345
2411
  const resolvedLevel = resolveLogLevel(error, level);
@@ -3377,28 +3443,34 @@ class DynamicWalletClient {
3377
3443
  }, this.getTraceContext(traceContext))));
3378
3444
  throw new Error('Timed out waiting for wallet creation ceremony to complete');
3379
3445
  }
3380
- async clientInitializeKeyGen({ chainName, thresholdSignatureScheme, bitcoinConfig }) {
3446
+ async clientInitializeKeyGen({ chainName, thresholdSignatureScheme, bitcoinConfig, variant }) {
3381
3447
  // Get the mpc signer
3382
3448
  const mpcSigner = getMPCSigner({
3383
3449
  chainName,
3384
3450
  baseRelayUrl: this.baseMPCRelayApiUrl,
3385
- bitcoinConfig
3451
+ bitcoinConfig,
3452
+ variant
3386
3453
  });
3387
3454
  const clientThreshold = core.getClientThreshold(thresholdSignatureScheme);
3388
3455
  const keygenInitResults = await Promise.all(Array(clientThreshold).fill(null).map(()=>mpcSigner.initKeygen()));
3389
3456
  return keygenInitResults;
3390
3457
  }
3391
- async derivePublicKey({ chainName, keyShare, derivationPath, bitcoinConfig }) {
3458
+ async derivePublicKey({ chainName, keyShare, derivationPath, bitcoinConfig, variant }) {
3392
3459
  const mpcSigner = getMPCSigner({
3393
3460
  chainName,
3394
3461
  baseRelayUrl: this.baseMPCRelayApiUrl,
3395
- bitcoinConfig
3462
+ bitcoinConfig,
3463
+ variant
3396
3464
  });
3397
3465
  let publicKey;
3398
3466
  if (mpcSigner instanceof web.Ecdsa) {
3399
3467
  publicKey = await mpcSigner.derivePubkey(keyShare, derivationPath);
3400
3468
  } else if (mpcSigner instanceof web.ExportableEd25519) {
3401
3469
  publicKey = await mpcSigner.getPubkey(keyShare);
3470
+ } else if (mpcSigner instanceof web.Ed25519) {
3471
+ // Non-exportable Ed25519 (raw-scalar-imported wallets). Derivation is not
3472
+ // standardized for ed25519, so the root pubkey is taken with an undefined path.
3473
+ publicKey = await mpcSigner.derivePubkey(keyShare, derivationPath);
3402
3474
  } else if (mpcSigner instanceof web.BIP340) {
3403
3475
  publicKey = await mpcSigner.deriveTweakPubkey(keyShare, derivationPath);
3404
3476
  }
@@ -3727,7 +3799,7 @@ class DynamicWalletClient {
3727
3799
  // rejection (the user already has a wallet for this chain), not a system
3728
3800
  // failure — log it at warn so it doesn't flood error dashboards. All
3729
3801
  // other keygen failures stay at error.
3730
- const isExpectedRejection = error instanceof Error && error.message === ERROR_MULTIPLE_WALLETS_PER_CHAIN;
3802
+ const isExpectedRejection = isMultipleWalletsPerChainRejection(error);
3731
3803
  // On the HTTP transport, tag the failure with the cross-repo join keys and
3732
3804
  // a stable errorKind so it lines up with the redcoast worker ceremony logs.
3733
3805
  const usedHttpTransport = this.featureFlags[core.FEATURE_FLAGS.ENABLE_HTTP_WAAS_TRANSPORT] === true && !!idempotencyKey;
@@ -3762,7 +3834,7 @@ class DynamicWalletClient {
3762
3834
  shouldRetry: (error)=>!isNonRetryableCeremonyError(error)
3763
3835
  });
3764
3836
  }
3765
- async runImportRawPrivateKeyAttempt({ chainName, privateKey, thresholdSignatureScheme, bitcoinConfig, onError, onCeremonyComplete, traceContext, legacyWalletId, password, signedSessionId }) {
3837
+ async runImportRawPrivateKeyAttempt({ chainName, privateKey, thresholdSignatureScheme, bitcoinConfig, onError, onCeremonyComplete, traceContext, legacyWalletId, password, signedSessionId, isRawScalarImport }) {
3766
3838
  const dynamicRequestId = uuid.v4();
3767
3839
  try {
3768
3840
  this.assertPasswordRequired(password);
@@ -3770,15 +3842,21 @@ class DynamicWalletClient {
3770
3842
  password,
3771
3843
  signedSessionId
3772
3844
  });
3845
+ // A raw-scalar import must run on the non-exportable `Ed25519` protocol for
3846
+ // the whole ceremony (init keygen, import, pubkey derivation) so the produced
3847
+ // key shares are consistent. Non-raw imports keep the default ('ed25519Exportable').
3848
+ const variant = isRawScalarImport ? 'ed25519Standard' : undefined;
3773
3849
  const mpcSigner = getMPCSigner({
3774
3850
  chainName,
3775
3851
  baseRelayUrl: this.baseMPCRelayApiUrl,
3776
- bitcoinConfig
3852
+ bitcoinConfig,
3853
+ variant
3777
3854
  });
3778
3855
  const clientKeygenInitResults = await this.clientInitializeKeyGen({
3779
3856
  chainName,
3780
3857
  thresholdSignatureScheme,
3781
- bitcoinConfig
3858
+ bitcoinConfig,
3859
+ variant
3782
3860
  });
3783
3861
  const clientKeygenIds = clientKeygenInitResults.map((result)=>result.keygenId);
3784
3862
  this.logger.info('[DynamicWaasWalletClient] Client key generation initialized', _extends({
@@ -3796,7 +3874,9 @@ class DynamicWalletClient {
3796
3874
  bitcoinConfig,
3797
3875
  onError,
3798
3876
  onCeremonyComplete,
3799
- legacyWalletId
3877
+ legacyWalletId,
3878
+ // Persist the protocol variant server-side so later sign/reshare read it back.
3879
+ variant
3800
3880
  });
3801
3881
  this.logger.info('[DynamicWaasWalletClient] Server key generation initialized', _extends({
3802
3882
  roomId,
@@ -3814,7 +3894,9 @@ class DynamicWalletClient {
3814
3894
  ...serverKeygenIds,
3815
3895
  ...otherClientKeygenIds
3816
3896
  ];
3817
- const importerKeygenResult = await mpcSigner.importPrivateKeyImporter(roomId, threshold, privateKey, currentInit, otherKeyGenIds);
3897
+ const importerKeygenResult = isRawScalarImport ? // uses the 32 bytes directly as the signing scalar instead of
3898
+ // SHA-512-expanding them as an RFC-8032 seed.
3899
+ await mpcSigner.importPrivateKeyImporter(roomId, threshold, privateKey, currentInit, otherKeyGenIds, true) : await mpcSigner.importPrivateKeyImporter(roomId, threshold, privateKey, currentInit, otherKeyGenIds);
3818
3900
  return importerKeygenResult;
3819
3901
  } else {
3820
3902
  const recipientKeygenResult = await mpcSigner.importPrivateKeyRecipient(roomId, threshold, currentInit, [
@@ -3831,7 +3913,8 @@ class DynamicWalletClient {
3831
3913
  chainName,
3832
3914
  keyShare: clientKeygenResult,
3833
3915
  derivationPath,
3834
- bitcoinConfig
3916
+ bitcoinConfig,
3917
+ variant
3835
3918
  });
3836
3919
  this.logger.info('[DynamicWaasWalletClient] Completed import of raw private key', _extends({
3837
3920
  rawPublicKey,
@@ -3866,6 +3949,8 @@ class DynamicWalletClient {
3866
3949
  if (typeof message !== 'string') {
3867
3950
  message = `0x${Buffer.from(message).toString('hex')}`;
3868
3951
  }
3952
+ // Note: the server signing party (wallet-service) selects the ed25519
3953
+ // protocol variant from its own persisted EAC, so it is NOT threaded here.
3869
3954
  const serializedContext = context ? JSON.parse(JSON.stringify(context, (_key, value)=>typeof value === 'bigint' ? value.toString() : value)) : undefined;
3870
3955
  const useHttpTransport = this.featureFlags[core.FEATURE_FLAGS.ENABLE_HTTP_WAAS_TRANSPORT] === true && this.featureFlags[core.FEATURE_FLAGS.ENABLE_HTTP_WAAS_TRANSPORT_SIGNMESSAGE] === true;
3871
3956
  const params = {
@@ -3929,7 +4014,7 @@ class DynamicWalletClient {
3929
4014
  }
3930
4015
  return signatureBytes;
3931
4016
  }
3932
- async forwardMPCClientSign({ chainName, message, roomId, keyShare, derivationPath, formattedMessage, dynamicRequestId, isFormatted, traceContext, bitcoinConfig }) {
4017
+ async forwardMPCClientSign({ chainName, message, roomId, keyShare, derivationPath, formattedMessage, dynamicRequestId, isFormatted, traceContext, bitcoinConfig, variant }) {
3933
4018
  try {
3934
4019
  const chainConfig = core.getMPCChainConfig(chainName, bitcoinConfig);
3935
4020
  const signingAlgo = chainConfig.signingAlgorithm;
@@ -3942,7 +4027,7 @@ class DynamicWalletClient {
3942
4027
  });
3943
4028
  const tweak = this.convertTweakForBIP340(bitcoinConfig == null ? void 0 : bitcoinConfig.tweak);
3944
4029
  this.logger.info('Forward MPC enabled, signing message with forward MPC (new)', this.getTraceContext(traceContext));
3945
- const { signature: signatureBytes } = await this.apiClient.forwardMPCClient.signMessage({
4030
+ const { signature: signatureBytes } = await this.apiClient.forwardMPCClient.signMessage(_extends({
3946
4031
  keyshare: keyShare,
3947
4032
  message: messageToSign,
3948
4033
  relayDomain: this.baseMPCRelayApiUrl || '',
@@ -3954,7 +4039,9 @@ class DynamicWalletClient {
3954
4039
  traceContext,
3955
4040
  userId: this.userId,
3956
4041
  environmentId: this.environmentId
3957
- });
4042
+ }, variant === 'ed25519Standard' ? {
4043
+ ed25519Variant: 'standard'
4044
+ } : {}));
3958
4045
  if (!(signatureBytes instanceof Uint8Array)) {
3959
4046
  throw new TypeError(`Invalid signature format: expected Uint8Array, got ${typeof signatureBytes}`);
3960
4047
  }
@@ -3974,15 +4061,22 @@ class DynamicWalletClient {
3974
4061
  throw error;
3975
4062
  }
3976
4063
  }
3977
- async clientSign({ chainName, message, roomId: initialRoomId, keyShare, derivationPath, isFormatted, dynamicRequestId, traceContext, bitcoinConfig, refreshRoom }) {
4064
+ async clientSign({ chainName, message, roomId: initialRoomId, keyShare, derivationPath, isFormatted, dynamicRequestId, traceContext, bitcoinConfig, variant, refreshRoom }) {
3978
4065
  // Reassigned when forward MPC fails and we mint a fresh room for the
3979
4066
  // relay-based fallback below.
3980
4067
  let roomId = initialRoomId;
4068
+ // Non-exportable Ed25519 (raw-scalar import) derives its address at the
4069
+ // root — see derivePrivateKeyFromExport's identical guard. Unlike
4070
+ // ExportableEd25519 (which ignores this arg), Ed25519.sign() *applies* a
4071
+ // non-undefined path, so forwarding the wallet's stored derivation path
4072
+ // here would sign under a derived child key instead of the wallet's own.
4073
+ const effectiveDerivationPath = variant === 'ed25519Standard' ? undefined : derivationPath;
3981
4074
  try {
3982
4075
  const mpcSigner = getMPCSigner({
3983
4076
  chainName,
3984
4077
  baseRelayUrl: this.baseMPCRelayApiUrl,
3985
- bitcoinConfig
4078
+ bitcoinConfig,
4079
+ variant
3986
4080
  });
3987
4081
  const formattedMessage = isFormatted ? new web.MessageHash(message) : formatMessage(chainName, message);
3988
4082
  this.logger.debug('[DynamicWaasWalletClient] Starting client sign', _extends({
@@ -4006,12 +4100,13 @@ class DynamicWalletClient {
4006
4100
  message,
4007
4101
  roomId,
4008
4102
  keyShare,
4009
- derivationPath,
4103
+ derivationPath: effectiveDerivationPath,
4010
4104
  formattedMessage,
4011
4105
  dynamicRequestId,
4012
4106
  isFormatted,
4013
4107
  traceContext,
4014
- bitcoinConfig
4108
+ bitcoinConfig,
4109
+ variant
4015
4110
  });
4016
4111
  } catch (error) {
4017
4112
  const errorInfo = core.classifyForwardMpcError(error);
@@ -4055,7 +4150,7 @@ class DynamicWalletClient {
4055
4150
  if (!('sign' in mpcSigner)) {
4056
4151
  throw new TypeError(`Message signing is not supported for chain ${chainName} — use signTransaction instead`);
4057
4152
  }
4058
- const signature = await mpcSigner.sign(roomId, keyShare, messageToSign, derivationPath, tweak);
4153
+ const signature = await mpcSigner.sign(roomId, keyShare, messageToSign, effectiveDerivationPath, tweak);
4059
4154
  return signature;
4060
4155
  } catch (error) {
4061
4156
  logError({
@@ -4139,7 +4234,8 @@ class DynamicWalletClient {
4139
4234
  chainName,
4140
4235
  localShares,
4141
4236
  recordedKeygenIds,
4142
- bitcoinConfig
4237
+ bitcoinConfig,
4238
+ variant: this.ed25519VariantForWallet(accountAddress)
4143
4239
  });
4144
4240
  localKeygenIds = comparison.localKeygenIds;
4145
4241
  staleness = comparison.fresh ? 'fresh' : 'stale';
@@ -4205,11 +4301,14 @@ class DynamicWalletClient {
4205
4301
  * the server recorded for the wallet's Dynamic backups. Shared by the
4206
4302
  * stale-share heal (verifyAndRecoverStaleShare) and the backup preflight
4207
4303
  * (ensureLocalSharesAreFresh).
4208
- */ async compareShareGenerations({ accountAddress, chainName, localShares, recordedKeygenIds, bitcoinConfig }) {
4304
+ */ async compareShareGenerations({ accountAddress, chainName, localShares, recordedKeygenIds, bitcoinConfig, variant }) {
4209
4305
  const mpcSigner = getMPCSigner({
4210
4306
  chainName,
4211
4307
  baseRelayUrl: this.baseMPCRelayApiUrl,
4212
- bitcoinConfig: bitcoinConfig != null ? bitcoinConfig : this.getBitcoinConfigForChain(chainName, accountAddress)
4308
+ bitcoinConfig: bitcoinConfig != null ? bitcoinConfig : this.getBitcoinConfigForChain(chainName, accountAddress),
4309
+ // Raw-scalar (non-exportable Ed25519) wallets: build the matching signer so
4310
+ // getKeygenIdForShare deserializes the Ed25519 share (else "Invalid discriminant").
4311
+ variant
4213
4312
  });
4214
4313
  const localKeygenIds = await Promise.all(localShares.map((share)=>this.getKeygenIdForShare(mpcSigner, share)));
4215
4314
  const recordedSet = new Set(recordedKeygenIds);
@@ -4367,6 +4466,7 @@ class DynamicWalletClient {
4367
4466
  dynamicRequestId,
4368
4467
  traceContext,
4369
4468
  bitcoinConfig,
4469
+ variant: wallet.variant,
4370
4470
  // On forward-MPC fallback, re-run the server sign with no roomId so a
4371
4471
  // brand-new server-joined room is created for the relay ceremony.
4372
4472
  refreshRoom: async ()=>{
@@ -4666,7 +4766,11 @@ class DynamicWalletClient {
4666
4766
  const mpcSigner = getMPCSigner({
4667
4767
  chainName,
4668
4768
  baseRelayUrl: this.baseMPCRelayApiUrl,
4669
- bitcoinConfig
4769
+ bitcoinConfig,
4770
+ // Raw-scalar (non-exportable Ed25519) wallets must refresh on the same
4771
+ // protocol their shares were created with, or the ceremony fails to
4772
+ // deserialize them ("Invalid discriminant").
4773
+ variant: this.ed25519VariantForWallet(accountAddress)
4670
4774
  });
4671
4775
  // Ensure client key shares exist before hitting the API
4672
4776
  const clientKeyShares = await this.ensureClientShare(accountAddress);
@@ -4848,11 +4952,26 @@ class DynamicWalletClient {
4848
4952
  }
4849
4953
  return mpcSigner.exportID(share);
4850
4954
  }
4851
- async getExportId({ chainName, clientKeyShare, bitcoinConfig }) {
4955
+ /**
4956
+ * The ed25519 protocol variant for an existing wallet, read from the wallet map
4957
+ * (sourced from `walletProperties.settings.variant`, persisted at import).
4958
+ *
4959
+ * It is per-wallet protocol metadata — like `thresholdSignatureScheme` or
4960
+ * `addressType` — and the single source of truth for which signer to build when
4961
+ * operating on a wallet's key shares. Wallets imported from a raw ed25519 scalar
4962
+ * (e.g. Fireblocks Embedded Wallets) are `'ed25519Standard'` (non-exportable `Ed25519`);
4963
+ * everything else is `undefined` → the default exportable protocol. Using the
4964
+ * wrong one fails to deserialize the key share with "Invalid discriminant".
4965
+ */ ed25519VariantForWallet(accountAddress) {
4966
+ var _this_getWalletFromMap;
4967
+ return (_this_getWalletFromMap = this.getWalletFromMap(accountAddress)) == null ? void 0 : _this_getWalletFromMap.variant;
4968
+ }
4969
+ async getExportId({ chainName, clientKeyShare, bitcoinConfig, accountAddress }) {
4852
4970
  const mpcSigner = getMPCSigner({
4853
4971
  chainName,
4854
4972
  baseRelayUrl: this.baseMPCRelayApiUrl,
4855
- bitcoinConfig
4973
+ bitcoinConfig,
4974
+ variant: accountAddress ? this.ed25519VariantForWallet(accountAddress) : undefined
4856
4975
  });
4857
4976
  try {
4858
4977
  const exportId = await this.getKeygenIdForShare(mpcSigner, clientKeyShare);
@@ -4900,7 +5019,8 @@ class DynamicWalletClient {
4900
5019
  return this.getExportId({
4901
5020
  chainName,
4902
5021
  clientKeyShare,
4903
- bitcoinConfig
5022
+ bitcoinConfig,
5023
+ accountAddress
4904
5024
  });
4905
5025
  }
4906
5026
  /**
@@ -4941,7 +5061,8 @@ class DynamicWalletClient {
4941
5061
  const existingClientKeygenIds = await Promise.all(existingClientKeyShares.map(async (keyShare)=>await this.getExportId({
4942
5062
  chainName,
4943
5063
  clientKeyShare: keyShare,
4944
- bitcoinConfig
5064
+ bitcoinConfig,
5065
+ accountAddress
4945
5066
  })));
4946
5067
  return {
4947
5068
  newClientInitKeygenResults,
@@ -5010,7 +5131,8 @@ class DynamicWalletClient {
5010
5131
  const existingClientKeygenId = await this.getExportId({
5011
5132
  chainName,
5012
5133
  clientKeyShare: existingClientShare,
5013
- bitcoinConfig
5134
+ bitcoinConfig,
5135
+ accountAddress
5014
5136
  });
5015
5137
  // Gate matches internalRefresh/Reshare — SSE error rejects awaitTerminal.
5016
5138
  let newShareSetId;
@@ -5619,7 +5741,11 @@ class DynamicWalletClient {
5619
5741
  const mpcSigner = getMPCSigner({
5620
5742
  chainName,
5621
5743
  baseRelayUrl: this.baseMPCRelayApiUrl,
5622
- bitcoinConfig
5744
+ bitcoinConfig,
5745
+ // Raw-scalar (non-exportable Ed25519) wallets must reshare on the same
5746
+ // protocol their shares were created with, or the ceremony fails to
5747
+ // deserialize them ("Invalid discriminant").
5748
+ variant: this.ed25519VariantForWallet(accountAddress)
5623
5749
  });
5624
5750
  let existingReshareResults;
5625
5751
  let newReshareResults;
@@ -5983,6 +6109,9 @@ class DynamicWalletClient {
5983
6109
  if (mpcSigner instanceof web.ExportableEd25519) {
5984
6110
  return new web.ExportableEd25519KeygenResult(extractedPubkey, secretShare);
5985
6111
  }
6112
+ if (mpcSigner instanceof web.Ed25519) {
6113
+ return new web.Ed25519KeygenResult(extractedPubkey, secretShare);
6114
+ }
5986
6115
  return new web.BIP340KeygenResult(extractedPubkey, secretShare);
5987
6116
  }
5988
6117
  async exportKey({ accountAddress, chainName, bitcoinConfig, password = undefined, signedSessionId, mfaToken, elevatedAccessToken, traceContext }) {
@@ -6013,13 +6142,15 @@ class DynamicWalletClient {
6013
6142
  const mpcSigner = getMPCSigner({
6014
6143
  chainName,
6015
6144
  baseRelayUrl: this.baseMPCRelayApiUrl,
6016
- bitcoinConfig
6145
+ bitcoinConfig,
6146
+ variant: wallet.variant
6017
6147
  });
6018
6148
  const reconstructedKeyShare = await this.getReconstructedKeyShare(accountAddress, mpcSigner);
6019
6149
  const exportId = await this.getExportId({
6020
6150
  chainName,
6021
6151
  clientKeyShare: reconstructedKeyShare,
6022
- bitcoinConfig
6152
+ bitcoinConfig,
6153
+ accountAddress: wallet.accountAddress
6023
6154
  });
6024
6155
  const data = await this.apiClient.exportKey({
6025
6156
  walletId: wallet.walletId,
@@ -6162,20 +6293,35 @@ class DynamicWalletClient {
6162
6293
  if (mpcSigner instanceof web.Ecdsa) {
6163
6294
  return mpcSigner.derivePrivateKeyFromXpriv(keyExportRaw, derivationPath);
6164
6295
  } else if (mpcSigner instanceof web.ExportableEd25519) {
6296
+ // ExportableEd25519's exportFullPrivateKey already returns the RFC-8032
6297
+ // form (hex of seed||pubkey); pass it straight through.
6165
6298
  return keyExportRaw;
6299
+ } else if (mpcSigner instanceof web.Ed25519) {
6300
+ // Non-exportable Ed25519's exportFullPrivateKey returns an `spriv`
6301
+ // (extended private key), NOT a raw hex key — it must be parsed to the
6302
+ // actual private scalar. Returning the spriv verbatim makes the chain
6303
+ // client hex-decode it into an empty buffer (blank export field).
6304
+ //
6305
+ // This protocol is only used for raw-scalar import, whose public key is
6306
+ // the ROOT scalar (`A = s·B`, derived at import via derivePubkey(_, undefined)).
6307
+ // The chain-config derivation path stored on the wallet (e.g. Solana's
6308
+ // [44,501,0,0,0]) must NOT be applied here — deriving a child scalar would
6309
+ // export a key that doesn't correspond to the wallet's address.
6310
+ return mpcSigner.derivePrivateKeyFromSpriv(keyExportRaw);
6166
6311
  } else if (mpcSigner instanceof web.BIP340) {
6167
6312
  return mpcSigner.derivePrivateKeyFromXpriv(keyExportRaw, derivationPath);
6168
6313
  }
6169
6314
  return undefined;
6170
6315
  }
6171
- async offlineExportKey({ chainName, keyShares, derivationPath }) {
6316
+ async offlineExportKey({ chainName, keyShares, derivationPath, variant }) {
6172
6317
  try {
6173
6318
  if (!keyShares || keyShares.length < 2) {
6174
6319
  throw new Error(`Must provide at least min threshold of key shares`);
6175
6320
  }
6176
6321
  const mpcSigner = getMPCSigner({
6177
6322
  chainName,
6178
- baseRelayUrl: this.baseMPCRelayApiUrl
6323
+ baseRelayUrl: this.baseMPCRelayApiUrl,
6324
+ variant
6179
6325
  });
6180
6326
  const walletKeyShares = keyShares.map((keyShare)=>{
6181
6327
  if (!('pubkey' in keyShare)) {
@@ -6194,6 +6340,10 @@ class DynamicWalletClient {
6194
6340
  derivedPrivateKey = await mpcSigner.derivePrivateKeyFromXpriv(keyExportRaw, walletDerivationPath);
6195
6341
  } else if (mpcSigner instanceof web.ExportableEd25519) {
6196
6342
  derivedPrivateKey = keyExportRaw;
6343
+ } else if (mpcSigner instanceof web.Ed25519) {
6344
+ // Non-exportable Ed25519 (raw-scalar) returns an `spriv`, not raw hex —
6345
+ // parse it to the root scalar (see derivePrivateKeyFromExport).
6346
+ derivedPrivateKey = await mpcSigner.derivePrivateKeyFromSpriv(keyExportRaw);
6197
6347
  } else if (mpcSigner instanceof web.BIP340) {
6198
6348
  derivedPrivateKey = await mpcSigner.derivePrivateKeyFromXpriv(keyExportRaw, walletDerivationPath);
6199
6349
  }
@@ -6399,7 +6549,7 @@ class DynamicWalletClient {
6399
6549
  * @param thresholdSignatureScheme - The TSS scheme used
6400
6550
  * @param derivationPath - Optional derivation path (will be computed from chainConfig if not provided)
6401
6551
  * @param additionalProps - Any chain-specific additional properties to merge
6402
- */ initializeWalletMapEntry({ accountAddress, walletId, chainName, thresholdSignatureScheme, derivationPath, shareSetId, shareSetType, otherShareSets, additionalProps = {} }) {
6552
+ */ initializeWalletMapEntry({ accountAddress, walletId, chainName, thresholdSignatureScheme, derivationPath, shareSetId, shareSetType, otherShareSets, variant, additionalProps = {} }) {
6403
6553
  this.updateWalletMap(accountAddress, _extends({
6404
6554
  accountAddress,
6405
6555
  walletId,
@@ -6413,6 +6563,8 @@ class DynamicWalletClient {
6413
6563
  shareSetType
6414
6564
  } : {}, otherShareSets ? {
6415
6565
  otherShareSets
6566
+ } : {}, variant ? {
6567
+ variant
6416
6568
  } : {}, {
6417
6569
  clientKeySharesBackupInfo: getClientKeyShareBackupInfo()
6418
6570
  }, additionalProps));
@@ -6632,7 +6784,7 @@ class DynamicWalletClient {
6632
6784
  tokenSource: googleDriveTokenSource
6633
6785
  } : {});
6634
6786
  }
6635
- async publishDelegatedShare({ walletId, shareSetId, delegatedShare, signedSessionId, dynamicRequestId, chainName, bitcoinConfig, isPasswordEncrypted }) {
6787
+ async publishDelegatedShare({ walletId, shareSetId, delegatedShare, signedSessionId, dynamicRequestId, chainName, accountAddress, bitcoinConfig, isPasswordEncrypted }) {
6636
6788
  var _publicKey_key, _publicKey_key1, _publicKey_key2;
6637
6789
  const publicKey = await this.apiClient.getDelegatedEncryptionKey({
6638
6790
  environmentId: this.environmentId
@@ -6657,7 +6809,8 @@ class DynamicWalletClient {
6657
6809
  chainName,
6658
6810
  clientKeyShare: delegatedShare,
6659
6811
  bitcoinConfig,
6660
- walletId
6812
+ walletId,
6813
+ accountAddress
6661
6814
  });
6662
6815
  return {
6663
6816
  location: core.BackupLocation.DELEGATED,
@@ -6823,6 +6976,7 @@ class DynamicWalletClient {
6823
6976
  signedSessionId: resolvedSignedSessionId,
6824
6977
  dynamicRequestId,
6825
6978
  chainName: walletData.chainName,
6979
+ accountAddress,
6826
6980
  bitcoinConfig,
6827
6981
  isPasswordEncrypted
6828
6982
  }), {
@@ -6991,7 +7145,8 @@ class DynamicWalletClient {
6991
7145
  accountAddress,
6992
7146
  chainName,
6993
7147
  localShares,
6994
- recordedKeygenIds
7148
+ recordedKeygenIds,
7149
+ variant: walletData.variant
6995
7150
  });
6996
7151
  if (fresh) return;
6997
7152
  this.logger.warn('[storeEncryptedBackupByWallet] Stale local shares detected; refreshing local storage', {
@@ -8416,9 +8571,17 @@ class DynamicWalletClient {
8416
8571
  const user = await this.getUserWithEnvCheck(dynamicRequestId);
8417
8572
  const waasWallets = (_user_verifiedCredentials = user.verifiedCredentials) == null ? void 0 : _user_verifiedCredentials.filter((vc)=>vc.walletName === 'dynamicwaas');
8418
8573
  for (const vc of waasWallets != null ? waasWallets : []){
8574
+ var _props_settings, _this_getWalletFromMap;
8419
8575
  const addr = vc.address;
8420
8576
  const props = vc.walletProperties;
8421
- this.updateWalletMap(addr, {
8577
+ var _props_settings_variant, _ref;
8578
+ // Resolve the ed25519 protocol variant: server-surfaced (walletProperties
8579
+ // .settings or top-level) first, then any locally-seeded value (set at raw
8580
+ // import). Required on a fresh session (no local seed) so recover/sign
8581
+ // rebuild the non-exportable Ed25519 signer for raw-scalar wallets instead
8582
+ // of defaulting to exportable → "Invalid discriminant".
8583
+ const resolvedVariant = (_ref = (_props_settings_variant = props == null ? void 0 : (_props_settings = props.settings) == null ? void 0 : _props_settings.variant) != null ? _props_settings_variant : props == null ? void 0 : props.variant) != null ? _ref : (_this_getWalletFromMap = this.getWalletFromMap(addr)) == null ? void 0 : _this_getWalletFromMap.variant;
8584
+ this.updateWalletMap(addr, _extends({
8422
8585
  walletId: vc.id,
8423
8586
  chainName: core.verifiedCredentialNameToChainEnum[vc.chain],
8424
8587
  accountAddress: addr,
@@ -8431,7 +8594,9 @@ class DynamicWalletClient {
8431
8594
  shareSetId: props == null ? void 0 : props.shareSetId,
8432
8595
  shareSetType: props == null ? void 0 : props.shareSetType,
8433
8596
  otherShareSets: props == null ? void 0 : props.otherShareSets
8434
- });
8597
+ }, resolvedVariant ? {
8598
+ variant: resolvedVariant
8599
+ } : {}));
8435
8600
  }
8436
8601
  if (walletOperation !== core.WalletOperation.NO_OPERATION && await this.requiresRestoreBackupSharesForOperation({
8437
8602
  accountAddress,
@@ -8809,8 +8974,8 @@ class DynamicWalletClient {
8809
8974
  this.userId = user.id;
8810
8975
  const waasWallets = (_user_verifiedCredentials = user.verifiedCredentials) == null ? void 0 : _user_verifiedCredentials.filter((vc)=>vc.walletName === 'dynamicwaas');
8811
8976
  const wallets = waasWallets.map((vc)=>{
8812
- var _this_getWalletFromMap, _vc_walletProperties, _vc_walletProperties1, _vc_walletProperties2, _vc_walletProperties3, _vc_walletProperties4, _vc_walletProperties5, _vc_walletProperties6;
8813
- var _this_getWalletFromMap_derivationPath;
8977
+ var _this_getWalletFromMap, _vc_walletProperties, _vc_walletProperties1, _vc_walletProperties2, _vc_walletProperties3, _vc_walletProperties4, _vc_walletProperties5, _vc_walletProperties6, _vc_walletProperties_settings, _vc_walletProperties7, _vc_walletProperties8, _this_getWalletFromMap1;
8978
+ var _this_getWalletFromMap_derivationPath, _vc_walletProperties_settings_variant, _ref;
8814
8979
  return {
8815
8980
  walletId: vc.id,
8816
8981
  chainName: core.verifiedCredentialNameToChainEnum[vc.chain],
@@ -8828,14 +8993,18 @@ class DynamicWalletClient {
8828
8993
  // On-sign-on settings (shouldRefreshOnNextSignOn / reshareOnNextSignOn /
8829
8994
  // revokeOnNextSignOn). Surfaced so the SDK can self-drive the on-sign-on
8830
8995
  // orchestration instead of the host reading these flags.
8831
- settings: (_vc_walletProperties6 = vc.walletProperties) == null ? void 0 : _vc_walletProperties6.settings
8996
+ settings: (_vc_walletProperties6 = vc.walletProperties) == null ? void 0 : _vc_walletProperties6.settings,
8997
+ // Prefer the server-persisted variant (surfaced via walletProperties.settings),
8998
+ // then the locally-known value (set at raw import) so same-session
8999
+ // import → sign works before the server has surfaced it.
9000
+ variant: (_ref = (_vc_walletProperties_settings_variant = (_vc_walletProperties7 = vc.walletProperties) == null ? void 0 : (_vc_walletProperties_settings = _vc_walletProperties7.settings) == null ? void 0 : _vc_walletProperties_settings.variant) != null ? _vc_walletProperties_settings_variant : (_vc_walletProperties8 = vc.walletProperties) == null ? void 0 : _vc_walletProperties8.variant) != null ? _ref : (_this_getWalletFromMap1 = this.getWalletFromMap(vc.address)) == null ? void 0 : _this_getWalletFromMap1.variant
8832
9001
  };
8833
9002
  });
8834
9003
  const existingWalletMap = this.walletMap;
8835
9004
  this.walletMap = wallets.reduce((acc, wallet)=>{
8836
9005
  const normalizedAddress = normalizeAddress(wallet.accountAddress);
8837
9006
  const existingWallet = existingWalletMap[normalizedAddress];
8838
- acc[normalizedAddress] = {
9007
+ acc[normalizedAddress] = _extends({
8839
9008
  walletId: wallet.walletId,
8840
9009
  chainName: wallet.chainName,
8841
9010
  accountAddress: wallet.accountAddress,
@@ -8848,7 +9017,9 @@ class DynamicWalletClient {
8848
9017
  shareSetType: wallet.shareSetType,
8849
9018
  businessAccountId: wallet.businessAccountId,
8850
9019
  otherShareSets: wallet.otherShareSets
8851
- };
9020
+ }, wallet.variant ? {
9021
+ variant: wallet.variant
9022
+ } : {});
8852
9023
  return acc;
8853
9024
  }, {});
8854
9025
  this.logger.info('[walletMap] getWallets: walletMap updated from API', {
@@ -9302,6 +9473,46 @@ DynamicWalletClient.roomsPersistDirty = false;
9302
9473
  // rooms back into the new user's storage (TOCTOU on the create→merge gap).
9303
9474
  DynamicWalletClient.roomsGeneration = 0;
9304
9475
 
9476
+ /**
9477
+ * Derive the 32-byte ed25519 public key from a **raw** 32-byte signing scalar
9478
+ * (`A = s·B`).
9479
+ *
9480
+ * This is NOT the same as deriving a public key from an RFC-8032 seed: the
9481
+ * standard ed25519 key-generation step takes a 32-byte seed and SHA-512-expands
9482
+ * it (then clamps) to obtain the scalar. A scalar exported from an external MPC
9483
+ * system (e.g. Fireblocks Embedded Wallets) has no preimage seed, so it must be
9484
+ * used directly as the scalar. Using `Keypair.fromSecretKey` / `keyPairFromSeed`
9485
+ * on such bytes would re-expand them and produce the wrong public key/address.
9486
+ *
9487
+ * @param scalar Raw 32-byte ed25519 scalar, little-endian, as hex (with or
9488
+ * without a `0x` prefix) or as raw bytes.
9489
+ * @returns The compressed 32-byte ed25519 public key.
9490
+ */ const getEd25519PublicKeyFromRawScalar = (scalar)=>{
9491
+ let bytes;
9492
+ if (typeof scalar === 'string') {
9493
+ const hex = scalar.replace(/^0x/, '');
9494
+ // Validate before hexToBytes so malformed input fails with a clear, own
9495
+ // error rather than an opaque/implementation-specific one from the decoder.
9496
+ if (!/^[0-9a-fA-F]+$/.test(hex)) {
9497
+ throw new Error('Invalid raw ed25519 scalar: expected a hex string');
9498
+ }
9499
+ bytes = utils.hexToBytes(hex);
9500
+ } else {
9501
+ bytes = scalar;
9502
+ }
9503
+ if (bytes.length !== 32) {
9504
+ throw new Error(`Invalid raw ed25519 scalar length: ${bytes.length}, expected 32`);
9505
+ }
9506
+ // Interpret the scalar little-endian and reduce mod the curve order. We do NOT
9507
+ // clamp here: the caller provides the actual signing scalar `s`, and the public
9508
+ // key is exactly `s·B`.
9509
+ const reduced = utils.bytesToNumberLE(bytes) % ed25519.ed25519.CURVE.n;
9510
+ if (reduced === 0n) {
9511
+ throw new Error('Invalid raw ed25519 scalar: reduces to zero mod the curve order');
9512
+ }
9513
+ return ed25519.ed25519.ExtendedPoint.BASE.multiply(reduced).toRawBytes();
9514
+ };
9515
+
9305
9516
  Object.defineProperty(exports, "Logger", {
9306
9517
  enumerable: true,
9307
9518
  get: function () { return core.Logger; }
@@ -9416,6 +9627,7 @@ exports.getBitcoinAddressTypeFromDerivationPath = getBitcoinAddressTypeFromDeriv
9416
9627
  exports.getClientKeyShareBackupInfo = getClientKeyShareBackupInfo;
9417
9628
  exports.getClientKeyShareExportFileName = getClientKeyShareExportFileName;
9418
9629
  exports.getDelegatedShareSet = getDelegatedShareSet;
9630
+ exports.getEd25519PublicKeyFromRawScalar = getEd25519PublicKeyFromRawScalar;
9419
9631
  exports.getGoogleOAuthAccountId = getGoogleOAuthAccountId;
9420
9632
  exports.getHttpStatus = getHttpStatus;
9421
9633
  exports.getICloudBackup = getICloudBackup;
@@ -9429,6 +9641,7 @@ exports.isBrowser = isBrowser;
9429
9641
  exports.isHeavyQueueOperation = isHeavyQueueOperation;
9430
9642
  exports.isHexString = isHexString;
9431
9643
  exports.isICloudAuthenticated = isICloudAuthenticated;
9644
+ exports.isMultipleWalletsPerChainRejection = isMultipleWalletsPerChainRejection;
9432
9645
  exports.isNonRetryableCeremonyError = isNonRetryableCeremonyError;
9433
9646
  exports.isPublicKeyMismatchError = isPublicKeyMismatchError;
9434
9647
  exports.isRecoverQueueOperation = isRecoverQueueOperation;