Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type TypedData, type TypedDataDefinition, hashTypedData } from "viem";
import * as ox__TypedData from "ox/TypedData";
import type { ThirdwebContract } from "../../contract/contract.js";
import { isHex } from "../../utils/encoding/hex.js";
import { isValidSignature } from "./__generated__/isValidSignature/read/isValidSignature.js";
Expand All @@ -7,11 +7,11 @@ import { isValidSignature } from "./__generated__/isValidSignature/read/isValidS
* @extension ERC1271
*/
export type CheckContractWalletSignTypedDataOptions<
typedData extends TypedData | Record<string, unknown>,
typedData extends ox__TypedData.TypedData | Record<string, unknown>,
primaryType extends keyof typedData | "EIP712Domain" = keyof typedData,
> = {
contract: ThirdwebContract;
data: TypedDataDefinition<typedData, primaryType>;
data: ox__TypedData.Definition<typedData, primaryType>;
signature: string;
};
const MAGIC_VALUE = "0x1626ba7e";
Expand Down Expand Up @@ -42,15 +42,19 @@ const MAGIC_VALUE = "0x1626ba7e";
* @returns A promise that resolves with a boolean indicating if the signature is valid.
*/
export async function checkContractWalletSignedTypedData<
typedData extends TypedData | Record<string, unknown>,
typedData extends ox__TypedData.TypedData | Record<string, unknown>,
primaryType extends keyof typedData | "EIP712Domain" = keyof typedData,
>(options: CheckContractWalletSignTypedDataOptions<typedData, primaryType>) {
if (!isHex(options.signature)) {
throw new Error("The signature must be a valid hex string.");
}
const result = await isValidSignature({
contract: options.contract,
hash: hashTypedData(options.data),
hash: ox__TypedData.hashStruct({
primaryType: options.data.primaryType,
data: options.data.message as Record<string, unknown>,
types: options.data.types as ox__TypedData.Definition["types"],
}),
signature: options.signature,
});
return result === MAGIC_VALUE;
Expand Down
26 changes: 15 additions & 11 deletions packages/thirdweb/src/utils/signatures/helpers/parseTypedData.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
import type { TypedData } from "abitype";
import type { TypedDataDefinition } from "viem";
import { type Hex, hexToNumber, isHex } from "../../encoding/hex.js";
import * as ox__Hex from "ox/Hex";
import type * as ox__TypedData from "ox/TypedData";
import type { Hex } from "../../encoding/hex.js";

type UnknownDomain = unknown & { chainId?: unknown }; // TODO: create our own typed data types so this is cleaner
type HexDomain = unknown & { chainId: Hex }; // TODO: create our own typed data types so this is cleaner
type UnknownDomain = unknown & { chainId?: unknown };
type HexDomain = unknown & { chainId: Hex };

/**
* @internal
*/
export function parseTypedData<
typedData extends TypedData | Record<string, unknown> = TypedData,
typedData extends
| ox__TypedData.TypedData
| Record<string, unknown> = ox__TypedData.TypedData,
primaryType extends keyof typedData | "EIP712Domain" = keyof typedData,
>(
typedData: TypedDataDefinition<typedData, primaryType>,
): TypedDataDefinition<typedData, primaryType> {
typedData: ox__TypedData.Definition<typedData, primaryType>,
): ox__TypedData.Definition<typedData, primaryType> {
const domain = typedData.domain as UnknownDomain;
if (domain?.chainId !== undefined && isHex(domain.chainId)) {
if (domain?.chainId !== undefined && ox__Hex.validate(domain.chainId)) {
typedData.domain = {
...(typedData.domain as HexDomain),
chainId: hexToNumber((typedData.domain as unknown as HexDomain).chainId),
} as unknown as TypedDataDefinition<typedData, primaryType>["domain"];
chainId: ox__Hex.toNumber(
(typedData.domain as unknown as HexDomain).chainId,
),
} as unknown as ox__TypedData.Definition<typedData, primaryType>["domain"];
}
return typedData;
}
21 changes: 15 additions & 6 deletions packages/thirdweb/src/utils/signatures/sign-message.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import * as ox__Hex from "ox/Hex";
import * as ox__PersonalMessage from "ox/PersonalMessage";
import * as ox__Secp256k1 from "ox/Secp256k1";
import * as ox__Signature from "ox/Signature";
import type { Account } from "../../wallets/interfaces/wallet.js";
import type { Hex } from "../encoding/hex.js";
import { hashMessage } from "../hashing/hashMessage.js";
import type { Prettify } from "../type-utils.js";
import { sign } from "./sign.js";
import { signatureToHex } from "./signature-to-hex.js";

type Message = Prettify<
| string
Expand Down Expand Up @@ -59,9 +60,17 @@ export function signMessage(
options: SignMessageOptions | { message: Message; account: Account },
): Hex | Promise<Hex> {
if ("privateKey" in options) {
const { message, privateKey } = options;
const signature = sign({ hash: hashMessage(message), privateKey });
return signatureToHex(signature);
const payload = ox__PersonalMessage.getSignPayload(
typeof options.message === "object"
? options.message.raw
: ox__Hex.fromString(options.message),
);

const signature = ox__Secp256k1.sign({
payload,
privateKey: options.privateKey,
});
return ox__Signature.toHex(signature);
}
if ("account" in options) {
const { message, account } = options;
Expand Down
25 changes: 13 additions & 12 deletions packages/thirdweb/src/utils/signatures/sign-typed-data.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import type { TypedData } from "abitype";
import { type TypedDataDefinition, hashTypedData } from "viem";
import * as ox__Secp256k1 from "ox/Secp256k1";
import * as ox__Signature from "ox/Signature";
import * as ox__TypedData from "ox/TypedData";
import type { Hex } from "../encoding/hex.js";
import { parseTypedData } from "./helpers/parseTypedData.js";
import { sign } from "./sign.js";
import { signatureToHex } from "./signature-to-hex.js";

export type SignTypedDataOptions<
typedData extends TypedData | Record<string, unknown> = TypedData,
typedData extends
| ox__TypedData.TypedData
| Record<string, unknown> = ox__TypedData.TypedData,
primaryType extends keyof typedData | "EIP712Domain" = keyof typedData,
> = TypedDataDefinition<typedData, primaryType> & {
> = ox__TypedData.Definition<typedData, primaryType> & {
privateKey: Hex;
};

Expand All @@ -28,17 +28,18 @@ export type SignTypedDataOptions<
* @utils
*/
export function signTypedData<
const typedData extends TypedData | Record<string, unknown>,
const typedData extends ox__TypedData.TypedData | Record<string, unknown>,
primaryType extends keyof typedData | "EIP712Domain",
>(options: SignTypedDataOptions<typedData, primaryType>): Hex {
const { privateKey, ...typedData } =
options as unknown as SignTypedDataOptions;

const parsedTypeData = parseTypedData(typedData);
const payload = ox__TypedData.getSignPayload(typedData);

const signature = sign({
hash: hashTypedData(parsedTypeData), // TODO: Implement native hashTypedData
const signature = ox__Secp256k1.sign({
payload,
privateKey,
});
return signatureToHex(signature);

return ox__Signature.toHex(signature);
}
11 changes: 5 additions & 6 deletions packages/thirdweb/src/utils/signatures/sign.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { secp256k1 } from "@noble/curves/secp256k1";
import type { Signature } from "viem";
import * as ox__Secp256k1 from "ox/Secp256k1";

import { type Hex, toHex } from "../encoding/hex.js";

Expand Down Expand Up @@ -28,12 +27,12 @@ export type SignOptions = {
* ```
* @utils
*/
export function sign({ hash, privateKey }: SignOptions): Signature {
const { r, s, recovery } = secp256k1.sign(hash.slice(2), privateKey.slice(2));
export function sign({ hash, privateKey }: SignOptions) {
const { r, s, yParity } = ox__Secp256k1.sign({ payload: hash, privateKey });
return {
r: toHex(r, { size: 32 }),
s: toHex(s, { size: 32 }),
v: recovery ? 28n : 27n,
yParity: recovery,
v: yParity === 1 ? 28n : 27n,
yParity,
};
}
10 changes: 8 additions & 2 deletions packages/thirdweb/src/utils/signatures/signature-to-hex.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { secp256k1 } from "@noble/curves/secp256k1";
import type { Signature } from "viem";
import { type Hex, hexToBigInt } from "../encoding/hex.js";

// We can't migrate this to Ox without breaking changes since Ox does not support pre-EIP1559 signatures

/**
* Converts a signature to a hex string.
* @param signature The signature to convert.
Expand All @@ -25,7 +26,12 @@ import { type Hex, hexToBigInt } from "../encoding/hex.js";
* ```
* @utils
*/
export function signatureToHex(signature: Signature): Hex {
export function signatureToHex(signature: {
r: Hex;
s: Hex;
v?: bigint;
yParity?: number;
}): Hex {
const { r, s, v, yParity } = signature;
const yParity_ = (() => {
if (yParity === 0 || yParity === 1) return yParity;
Expand Down
28 changes: 12 additions & 16 deletions packages/thirdweb/src/wallets/coinbase/coinbaseWebSDK.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import type { ProviderInterface } from "@coinbase/wallet-sdk";
import type { Address } from "abitype";
import {
type SignTypedDataParameters,
getTypesForEIP712Domain,
isHex,
serializeTypedData,
validateTypedData,
} from "viem";
import * as ox__Hex from "ox/Hex";
import * as ox__TypedData from "ox/TypedData";
import type { Account, Wallet } from "../interfaces/wallet.js";
import type { SendTransactionOption } from "../interfaces/wallet.js";
import type { AppMetadata, DisconnectFn, SwitchChainFn } from "../types.js";
Expand Down Expand Up @@ -338,29 +333,30 @@ function createAccount({
method: "personal_sign",
params: [messageToSign, account.address],
});
if (!isHex(res)) {
if (!ox__Hex.validate(res)) {
throw new Error("Invalid signature returned");
}
return res;
},
async signTypedData(_typedData) {
async signTypedData(typedData) {
if (!account.address) {
throw new Error("Provider not setup");
}
const typedData = parseTypedData(_typedData);
const { domain, message, primaryType } =
typedData as unknown as SignTypedDataParameters;

const { domain, message, primaryType } = parseTypedData(
typedData,
) as ox__TypedData.Definition;

const types = {
EIP712Domain: getTypesForEIP712Domain({ domain }),
EIP712Domain: ox__TypedData.extractEip712DomainTypes(domain),
...typedData.types,
};

// Need to do a runtime validation check on addresses, byte ranges, integer ranges, etc
// as we can't statically check this with TypeScript.
validateTypedData({ domain, message, primaryType, types });
ox__TypedData.validate({ domain, message, primaryType, types });

const stringifiedData = serializeTypedData({
const stringifiedData = ox__TypedData.serialize({
domain: domain ?? {},
message,
primaryType,
Expand All @@ -371,7 +367,7 @@ function createAccount({
method: "eth_signTypedData_v4",
params: [account.address, stringifiedData],
});
if (!isHex(res)) {
if (!ox__Hex.validate(res)) {
throw new Error("Invalid signed payload returned");
}
return res;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
import type { TypedData } from "abitype";
import type { TypedDataDefinition } from "viem";
import type * as ox__TypedData from "ox/TypedData";
import type { ThirdwebClient } from "../../../../client/client.js";
import { getThirdwebBaseUrl } from "../../../../utils/domains.js";
import { getClientFetch } from "../../../../utils/fetch.js";
import { stringify } from "../../../../utils/json.js";
import type { ClientScopedStorage } from "../authentication/client-scoped-storage.js";

export async function signTypedData<
const typedData extends TypedData | Record<string, unknown>,
const typedData extends ox__TypedData.TypedData | Record<string, unknown>,
primaryType extends keyof typedData | "EIP712Domain" = keyof typedData,
>({
client,
payload,
storage,
}: {
client: ThirdwebClient;
payload: TypedDataDefinition<typedData, primaryType>;
payload: ox__TypedData.Definition<typedData, primaryType>;
storage: ClientScopedStorage;
}) {
const authToken = await storage.getAuthCookie();
Expand Down
13 changes: 4 additions & 9 deletions packages/thirdweb/src/wallets/interfaces/wallet.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import type { Address } from "abitype";
import type {
Hex,
SignableMessage,
TransactionSerializable,
TypedData,
TypedDataDefinition,
} from "viem";
import type * as ox__TypedData from "ox/TypedData";
import type { Hex, SignableMessage, TransactionSerializable } from "viem";
import type { Chain } from "../../chains/types.js";
import type {
EIP712TransactionOptions,
Expand Down Expand Up @@ -191,10 +186,10 @@ export type Account = {
* ```
*/
signTypedData: <
const typedData extends TypedData | Record<string, unknown>,
const typedData extends ox__TypedData.TypedData | Record<string, unknown>,
primaryType extends keyof typedData | "EIP712Domain" = keyof typedData,
>(
_typedData: TypedDataDefinition<typedData, primaryType>,
_typedData: ox__TypedData.Definition<typedData, primaryType>,
) => Promise<Hex>;

// OPTIONAL
Expand Down
16 changes: 5 additions & 11 deletions packages/thirdweb/src/wallets/private-key.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { secp256k1 } from "@noble/curves/secp256k1";
import type {
SignableMessage,
TransactionSerializable,
TypedData,
TypedDataDefinition,
} from "viem";
import type * as ox__TypedData from "ox/TypedData";
import type { SignableMessage, TransactionSerializable } from "viem";
import { publicKeyToAddress } from "viem/utils";
import { getCachedChain } from "../chains/utils.js";
import type { ThirdwebClient } from "../client/client.js";
Expand Down Expand Up @@ -69,13 +65,11 @@ export function privateKeyToAccount(
const privateKey = `0x${options.privateKey.replace(/^0x/, "")}` satisfies Hex;

const publicKey = toHex(secp256k1.getPublicKey(privateKey.slice(2), false));
const address = publicKeyToAddress(publicKey); // TODO: Implement publicKeyToAddress natively (will need checksumAddress downstream)
const address = publicKeyToAddress(publicKey);

const account = {
address,
sendTransaction: async (
// TODO: figure out how we would pass our "chain" object in here?
// maybe we *do* actually have to take in a tx object instead of the raw tx?
tx: TransactionSerializable & { chainId: number },
) => {
const rpcRequest = getRpcClient({
Expand All @@ -101,10 +95,10 @@ export function privateKeyToAccount(
});
},
signTypedData: async <
const typedData extends TypedData | Record<string, unknown>,
const typedData extends ox__TypedData.TypedData | Record<string, unknown>,
primaryType extends keyof typedData | "EIP712Domain" = keyof typedData,
>(
_typedData: TypedDataDefinition<typedData, primaryType>,
_typedData: ox__TypedData.Definition<typedData, primaryType>,
) => {
return signTypedData({
..._typedData,
Expand Down
Loading
Loading