Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/lucky-pens-send.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@turnkey/viem": minor
---

Replaced concrete client types with a generic method-based interface to avoid type errors and make client types version-agnostic.
1 change: 1 addition & 0 deletions packages/viem/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"@turnkey/sdk-browser": "workspace:*",
"@turnkey/sdk-server": "workspace:*",
"@turnkey/core": "workspace:*",
"@turnkey/sdk-types": "workspace:*",
"cross-fetch": "^4.0.0"
},
"devDependencies": {
Expand Down
153 changes: 95 additions & 58 deletions packages/viem/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,86 @@ import {
TurnkeyActivityConsensusNeededError as TurnkeyHttpActivityConsensusNeededError,
TurnkeyClient,
type TurnkeyApiTypes,
type TurnkeyApi as HttpApiTypes,
} from "@turnkey/http";
import { ApiKeyStamper } from "@turnkey/api-key-stamper";
import type { TurnkeyBrowserClient } from "@turnkey/sdk-browser";
import type { TurnkeySDKClientBase } from "@turnkey/core";
import type { TurnkeyServerClient } from "@turnkey/sdk-server";
import type { TurnkeySDKApiTypes as BrowserApiTypes } from "@turnkey/sdk-browser";
import type { TurnkeySDKApiTypes as ServerApiTypes } from "@turnkey/sdk-server";
import type {
TGetPrivateKeyBody as SdkTypesGetPrivateKeyBody,
TSignTransactionBody as SdkTypesSignTransactionBody,
TSignRawPayloadBody as SdkTypesSignRawPayloadBody,
} from "@turnkey/sdk-types";

/**
* Union types for inputs — accept the body format from any SDK package.
*/
export type TGetPrivateKeyBody =
| BrowserApiTypes.TGetPrivateKeyBody
| ServerApiTypes.TGetPrivateKeyBody
| SdkTypesGetPrivateKeyBody
| HttpApiTypes.TGetPrivateKeyBody;

export type TSignTransactionBody =
| BrowserApiTypes.TSignTransactionBody
| ServerApiTypes.TSignTransactionBody
| SdkTypesSignTransactionBody
| HttpApiTypes.TSignTransactionBody;

export type TSignRawPayloadBody =
| BrowserApiTypes.TSignRawPayloadBody
| ServerApiTypes.TSignRawPayloadBody
| SdkTypesSignRawPayloadBody
| HttpApiTypes.TSignRawPayloadBody;

/**
* Minimal response types — only the fields the viem package actually reads.
* This makes responses version-agnostic; any SDK version will satisfy these.
*/
type TGetPrivateKeyResponse = {
privateKey: {
addresses: Array<{ format?: string; address?: string }>;
};
};

type TSignTransactionResponse = {
activity: {
id: string;
status: string;
result: {
signTransactionResult?: {
signedTransaction?: string;
};
};
};
};

type TSignRawPayloadResponse = {
activity: {
id: string;
status: string;
result: {
signRawPayloadResult?: {
r?: string;
s?: string;
v?: string;
};
};
};
};

/**
* Generic client interface for any Turnkey client (HTTP, browser SDK, server SDK, core, etc.).
*/
export interface TurnkeyClientInterface {
getPrivateKey(input: TGetPrivateKeyBody): Promise<TGetPrivateKeyResponse>;
signTransaction(
input: TSignTransactionBody,
): Promise<TSignTransactionResponse>;
signRawPayload(input: TSignRawPayloadBody): Promise<TSignRawPayloadResponse>;
}

export type TTurnkeyClient = TurnkeyClientInterface;

export type TTurnkeyConsensusNeededErrorType = TurnkeyConsensusNeededError & {
name: "TurnkeyConsensusNeededError";
Expand Down Expand Up @@ -124,11 +199,7 @@ export class TurnkeyActivityError extends BaseError {
}

export function createAccountWithAddress(input: {
client:
| TurnkeyClient
| TurnkeyBrowserClient
| TurnkeyServerClient
| TurnkeySDKClientBase;
client: TTurnkeyClient;
organizationId: string;
// This can be a wallet account address, private key address, or private key ID.
signWith: string;
Expand Down Expand Up @@ -202,11 +273,7 @@ export function createAccountWithAddress(input: {
}

export async function createAccount(input: {
client:
| TurnkeyClient
| TurnkeyBrowserClient
| TurnkeyServerClient
| TurnkeySDKClientBase;
client: TTurnkeyClient;
organizationId: string;
// This can be a wallet account address, private key address, or private key ID.
signWith: string;
Expand Down Expand Up @@ -371,11 +438,7 @@ export async function createApiKeyAccount(
}

export async function signAuthorization(
client:
| TurnkeyClient
| TurnkeyBrowserClient
| TurnkeyServerClient
| TurnkeySDKClientBase,
client: TTurnkeyClient,
parameters: TSignAuthorizationParameters,
organizationId: string,
signWith: string,
Expand Down Expand Up @@ -417,11 +480,7 @@ export async function signAuthorization(
}

export async function signMessage(
client:
| TurnkeyClient
| TurnkeyBrowserClient
| TurnkeyServerClient
| TurnkeySDKClientBase,
client: TTurnkeyClient,
message: SignableMessage,
organizationId: string,
signWith: string,
Expand All @@ -438,11 +497,7 @@ export async function signMessage(
export async function signTransaction<
TTransactionSerializable extends TransactionSerializable,
>(
client:
| TurnkeyClient
| TurnkeyBrowserClient
| TurnkeyServerClient
| TurnkeySDKClientBase,
client: TTurnkeyClient,
transaction: TTransactionSerializable,
serializer: SerializeTransactionFn<TTransactionSerializable>,
organizationId: string,
Expand Down Expand Up @@ -485,11 +540,7 @@ export async function signTransaction<
}

export async function signTypedData(
client:
| TurnkeyClient
| TurnkeyBrowserClient
| TurnkeyServerClient
| TurnkeySDKClientBase,
client: TTurnkeyClient,
data: TypedData | { [key: string]: unknown },
organizationId: string,
signWith: string,
Expand All @@ -505,11 +556,7 @@ export async function signTypedData(
}

async function signTransactionWithErrorWrapping(
client:
| TurnkeyClient
| TurnkeyBrowserClient
| TurnkeyServerClient
| TurnkeySDKClientBase,
client: TTurnkeyClient,
unsignedTransaction: string,
organizationId: string,
signWith: string,
Expand Down Expand Up @@ -551,11 +598,7 @@ async function signTransactionWithErrorWrapping(
}

async function signTransactionImpl(
client:
| TurnkeyClient
| TurnkeyBrowserClient
| TurnkeyServerClient
| TurnkeySDKClientBase,
client: TTurnkeyClient,
unsignedTransaction: string,
organizationId: string,
signWith: string,
Expand All @@ -579,7 +622,7 @@ async function signTransactionImpl(
activity?.result?.signTransactionResult?.signedTransaction,
);
} else {
const { activity, signedTransaction } = await client.signTransaction({
const { activity } = await client.signTransaction({
organizationId,
signWith,
type: transactionType ?? "TRANSACTION_TYPE_ETHEREUM",
Expand All @@ -590,16 +633,14 @@ async function signTransactionImpl(
activity as any /* Type casting is ok here. The invalid types are both actually strings. TS is too strict here! */,
);

const signedTransaction =
activity.result.signTransactionResult?.signedTransaction;
return assertNonNull(signedTransaction);
}
}

async function signMessageWithErrorWrapping(
client:
| TurnkeyClient
| TurnkeyBrowserClient
| TurnkeyServerClient
| TurnkeySDKClientBase,
client: TTurnkeyClient,
message: string,
organizationId: string,
signWith: string,
Expand Down Expand Up @@ -644,11 +685,7 @@ async function signMessageWithErrorWrapping(
}

async function signMessageImpl(
client:
| TurnkeyClient
| TurnkeyBrowserClient
| TurnkeyServerClient
| TurnkeySDKClientBase,
client: TTurnkeyClient,
message: string,
organizationId: string,
signWith: string,
Expand All @@ -674,7 +711,7 @@ async function signMessageImpl(

result = assertNonNull(activity?.result?.signRawPayloadResult);
} else {
Copy link
Contributor

@moeodeh3 moeodeh3 Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’m wondering why we’re still conditionally rendering based on isHttpClient(). Will that even work? I don’t think so 🤔

with your changes, isHttpClient() will now always return false, which means we’ll always hit the same code path. That makes this conditional statment dead

but the bigger issue is how we’re constructing and sending the request. The reason we differentiate httpClient from other clients isn’t because the response shape is different, it’s because the intent format is different

for httpClient, the intent is structured like this:

{
  type: "ACTIVITY_TYPE_SIGN_TRANSACTION_V2",
  organizationId,
  parameters: {
    signWith,
    type: transactionType ?? "TRANSACTION_TYPE_ETHEREUM",
    unsignedTransaction,
  },
  timestampMs: String(Date.now()),
}

whereas for other clients, the intent is structured like this:

{
  organizationId,
  signWith,
  type: transactionType ?? "TRANSACTION_TYPE_ETHEREUM",
  unsignedTransaction,
}

so we would POST the request to the Turnkey without the required type: "ACTIVITY_TYPE_...", ``timestampMs, and paramaters` wrapper

which would fail :(

const { activity, r, s, v } = await client.signRawPayload({
const { activity } = await client.signRawPayload({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same thing goes here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tested and we chillin, isHttpClient() codepath works correctly still

organizationId,
signWith,
payload: message,
Expand All @@ -687,9 +724,9 @@ async function signMessageImpl(
);

result = {
r,
s,
v,
r: assertNonNull(activity?.result?.signRawPayloadResult?.r),
s: assertNonNull(activity?.result?.signRawPayloadResult?.s),
v: assertNonNull(activity?.result?.signRawPayloadResult?.v),
};
}

Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading