Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Various Type Guards w. Tests #146

Merged
merged 3 commits into from
Nov 12, 2024
Merged
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
3 changes: 1 addition & 2 deletions src/beta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ import {
relaySignedTransaction,
toPayload,
} from "./utils/transaction";
import { NearEncodedSignRequest, signMethods } from "./types";
import { isSignMethod, NearEncodedSignRequest, signMethods } from "./types";
import { NearEthAdapter } from "./chains/ethereum";
import { Web3WalletTypes } from "@walletconnect/web3wallet";
import { isSignMethod } from "./guards";
import { requestRouter } from "./utils/request";

function stripEip155Prefix(eip155Address: string): string {
Expand Down
11 changes: 0 additions & 11 deletions src/guards.ts

This file was deleted.

1 change: 0 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {

export * from "./chains/ethereum";
export * from "./chains/near";
export * from "./guards";
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is now exported via types/index.ts

export * from "./mpcContract";
export * from "./network";
export * from "./types";
Expand Down
106 changes: 106 additions & 0 deletions src/types/guards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import {
Hex,
isAddress,
parseTransaction,
serializeTransaction,
TransactionSerializable,
TypedDataDomain,
} from "viem";
import { EIP712TypedData, SignMethod, TypedMessageTypes } from ".";

export function isSignMethod(method: unknown): method is SignMethod {
return (
typeof method === "string" &&
[
"eth_sign",
"personal_sign",
"eth_sendTransaction",
"eth_signTypedData",
"eth_signTypedData_v4",
].includes(method)
);
}

const isTypedDataDomain = (domain: unknown): domain is TypedDataDomain => {
if (typeof domain !== "object" || domain === null) return false;

const candidate = domain as Record<string, unknown>;

// Check that all properties, if present, are of the correct type
return Object.entries(candidate).every(([key, value]) => {
switch (key) {
case "chainId":
return typeof value === "undefined" || typeof value === "number";
case "name":
case "version":
return typeof value === "undefined" || typeof value === "string";
case "verifyingContract":
return (
typeof value === "undefined" ||
(typeof value === "string" && isAddress(value))
);
case "salt":
return typeof value === "undefined" || typeof value === "string";
default:
return false; // Reject unknown properties
}
});
};

const isTypedMessageTypes = (types: unknown): types is TypedMessageTypes => {
if (typeof types !== "object" || types === null) return false;

return Object.entries(types).every(([_, value]) => {
return (
Array.isArray(value) &&
value.every(
(item) =>
typeof item === "object" &&
item !== null &&
"name" in item &&
"type" in item &&
typeof item.name === "string" &&
typeof item.type === "string"
)
);
});
};

export const isEIP712TypedData = (obj: unknown): obj is EIP712TypedData => {
if (typeof obj !== "object" || obj === null) return false;

const candidate = obj as Record<string, unknown>;

return (
"domain" in candidate &&
"types" in candidate &&
"message" in candidate &&
"primaryType" in candidate &&
isTypedDataDomain(candidate.domain) &&
isTypedMessageTypes(candidate.types) &&
typeof candidate.message === "object" &&
candidate.message !== null &&
typeof candidate.primaryType === "string"
);
};

// Cheeky attempt to serialize. return true if successful!
export function isTransactionSerializable(
data: unknown
): data is TransactionSerializable {
try {
serializeTransaction(data as TransactionSerializable);
return true;
} catch (error) {
return false;
}
}

export function isRlpHex(data: unknown): data is Hex {
try {
parseTransaction(data as Hex);
return true;
} catch (error) {
return false;
}
}
8 changes: 5 additions & 3 deletions src/types.ts → src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IMpcContract } from "./mpcContract";
import { IMpcContract } from "../mpcContract";
import {
Address,
Hash,
Expand All @@ -9,6 +9,8 @@ import {
TypedDataDomain,
} from "viem";

export * from "./guards";

/**
* Borrowed from @near-wallet-selector/core
* https://github.com/near/wallet-selector/blob/01081aefaa3c96ded9f83a23ecf0d210a4b64590/packages/core/src/lib/wallet/transactions.types.ts#L12
Expand Down Expand Up @@ -169,11 +171,11 @@ export interface MessageData {
message: SignableMessage;
}

interface TypedDataTypes {
export interface TypedDataTypes {
name: string;
type: string;
}
type TypedMessageTypes = {
export type TypedMessageTypes = {
[key: string]: TypedDataTypes[];
};

Expand Down
135 changes: 135 additions & 0 deletions tests/unit/types.guards.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { TransactionSerializable } from "viem";
import {
isEIP712TypedData,
isRlpHex,
isSignMethod,
isTransactionSerializable,
} from "../../src/";

const validEIP1559Transaction: TransactionSerializable = {
to: "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
value: BigInt(1000000000000000000), // 1 ETH
chainId: 1,
maxFeePerGas: 1n,
};

const commonInvalidCases = [
null,
undefined,
{},
{ to: "invalid-address" },
{ value: "not-a-bigint" },
{ chainId: "not-a-number" },
"random string",
123,
[],
];

describe("SignMethod", () => {
it("returns true for all valid SignMethods", async () => {
[
"eth_sign",
"personal_sign",
"eth_sendTransaction",
"eth_signTypedData",
"eth_signTypedData_v4",
].map((item) => expect(isSignMethod(item)).toBe(true));
});

it("returns false for invalid data inputs", async () => {
["poop", undefined, false, 1, {}].map((item) =>
expect(isSignMethod(item)).toBe(false)
);
});
});
describe("isEIP712TypedData", () => {
it("returns true for valid EIP712TypedData", async () => {
const message = {
from: {
name: "Cow",
wallet: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826",
},
to: {
name: "Bob",
wallet: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB",
},
contents: "Hello, Bob!",
} as const;

const domain = {
name: "Ether Mail",
version: "1",
chainId: 1,
verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC",
} as const;

const types = {
Person: [
{ name: "name", type: "string" },
{ name: "wallet", type: "address" },
],
Mail: [
{ name: "from", type: "Person" },
{ name: "to", type: "Person" },
{ name: "contents", type: "string" },
],
} as const;

const typedData = {
types,
primaryType: "Mail",
message,
domain,
} as const;
expect(isEIP712TypedData(typedData)).toBe(true);
});

it("returns false for invalid data inputs", async () => {
commonInvalidCases.map((item) =>
expect(isEIP712TypedData(item)).toBe(false)
);
});
});

describe("isTransactionSerializable", () => {
it("should return true for valid transaction data", () => {
expect(isTransactionSerializable(validEIP1559Transaction)).toBe(true);
});

it("should return false for invalid transaction data", () => {
commonInvalidCases.forEach((testCase) => {
expect(isTransactionSerializable(testCase)).toBe(false);
});
});
});

describe("isRlpHex", () => {
it("should return true for valid RLP-encoded transaction hex", () => {
// This is an example of a valid RLP-encoded transaction hex:

// serializeTransaction(validEIP1559Transaction)
const validRlpHex =
"0x02e501808001809470997970c51812dc3a010c7d01b50e0d17dc79c8880de0b6b3a764000080c0";
expect(isRlpHex(validRlpHex)).toBe(true);
});

it("should return false for invalid RLP hex data", () => {
const invalidCases = [
null,
undefined,
{},
"not-a-hex",
"0x", // empty hex
"0x1234", // too short
"0xinvalid",
123,
[],
// Invalid RLP structure but valid hex
"0x1234567890abcdef",
];

invalidCases.forEach((testCase) => {
expect(isRlpHex(testCase)).toBe(false);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
najPublicKeyStrToUncompressedHexPoint,
deriveChildPublicKey,
uncompressedHexPointToEvmAddress,
} from "../../src/utils/kdf";
} from "../../../src/utils/kdf";

const ROOT_PK =
"secp256k1:54hU5wcCmVUPFWLDALXMh1fFToZsVXrx9BbTbHzSfQq1Kd1rJZi52iPa4QQxo6s5TgjWqgpY8HamYuUDzG6fAaUq";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { recoverMessageAddress } from "viem";
import { mockAdapter } from "../../src/utils/mock-sign";
import { mockAdapter } from "../../../src/utils/mock-sign";

describe("Mock Signing", () => {
it("MockAdapter", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
signatureFromOutcome,
signatureFromTxHash,
transformSignature,
} from "../../src/utils/signature";
} from "../../../src/utils/signature";

describe("utility: get Signature", () => {
const url: string = "https://archival-rpc.testnet.near.org";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { zeroAddress } from "viem";
import { Network, TransactionWithSignature } from "../../src";
import { Network, TransactionWithSignature } from "../../../src";
import {
buildTxPayload,
addSignature,
toPayload,
populateTx,
fromPayload,
} from "../../src/utils/transaction";
} from "../../../src/utils/transaction";

describe("Transaction Builder Functions", () => {
it("buildTxPayload", async () => {
Expand Down