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
26 changes: 17 additions & 9 deletions src/FormoAnalytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import {
TrackingOptions,
TransactionStatus,
} from "./types";
import { toChecksumAddress, getValidAddress } from "./utils";
import { validateAddress } from "./utils";
import { parseTrafficSource, updateStoredTrafficSource } from "./utils/trafficSource";
import { captureInstallReferrer } from "./lib/installReferrer";
import { Linking, EmitterSubscription } from "react-native";
Expand Down Expand Up @@ -306,23 +306,23 @@ export class FormoAnalytics implements IFormoAnalytics {
return;
}

const checksummedAddress = this.validateAndChecksumAddress(address);
if (!checksummedAddress) {
const validatedAddress = this.validateAndChecksumAddress(address, chainId);
if (!validatedAddress) {
logger.warn(`Connect: Invalid address provided ("${address}")`);
return;
}

// Track event before updating state so connect events TO excluded chains are tracked
await this.trackEvent(
EventType.CONNECT,
{ chainId, address: checksummedAddress },
{ chainId, address: validatedAddress },
properties,
context,
callback
);

this.currentChainId = chainId;
this.currentAddress = checksummedAddress;
this.currentAddress = validatedAddress;
}

/**
Expand Down Expand Up @@ -514,6 +514,8 @@ export class FormoAnalytics implements IFormoAnalytics {
return;
}
this.currentAddress = validAddress;
// Note: validateAddress returns Solana addresses unchanged (Base58, case-sensitive)
// and EVM addresses checksummed.
} else {
this.currentAddress = undefined;
}
Expand Down Expand Up @@ -760,11 +762,17 @@ export class FormoAnalytics implements IFormoAnalytics {
}

/**
* Validate and checksum address
* Validate and normalize an address for the given chain.
*
* EVM addresses are returned in EIP-55 checksum format.
* Solana addresses are returned as-is (Base58 is case-sensitive).
* When chainId is omitted, EVM is tried first with Solana as fallback.
*/
private validateAndChecksumAddress(address: string): Address | undefined {
const validAddress = getValidAddress(address);
return validAddress ? toChecksumAddress(validAddress) : undefined;
private validateAndChecksumAddress(
address: string,
chainId?: ChainID
): Address | undefined {
return validateAddress(address, chainId);
}

/**
Expand Down
179 changes: 179 additions & 0 deletions src/__tests__/solana.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import {
isSolanaAddress,
getValidSolanaAddress,
isBlockedSolanaAddress,
isSolanaSystemAddress,
SOLANA_SYSTEM_ADDRESSES,
} from '../solana/address';
import {
isSolanaChainId,
SOLANA_CHAIN_IDS,
DEFAULT_SOLANA_CHAIN_ID,
} from '../solana/types';
import {
validateAddress,
isBlockedAddress,
} from '../utils/address';

const VITALIK_EVM = '0xd8da6bf26964af9d7eed9e03e53415d37aa96045';
const VITALIK_EVM_CHECKSUM = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045';

// A real Solana address (Vitalik example replaced with valid Base58 32-byte key)
const VALID_SOLANA = 'FDKJvWcJNe6wecbgDYDFPCfgs14aJnVsUfWQRYWLn4Tn';
const ANOTHER_SOLANA = '7v91N7iZ9eyTktonbRy7sTjKBNMWFM8jShaeP4S2t9NA';

describe('Solana address utilities', () => {
describe('isSolanaAddress()', () => {
it('returns true for a valid Base58 32-byte key', () => {
expect(isSolanaAddress(VALID_SOLANA)).toBe(true);
expect(isSolanaAddress(ANOTHER_SOLANA)).toBe(true);
});

it('returns true for system addresses', () => {
expect(isSolanaAddress(SOLANA_SYSTEM_ADDRESSES.SYSTEM_PROGRAM)).toBe(true);
expect(isSolanaAddress(SOLANA_SYSTEM_ADDRESSES.TOKEN_PROGRAM)).toBe(true);
});

it('returns false for too-short strings', () => {
expect(isSolanaAddress('abc')).toBe(false);
});

it('returns false for too-long strings', () => {
expect(isSolanaAddress('1'.repeat(45))).toBe(false);
});

it('returns false for strings containing non-Base58 characters', () => {
// 0 (zero), O (capital o), I (capital i), l (lower L) are not in Base58
expect(isSolanaAddress('0OIl' + '1'.repeat(28))).toBe(false);
});

it('returns false for EVM addresses', () => {
expect(isSolanaAddress(VITALIK_EVM)).toBe(false);
});

it('returns false for non-string values', () => {
expect(isSolanaAddress(null)).toBe(false);
expect(isSolanaAddress(undefined)).toBe(false);
expect(isSolanaAddress(123)).toBe(false);
expect(isSolanaAddress({})).toBe(false);
});
});

describe('getValidSolanaAddress()', () => {
it('trims whitespace and returns the address', () => {
expect(getValidSolanaAddress(` ${VALID_SOLANA} `)).toBe(VALID_SOLANA);
});

it('returns null for invalid addresses', () => {
expect(getValidSolanaAddress('invalid')).toBeNull();
expect(getValidSolanaAddress('')).toBeNull();
expect(getValidSolanaAddress(null)).toBeNull();
expect(getValidSolanaAddress(undefined)).toBeNull();
});

it('handles PublicKey-like objects with toBase58', () => {
const pk = {
toBase58: () => VALID_SOLANA,
toString: () => VALID_SOLANA,
toBytes: () => new Uint8Array(),
equals: () => false,
};
expect(getValidSolanaAddress(pk)).toBe(VALID_SOLANA);
});

it('returns null when toBase58 throws', () => {
const pk = {
toBase58: () => {
throw new Error('boom');
},
toString: () => '',
toBytes: () => new Uint8Array(),
equals: () => false,
};
expect(getValidSolanaAddress(pk)).toBeNull();
});
});

describe('isSolanaSystemAddress() / isBlockedSolanaAddress()', () => {
it('returns true for system program', () => {
expect(isSolanaSystemAddress(SOLANA_SYSTEM_ADDRESSES.SYSTEM_PROGRAM)).toBe(true);
expect(isBlockedSolanaAddress(SOLANA_SYSTEM_ADDRESSES.SYSTEM_PROGRAM)).toBe(true);
});

it('returns false for normal user addresses', () => {
expect(isSolanaSystemAddress(VALID_SOLANA)).toBe(false);
expect(isBlockedSolanaAddress(VALID_SOLANA)).toBe(false);
});
});

describe('isSolanaChainId()', () => {
it('returns true for known Solana chain IDs', () => {
expect(isSolanaChainId(SOLANA_CHAIN_IDS['mainnet-beta'])).toBe(true);
expect(isSolanaChainId(SOLANA_CHAIN_IDS.devnet)).toBe(true);
expect(isSolanaChainId(DEFAULT_SOLANA_CHAIN_ID)).toBe(true);
});

it('returns false for EVM chain IDs', () => {
expect(isSolanaChainId(1)).toBe(false);
expect(isSolanaChainId(137)).toBe(false);
});

it('returns false for null/undefined', () => {
expect(isSolanaChainId(null)).toBe(false);
expect(isSolanaChainId(undefined)).toBe(false);
});
});
});

describe('validateAddress()', () => {
it('returns checksummed EVM address when no chainId is provided', () => {
expect(validateAddress(VITALIK_EVM)).toBe(VITALIK_EVM_CHECKSUM);
});

it('returns Solana address as-is when no chainId is provided', () => {
expect(validateAddress(VALID_SOLANA)).toBe(VALID_SOLANA);
});

it('returns checksummed EVM address for EVM chainId', () => {
expect(validateAddress(VITALIK_EVM, 1)).toBe(VITALIK_EVM_CHECKSUM);
});

it('returns undefined for Solana address with EVM chainId', () => {
expect(validateAddress(VALID_SOLANA, 1)).toBeUndefined();
});

it('returns Solana address for Solana chainId', () => {
expect(validateAddress(VALID_SOLANA, DEFAULT_SOLANA_CHAIN_ID)).toBe(
VALID_SOLANA
);
});

it('returns undefined for EVM address with Solana chainId', () => {
expect(validateAddress(VITALIK_EVM, DEFAULT_SOLANA_CHAIN_ID)).toBeUndefined();
});

it('returns undefined for invalid input', () => {
expect(validateAddress('not-an-address')).toBeUndefined();
expect(validateAddress('')).toBeUndefined();
});
});

describe('isBlockedAddress() with Solana', () => {
it('returns false for normal Solana addresses', () => {
expect(isBlockedAddress(VALID_SOLANA)).toBe(false);
});

it('returns true for Solana system program', () => {
expect(isBlockedAddress(SOLANA_SYSTEM_ADDRESSES.SYSTEM_PROGRAM)).toBe(true);
expect(isBlockedAddress(SOLANA_SYSTEM_ADDRESSES.TOKEN_PROGRAM)).toBe(true);
});

it('still returns true for EVM zero/dead addresses', () => {
expect(
isBlockedAddress('0x0000000000000000000000000000000000000000')
).toBe(true);
expect(
isBlockedAddress('0x000000000000000000000000000000000000dead')
).toBe(true);
});
});
19 changes: 7 additions & 12 deletions src/lib/event/EventFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ import {
TransactionStatus,
} from "../../types";
import {
toChecksumAddress,
getValidAddress,
validateAddress,
toSnakeCase,
mergeDeepRight,
getStoredTrafficSource,
Expand Down Expand Up @@ -337,12 +336,11 @@ class EventFactory implements IEventFactory {
commonEventData.anonymous_id = generateAnonymousId(LOCAL_ANONYMOUS_ID_KEY);

// Handle address - convert undefined to null for consistency
const validAddress = getValidAddress(formoEvent.address);
if (validAddress) {
commonEventData.address = toChecksumAddress(validAddress);
} else {
commonEventData.address = null;
}
// Try EVM first, then Solana fallback (chainId is not always present here).
const validAddress = formoEvent.address
? validateAddress(formoEvent.address)
: undefined;
commonEventData.address = validAddress ?? null;

const processedEvent = mergeDeepRight(
formoEvent as Record<string, unknown>,
Expand Down Expand Up @@ -680,10 +678,7 @@ class EventFactory implements IEventFactory {

// 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.address = address ? validateAddress(address) ?? null : null;
}
formoEvent.user_id = userId || null;

Expand Down
Loading
Loading