Skip to content

Commit

Permalink
Add Various Type Guards w. Tests (#146)
Browse files Browse the repository at this point in the history
  • Loading branch information
bh2smith authored Nov 12, 2024
1 parent ae2c0df commit 872c0eb
Show file tree
Hide file tree
Showing 10 changed files with 252 additions and 22 deletions.
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";
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

0 comments on commit 872c0eb

Please sign in to comment.