Skip to content
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
51 changes: 36 additions & 15 deletions src/FormoAnalytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
ConnectInfo,
} from "./types";
import { toChecksumAddress } from "./utils";
import { isValidAddress, getValidAddress } from "./utils/address";
import { isAddress, isLocalhost } from "./validators";
import { parseChainId } from "./utils/chain";

Expand All @@ -46,7 +47,7 @@ export class FormoAnalytics implements IFormoAnalytics {

config: Config;
currentChainId?: ChainID;
currentAddress?: Address = "";
currentAddress?: Address;
currentUserId?: string = "";

private constructor(
Expand Down Expand Up @@ -174,7 +175,8 @@ export class FormoAnalytics implements IFormoAnalytics {
}

this.currentChainId = chainId;
this.currentAddress = address ? toChecksumAddress(address) : undefined;
const validAddress = getValidAddress(address);
this.currentAddress = validAddress ? toChecksumAddress(validAddress) : undefined;

await this.trackEvent(
EventType.CONNECT,
Expand Down Expand Up @@ -441,7 +443,8 @@ export class FormoAnalytics implements IFormoAnalytics {
// Explicit identify
const { userId, address, providerName, rdns } = params;
logger.info("Identify", address, userId, providerName, rdns);
if (address) this.currentAddress = toChecksumAddress(address);
const validAddress = getValidAddress(address);
if (validAddress) this.currentAddress = toChecksumAddress(validAddress);
if (userId) {
this.currentUserId = userId;
cookie().set(SESSION_USER_ID_KEY, userId);
Expand All @@ -450,7 +453,7 @@ export class FormoAnalytics implements IFormoAnalytics {
await this.trackEvent(
EventType.IDENTIFY,
{
address: address ? toChecksumAddress(address) : undefined,
address: validAddress ? toChecksumAddress(validAddress) : undefined,
providerName,
userId,
rdns,
Expand Down Expand Up @@ -582,12 +585,13 @@ export class FormoAnalytics implements IFormoAnalytics {
}

// Validate the first account is a valid address before processing
if (!isAddress(accounts[0])) {
const validAddress = getValidAddress(accounts[0]);
if (!validAddress) {
logger.warn("onAccountsChanged: Invalid address received", accounts[0]);
return;
}

const address = toChecksumAddress(accounts[0]);
const address = toChecksumAddress(validAddress);
if (address === this.currentAddress) {
// We have already reported this address
return;
Expand Down Expand Up @@ -624,7 +628,8 @@ export class FormoAnalytics implements IFormoAnalytics {
);
return Promise.resolve();
}
this.currentAddress = toChecksumAddress(address);
const validAddress = getValidAddress(address);
this.currentAddress = validAddress ? toChecksumAddress(validAddress) : undefined;
}

// Proceed only if the address exists
Expand Down Expand Up @@ -994,8 +999,9 @@ export class FormoAnalytics implements IFormoAnalytics {
try {
const accounts = await this.getAccounts(p);
if (accounts && accounts.length > 0) {
if (isAddress(accounts[0])) {
return toChecksumAddress(accounts[0]);
const validAddress = getValidAddress(accounts[0]);
if (validAddress) {
return toChecksumAddress(validAddress);
}
}
} catch (err) {
Expand All @@ -1014,7 +1020,10 @@ export class FormoAnalytics implements IFormoAnalytics {
method: "eth_accounts",
});
if (!res || res.length === 0) return null;
return res.filter((e) => isAddress(e)).map(toChecksumAddress);
return res
.map((e) => getValidAddress(e))
.filter((e): e is string => e !== null)
.map(toChecksumAddress);
} catch (err) {
if ((err as any).code !== 4001) {
logger.error(
Expand Down Expand Up @@ -1052,12 +1061,18 @@ export class FormoAnalytics implements IFormoAnalytics {
params: unknown[],
response?: unknown
) {
const rawAddress = method === "personal_sign"
? (params[1] as Address)
: (params[0] as Address);

const validAddress = getValidAddress(rawAddress);
if (!validAddress) {
throw new Error(`Invalid address in signature payload: ${rawAddress}`);
}

const basePayload = {
chainId: this.currentChainId,
address:
method === "personal_sign"
? (params[1] as Address)
: (params[0] as Address),
address: toChecksumAddress(validAddress),
};

if (method === "personal_sign") {
Expand Down Expand Up @@ -1086,10 +1101,16 @@ export class FormoAnalytics implements IFormoAnalytics {
to: string;
value: string;
};

const validAddress = getValidAddress(from);
if (!validAddress) {
throw new Error(`Invalid address in transaction payload: ${from}`);
}

return {
chainId: this.currentChainId || (await this.getCurrentChainId()),
data,
address: from,
address: toChecksumAddress(validAddress),
to,
value,
};
Expand Down
15 changes: 11 additions & 4 deletions src/lib/event/EventFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
UTMParameters,
} from "../../types";
import { toChecksumAddress, toSnakeCase } from "../../utils";
import { getValidAddress } from "../../utils/address";
import { getCurrentTimeFormatted } from "../../utils/timestamp";
import { isUndefined } from "../../validators";
import { logger } from "../logger";
Expand Down Expand Up @@ -219,10 +220,12 @@ class EventFactory implements IEventFactory {

commonEventData.anonymous_id = generateAnonymousId(LOCAL_ANONYMOUS_ID_KEY);

if (formoEvent.address) {
commonEventData.address = toChecksumAddress(formoEvent.address);
// Handle address - convert undefined to null for consistency
const validAddress = getValidAddress(formoEvent.address);
if (validAddress) {
commonEventData.address = toChecksumAddress(validAddress);
} else {
commonEventData.address = formoEvent.address;
commonEventData.address = null;
}

const processedEvent = mergeDeepRight(
Expand Down Expand Up @@ -524,7 +527,11 @@ class EventFactory implements IEventFactory {
break;
}

!formoEvent.address && (formoEvent.address = address ? toChecksumAddress(address) : null);
// Set address if not already set by the specific event generator
if (formoEvent.address === undefined || formoEvent.address === null) {
const validAddress = getValidAddress(address);
formoEvent.address = validAddress ? toChecksumAddress(validAddress) : null;
}
formoEvent.user_id = userId || null;

return formoEvent as IFormoEvent;
Expand Down
37 changes: 37 additions & 0 deletions src/utils/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,43 @@ import {
} from "../validators";
import { isNullish } from "../validators/object";

/**
* Private helper function to validate and trim an address
* @param address The address to validate and trim
* @returns The trimmed address if valid, null otherwise
*/
const _validateAndTrimAddress = (address: Address | null | undefined): string | null => {
if (typeof address === "string" && address.trim() !== "" && isAddress(address.trim())) {
return address.trim();
}
return null;
};

/**
* Type guard to check if an address is valid and non-empty after trimming.
* Note: This function checks if the trimmed value of the address is valid, but does not guarantee that the input address itself is trimmed.
* If you require a trimmed address, use `getValidAddress(address)` to obtain the trimmed value.
* @param address The address to validate
* @returns true if the trimmed address is valid and non-empty, false otherwise.
* @remarks
* This type guard only ensures that the trimmed value is a valid Address. The original input may still contain leading or trailing whitespace.
*/
export const isValidAddress = (address: Address | null | undefined): address is Address => {
return _validateAndTrimAddress(address) !== null;
};

/**
* Validates and returns a trimmed valid address.
* This function trims the input address and validates it, returning the trimmed value if valid.
* @param address The address to validate and trim
* @returns The trimmed address if valid, null otherwise
* @remarks
* This function is the preferred way to get a validated, trimmed address for use in your application.
*/
export const getValidAddress = (address: Address | null | undefined): string | null => {
return _validateAndTrimAddress(address);
};

export const toChecksumAddress = (address: Address): string => {
if (!isAddress(address, false)) {
throw new Error("Invalid address " + address);
Expand Down
148 changes: 148 additions & 0 deletions test/lib/event/EventFactory.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { describe, it } from "mocha";
import { expect } from "chai";
import { toChecksumAddress } from "../../../src/utils";
import { isValidAddress, getValidAddress } from "../../../src/utils/address";
import { isAddress } from "../../../src/validators";

describe("Address handling bug fix", () => {
describe("toChecksumAddress function", () => {
it("should throw error when empty string is passed", () => {
expect(() => toChecksumAddress("")).to.throw("Invalid address ");
});

it("should throw error when undefined is passed", () => {
expect(() => toChecksumAddress(undefined as any)).to.throw("Invalid address undefined");
});

it("should throw error when null is passed", () => {
expect(() => toChecksumAddress(null as any)).to.throw("Invalid address null");
});

it("should handle valid addresses correctly", () => {
const validAddress = "0x1095bBe769fDab716A823d0f7149CAD713d20A13";
expect(() => toChecksumAddress(validAddress)).to.not.throw();
});
});

describe("isValidAddress helper function", () => {
it("should correctly identify empty strings as invalid", () => {
const testAddress: string = "";
expect(isValidAddress(testAddress)).to.be.false;
});

it("should correctly identify undefined as invalid", () => {
const testAddress: string | undefined = undefined;
expect(isValidAddress(testAddress)).to.be.false;
});

it("should correctly identify null as invalid", () => {
const testAddress: string | null = null;
expect(isValidAddress(testAddress)).to.be.false;
});

it("should correctly identify whitespace-only strings as invalid", () => {
const testAddress: string = " ";
expect(isValidAddress(testAddress)).to.be.false;
});

it("should correctly identify valid addresses", () => {
const testAddress: string = "0x1095bBe769fDab716A823d0f7149CAD713d20A13";
expect(isValidAddress(testAddress)).to.be.true;
});

it("should correctly identify non-empty strings as valid", () => {
const testAddress: string = "0x1234567890123456789012345678901234567890";
expect(isValidAddress(testAddress)).to.be.true;
});

it("should correctly identify addresses with leading/trailing whitespace as valid", () => {
const testAddress: string = " 0x1095bBe769fDab716A823d0f7149CAD713d20A13 ";
expect(isValidAddress(testAddress)).to.be.true;
});
});

describe("getValidAddress function", () => {
it("should return trimmed valid address", () => {
const testAddress = " 0x1095bBe769fDab716A823d0f7149CAD713d20A13 ";
const result = getValidAddress(testAddress);
expect(result).to.equal("0x1095bBe769fDab716A823d0f7149CAD713d20A13");
});

it("should return null for invalid addresses", () => {
const testAddress = "invalid-address";
const result = getValidAddress(testAddress);
expect(result).to.be.null;
});

it("should return null for empty strings", () => {
const testAddress = "";
const result = getValidAddress(testAddress);
expect(result).to.be.null;
});

it("should return null for whitespace-only strings", () => {
const testAddress = " ";
const result = getValidAddress(testAddress);
expect(result).to.be.null;
});
});

describe("Comparison between isValidAddress and isAddress", () => {
it("should handle invalid addresses differently", () => {
const invalidAddresses = [
"", // empty string
" ", // whitespace only
"not-an-address", // invalid format
"0x123", // too short
"0x1234567890123456789012345678901234567890123456789012345678901234567890", // too long
];

invalidAddresses.forEach(address => {
// isValidAddress should return false for all invalid addresses
expect(isValidAddress(address)).to.be.false;

// isAddress should also return false for invalid addresses
expect(isAddress(address)).to.be.false;
});
});

it("should handle valid addresses consistently", () => {
const validAddresses = [
"0x1095bBe769fDab716A823d0f7149CAD713d20A13",
"0x1234567890123456789012345678901234567890",
"0x1095bBe769fDab716A823d0f7149CAD713d20A13 ", // with trailing whitespace
" 0x1095bBe769fDab716A823d0f7149CAD713d20A13", // with leading whitespace
];

validAddresses.forEach(address => {
// isValidAddress should return true for valid addresses (after trimming)
expect(isValidAddress(address)).to.be.true;

// isAddress should return true for valid addresses (after trimming)
expect(isAddress(address.trim())).to.be.true;
});
});
});

describe("Whitespace handling bug fix", () => {
it("should handle addresses with whitespace without throwing errors", () => {
const addressWithWhitespace = " 0x1095bBe769fDab716A823d0f7149CAD713d20A13 ";

// This should not throw an error
expect(() => {
const validAddress = getValidAddress(addressWithWhitespace);
if (validAddress) {
toChecksumAddress(validAddress);
}
}).to.not.throw();
});

it("should properly trim addresses before validation", () => {
const addressWithWhitespace = " 0x1095bBe769fDab716A823d0f7149CAD713d20A13 ";
const trimmedAddress = "0x1095bBe769fDab716A823d0f7149CAD713d20A13";

expect(isValidAddress(addressWithWhitespace)).to.be.true;
expect(getValidAddress(addressWithWhitespace)).to.equal(trimmedAddress);
});
});
});
Loading