@account-kit/signer 4.43.0 → 4.44.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.
@@ -2,7 +2,14 @@ import { ConnectionConfigSchema, type ConnectionConfig } from "@aa-sdk/core";
2
2
  import { TurnkeyClient, type TSignedRequest } from "@turnkey/http";
3
3
  import EventEmitter from "eventemitter3";
4
4
  import { jwtDecode } from "jwt-decode";
5
- import { sha256, type Hex } from "viem";
5
+ import {
6
+ hexToBytes,
7
+ recoverPublicKey,
8
+ serializeSignature,
9
+ sha256,
10
+ type Address,
11
+ type Hex,
12
+ } from "viem";
6
13
  import { NotAuthenticatedError, OAuthProvidersError } from "../errors.js";
7
14
  import { getDefaultProviderCustomization } from "../oauth.js";
8
15
  import type { OauthMode } from "../signer.js";
@@ -42,6 +49,8 @@ import type {
42
49
  AuthMethods,
43
50
  } from "./types.js";
44
51
  import { VERSION } from "../version.js";
52
+ import { secp256k1 } from "@noble/curves/secp256k1";
53
+ import { Point } from "@noble/secp256k1";
45
54
 
46
55
  export interface BaseSignerClientParams {
47
56
  stamper: TurnkeyClient["stamper"];
@@ -51,8 +60,8 @@ export interface BaseSignerClientParams {
51
60
  }
52
61
 
53
62
  export type ExportWalletStamper = TurnkeyClient["stamper"] & {
54
- injectWalletExportBundle(bundle: string): Promise<boolean>;
55
- injectKeyExportBundle(bundle: string): Promise<boolean>;
63
+ injectWalletExportBundle(bundle: string, orgId: string): Promise<boolean>;
64
+ injectKeyExportBundle(bundle: string, orgId: string): Promise<boolean>;
56
65
  publicKey(): string | null;
57
66
  };
58
67
 
@@ -64,6 +73,8 @@ const MFA_PAYLOAD = {
64
73
  LIST: "list_mfas",
65
74
  } as const;
66
75
 
76
+ const withHexPrefix = (hex: string) => `0x${hex}` as const;
77
+
67
78
  /**
68
79
  * Base class for all Alchemy Signer clients
69
80
  */
@@ -679,6 +690,160 @@ export abstract class BaseSignerClient<TExportWalletParams = unknown> {
679
690
  return signature;
680
691
  };
681
692
 
693
+ private experimental_createMultiOwnerStamper = () => ({
694
+ stamp: async (
695
+ request: string,
696
+ ): Promise<{
697
+ stampHeaderName: string;
698
+ stampHeaderValue: string;
699
+ }> => {
700
+ if (!this.user) {
701
+ throw new NotAuthenticatedError();
702
+ }
703
+
704
+ // we need this later to recover the public key from the signature, so we don't let turnkey hash
705
+ // this for us and pass HASH_FUNCTION_NO_OP instead
706
+ const hashed = sha256(new TextEncoder().encode(request));
707
+
708
+ // sign through the user's suborg
709
+ const signature = await this.signRawMessage(hashed, "ETHEREUM");
710
+
711
+ // recover the public key, we can't just use the address
712
+ const recoveredPublicKey = await recoverPublicKey({
713
+ hash: hashed,
714
+ signature,
715
+ });
716
+
717
+ // compute the stamp over the original payload using this signature
718
+ // the format here is important
719
+ const stamp = {
720
+ publicKey: Point.fromHex(hexToBytes(recoveredPublicKey)).toHex(true),
721
+ scheme: "SIGNATURE_SCHEME_TK_API_SECP256K1",
722
+ signature: secp256k1.Signature.fromCompact(
723
+ hexToBytes(signature).slice(0, 64),
724
+ ).toDERHex(),
725
+ };
726
+
727
+ return {
728
+ stampHeaderName: "X-Stamp",
729
+ stampHeaderValue: base64UrlEncode(Buffer.from(JSON.stringify(stamp))),
730
+ };
731
+ },
732
+ });
733
+
734
+ private experimental_createMultiOwnerTurnkeyClient = () =>
735
+ new TurnkeyClient(
736
+ { baseUrl: "https://api.turnkey.com" },
737
+ this.experimental_createMultiOwnerStamper(),
738
+ );
739
+
740
+ /**
741
+ * This will sign on behalf of the multi-owner org, without doing any transformations on the message.
742
+ * For SignMessage or SignTypedData, the caller should hash the message before calling this method and pass
743
+ * that result here.
744
+ *
745
+ * @param {Hex} msg the hex representation of the bytes to sign
746
+ * @param {string} orgId orgId of the multi-owner org to sign on behalf of
747
+ * @param {string} orgAddress address of the multi-owner org to sign on behalf of
748
+ * @returns {Promise<Hex>} the signature over the raw hex
749
+ */
750
+ public experimental_multiOwnerSignRawMessage = async (
751
+ msg: Hex,
752
+ orgId: string,
753
+ orgAddress: string,
754
+ ) => {
755
+ if (!this.user) {
756
+ throw new NotAuthenticatedError();
757
+ }
758
+ const multiOwnerClient = this.experimental_createMultiOwnerTurnkeyClient();
759
+
760
+ const signatureResult = await multiOwnerClient.signRawPayload({
761
+ organizationId: orgId,
762
+ type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
763
+ timestampMs: Date.now().toString(),
764
+ parameters: {
765
+ encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
766
+ hashFunction: "HASH_FUNCTION_NO_OP",
767
+ payload: msg,
768
+ signWith: orgAddress,
769
+ },
770
+ });
771
+
772
+ const signRawPayloadResult =
773
+ signatureResult.activity.result.signRawPayloadResult;
774
+ if (!signRawPayloadResult) {
775
+ throw new Error("No sign raw payload result");
776
+ }
777
+
778
+ return serializeSignature({
779
+ r: withHexPrefix(signRawPayloadResult.r),
780
+ s: withHexPrefix(signRawPayloadResult.s),
781
+ yParity: Number(signRawPayloadResult.v), // this is not actually a legacy v value, it's the y parity bit
782
+ });
783
+ };
784
+
785
+ /**
786
+ * This will create a multi-sig with the current user and additional specified signers
787
+ *
788
+ * @param {number} quorum multi sig quorum, currently only 1 is supported
789
+ * @param {Address[]} additionalMembers members to add, aside from the currently authenticated user
790
+ * @returns {Promise<SignerResponse<"/v1/multi-sig-create">>} created multi-sig
791
+ */
792
+ public experimental_createMultiSig = (
793
+ quorum: number,
794
+ additionalMembers: Address[],
795
+ ) => {
796
+ if (!this.user) {
797
+ throw new NotAuthenticatedError();
798
+ }
799
+
800
+ return this.request("/v1/multi-sig-create", {
801
+ members: [this.user.address, ...additionalMembers].map(
802
+ (evmSignerAddress) => ({ evmSignerAddress }),
803
+ ),
804
+ quorum,
805
+ });
806
+ };
807
+
808
+ /**
809
+ * This will add additional members to an existing multi-sig account
810
+ *
811
+ * @param {string} orgId orgId of the multi-sig to add members to
812
+ * @param {Address[]} members the addresses of the members to add
813
+ */
814
+ public experimental_addToMultiOwner = async (
815
+ orgId: string,
816
+ members: Address[],
817
+ ) => {
818
+ if (!this.user) {
819
+ throw new NotAuthenticatedError();
820
+ }
821
+
822
+ const multiOwnerClient = this.experimental_createMultiOwnerTurnkeyClient();
823
+
824
+ const prepared = await this.request("/v1/multi-sig-prepare-add", {
825
+ members: members.map((evmSignerAddress) => ({ evmSignerAddress })),
826
+ });
827
+
828
+ const stampedRequest = await multiOwnerClient.stampCreateUsers({
829
+ organizationId: orgId,
830
+ type: "ACTIVITY_TYPE_CREATE_USERS_V3",
831
+ timestampMs: Date.now().toString(),
832
+ parameters: prepared,
833
+ });
834
+
835
+ const { updateRootQuorumIntent } = await this.request("/v1/multi-sig-add", {
836
+ stampedRequest,
837
+ });
838
+
839
+ await multiOwnerClient.updateRootQuorum({
840
+ organizationId: orgId,
841
+ type: "ACTIVITY_TYPE_UPDATE_ROOT_QUORUM",
842
+ timestampMs: Date.now().toString(),
843
+ parameters: updateRootQuorumIntent,
844
+ });
845
+ };
846
+
682
847
  /**
683
848
  * Returns the current user or null if no user is set.
684
849
  *
@@ -936,7 +1101,10 @@ export abstract class BaseSignerClient<TExportWalletParams = unknown> {
936
1101
  "exportWalletResult",
937
1102
  );
938
1103
 
939
- const result = await stamper.injectWalletExportBundle(exportBundle);
1104
+ const result = await stamper.injectWalletExportBundle(
1105
+ exportBundle,
1106
+ this.user.orgId,
1107
+ );
940
1108
 
941
1109
  if (!result) {
942
1110
  throw new Error("Failed to inject wallet export bundle");
@@ -966,7 +1134,10 @@ export abstract class BaseSignerClient<TExportWalletParams = unknown> {
966
1134
  "exportWalletAccountResult",
967
1135
  );
968
1136
 
969
- const result = await stamper.injectKeyExportBundle(exportBundle);
1137
+ const result = await stamper.injectKeyExportBundle(
1138
+ exportBundle,
1139
+ this.user.orgId,
1140
+ );
970
1141
 
971
1142
  if (!result) {
972
1143
  throw new Error("Failed to inject wallet export bundle");
@@ -55,7 +55,10 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
55
55
  private iframeStamper: IframeStamper;
56
56
  private webauthnStamper: WebauthnStamper;
57
57
  oauthCallbackUrl: string;
58
- iframeContainerId: string;
58
+ iframeConfig: {
59
+ iframeElementId: string;
60
+ iframeContainerId: string;
61
+ };
59
62
 
60
63
  /**
61
64
  * Initializes a new instance with the given parameters, setting up the connection, iframe configuration, and WebAuthn stamper.
@@ -97,7 +100,7 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
97
100
  });
98
101
 
99
102
  this.iframeStamper = iframeStamper;
100
- this.iframeContainerId = iframeConfig.iframeContainerId;
103
+ this.iframeConfig = iframeConfig;
101
104
 
102
105
  this.webauthnStamper = new WebauthnStamper({
103
106
  rpId: rpId ?? window.location.hostname,
@@ -350,7 +353,19 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
350
353
  public override disconnect = async () => {
351
354
  this.user = undefined;
352
355
  this.iframeStamper.clear();
353
- await this.iframeStamper.init();
356
+
357
+ // In the latest version of the TK iframe stamper, the
358
+ // IframeStamper instance seems to not be usable after
359
+ // clearing it, so we need to create a new instance.
360
+ const stamper = new IframeStamper({
361
+ iframeContainer: document.getElementById(
362
+ this.iframeConfig.iframeContainerId,
363
+ ),
364
+ iframeElementId: this.iframeConfig.iframeElementId,
365
+ iframeUrl: "https://auth.turnkey.com",
366
+ });
367
+ this.iframeStamper = stamper;
368
+ await this.initSessionStamper();
354
369
  };
355
370
 
356
371
  /**
@@ -610,14 +625,29 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
610
625
  return this.request("/v1/prepare-oauth", { nonce });
611
626
  };
612
627
 
628
+ private initSessionStamperPromise: Promise<string> | null = null;
629
+
613
630
  protected override async initSessionStamper(): Promise<string> {
614
- if (!this.iframeStamper.publicKey()) {
615
- await this.iframeStamper.init();
631
+ if (this.initSessionStamperPromise) {
632
+ return this.initSessionStamperPromise;
616
633
  }
617
634
 
618
- this.setStamper(this.iframeStamper);
635
+ this.initSessionStamperPromise = (async () => {
636
+ if (!this.iframeStamper.publicKey()) {
637
+ await this.iframeStamper.init();
638
+ }
639
+
640
+ this.setStamper(this.iframeStamper);
641
+
642
+ return this.iframeStamper.publicKey()!;
643
+ })();
619
644
 
620
- return this.iframeStamper.publicKey()!;
645
+ try {
646
+ const result = await this.initSessionStamperPromise;
647
+ return result;
648
+ } finally {
649
+ this.initSessionStamperPromise = null;
650
+ }
621
651
  }
622
652
 
623
653
  protected override async initWebauthnStamper(
@@ -1,5 +1,9 @@
1
1
  import type { Address } from "@aa-sdk/core";
2
- import type { TSignedRequest, getWebAuthnAttestation } from "@turnkey/http";
2
+ import type {
3
+ TSignedRequest,
4
+ TurnkeyApiTypes,
5
+ getWebAuthnAttestation,
6
+ } from "@turnkey/http";
3
7
  import type { Hex } from "viem";
4
8
  import type { AuthParams } from "../signer";
5
9
 
@@ -264,6 +268,44 @@ export type SignerEndpoints = [
264
268
  multiFactors: MfaFactor[];
265
269
  };
266
270
  },
271
+ {
272
+ Route: "/v1/multi-sig-create";
273
+ Body: {
274
+ quorum: number;
275
+ members: {
276
+ evmSignerAddress: Address;
277
+ }[];
278
+ };
279
+ Response: {
280
+ orgId: string;
281
+ quorum: number;
282
+ evmSignerAddress: Address;
283
+ members: {
284
+ evmSignerAddress: Address;
285
+ }[];
286
+ };
287
+ },
288
+ {
289
+ Route: "/v1/multi-sig-prepare-add";
290
+ Body: {
291
+ members: {
292
+ evmSignerAddress: Address;
293
+ }[];
294
+ };
295
+ Response: TurnkeyApiTypes["v1CreateUsersIntentV3"];
296
+ },
297
+ {
298
+ Route: "/v1/multi-sig-add";
299
+ Body: {
300
+ stampedRequest: TSignedRequest;
301
+ };
302
+ Response: {
303
+ members: {
304
+ evmSignerAddress: Address;
305
+ }[];
306
+ updateRootQuorumIntent: TurnkeyApiTypes["v1UpdateRootQuorumIntent"];
307
+ };
308
+ },
267
309
  ];
268
310
 
269
311
  export type AuthenticatingEventMetadata = {
package/src/version.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  // This file is autogenerated by inject-version.ts. Any changes will be
2
2
  // overwritten on commit!
3
- export const VERSION = "4.43.0";
3
+ export const VERSION = "4.44.0";