diff --git a/src/hub/hub-utils.ts b/src/hub/hub-utils.ts new file mode 100644 index 0000000..64fe4a4 --- /dev/null +++ b/src/hub/hub-utils.ts @@ -0,0 +1,60 @@ +import { getAddress, keccak256, encodePacked } from "viem"; +import { VmType } from "../utils"; + +export interface TokenIdComponents { + family: VmType; + chainId: bigint; + address: string; +} + +export interface VirtualAddressComponents { + family: VmType; + chainId: bigint; + address: string; +} + +export type TokenId = bigint; +export type VirtualAddress = `0x${string}`; + +export const getCheckSummedAddress = (family: string, address: string) => { + const checksummedAddress = + family === "ethereum-vm" ? getAddress(address) : address; + return checksummedAddress; +}; + +/** + * Generates a virtual Ethereum address from token components + * @param components The token components (family, chainId, address) + * @returns A checksummed Ethereum address derived from the token ID + * @remarks This function first generates a token ID using the components, + * then converts the last 20 bytes of the hash to an Ethereum address. + * This is equivalent to the Solidity: address(uint160(uint256(addressHash))) + */ + +export function generateAddress( + components: VirtualAddressComponents +): VirtualAddress { + const { chainId, address, family } = components; + const addressHash = keccak256( + encodePacked( + ["string", "uint256", family === "ethereum-vm" ? "address" : "string"], + [family, chainId, getCheckSummedAddress(family, address)] + ) + ); + const addressBytes = addressHash.slice(2).slice(-40); + return getAddress("0x" + addressBytes) as `0x${string}`; +} + +/** + * Generates a token ID based on the chain type, chain ID, and address + * @param components The token components (family, chainId, address) + * @returns The keccak256 hash of the token components + */ +export function generateTokenId(components: TokenIdComponents): TokenId { + const { family, chainId, address } = components; + const packedData = encodePacked( + ["string", "uint256", family === "ethereum-vm" ? "address" : "string"], + [family, chainId, getCheckSummedAddress(family, address)] + ); + return BigInt(keccak256(packedData)); +} diff --git a/src/index.ts b/src/index.ts index 2ba191c..f87c702 100644 --- a/src/index.ts +++ b/src/index.ts @@ -70,6 +70,16 @@ import { WithdrawalAddressRequest, } from "./messages/v2.2/withdrawal-execution"; +import { + TokenIdComponents, + VirtualAddressComponents, + TokenId, + VirtualAddress, + getCheckSummedAddress, + generateAddress, + generateTokenId, +} from "./hub/hub-utils"; + export { // Order Order, @@ -130,6 +140,15 @@ export { encodeAction, decodeAction, + // Hub utils + TokenIdComponents, + VirtualAddressComponents, + TokenId, + VirtualAddress, + getCheckSummedAddress, + generateAddress, + generateTokenId, + // Onchain withdrawals SubmitWithdrawRequest, getSubmitWithdrawRequestHash, diff --git a/test/hub-utils/address.test.ts b/test/hub-utils/address.test.ts new file mode 100644 index 0000000..ae9fa07 --- /dev/null +++ b/test/hub-utils/address.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, test } from "vitest"; +import { generateAddress } from "../../src/hub/hub-utils"; +import { addressesTestCases } from "./fixtures/address"; +import { ethers } from "ethers"; + +describe("Virtual Addresses", () => { + test.each(addressesTestCases)("$name", ({ input, expectedAddress }) => { + const address = generateAddress(input); + + const differentInput = { + ...input, + chainId: input.chainId + 1n, + }; + const differentAddress = generateAddress(differentInput); + expect(address).not.toBe(differentAddress); + expect(address).toBe(expectedAddress); + + // address with correct checksum + expect(ethers.getAddress(expectedAddress)).toBe(expectedAddress); + }); +}); diff --git a/test/hub-utils/fixtures/address.ts b/test/hub-utils/fixtures/address.ts new file mode 100644 index 0000000..4ecc7cc --- /dev/null +++ b/test/hub-utils/fixtures/address.ts @@ -0,0 +1,44 @@ +import { VirtualAddressComponents } from "@relay-protocol/types" + +export const addressesTestCases: Array<{ + name: string + input: VirtualAddressComponents + expectedAddress: `0x${string}` +}> = [ + { + expectedAddress: "0xb1AF659094F7CF6c3FfE7e4d056d968B0Fe58663", + input: { + address: "0x0000000000000000000000000000000000000000", + chainId: 1n, + family: "ethereum-vm", + }, + name: "ETH on Ethereum", // '0x' + 64 hex characters + }, + { + expectedAddress: "0xa1e17A109f1909b54C5611c6655AFcbAF1F09239", + input: { + address: "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", + chainId: 1n, + family: "bitcoin-vm", + }, + name: "Bitcoin", + }, + { + expectedAddress: "0xAa261e59fd53c7B115f1aae3918D416629ace745", + input: { + address: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + chainId: 1n, + family: "solana-vm", + }, + name: "USDC on Solana", + }, + { + expectedAddress: "0xe3c144E770F8547Df5aF51A2d4E84C9c289CcC3a", + input: { + address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + chainId: 8453n, + family: "ethereum-vm", + }, + name: "USDC on Base", + }, +] diff --git a/test/hub-utils/fixtures/tokenId.ts b/test/hub-utils/fixtures/tokenId.ts new file mode 100644 index 0000000..48a7cb2 --- /dev/null +++ b/test/hub-utils/fixtures/tokenId.ts @@ -0,0 +1,47 @@ +import { TokenIdComponents } from "@relay-protocol/types" +export const tokenIdTestCases: Array<{ + name: string + input: TokenIdComponents + expectedValue: bigint +}> = [ + { + expectedValue: + 5126370114286486119248922823807248445856144931672230102669788761404601632355n, + input: { + address: "0x0000000000000000000000000000000000000000", + chainId: 1n, + family: "ethereum-vm", + }, + name: "ETH on Ethereum", + }, + { + expectedValue: + 101142405549722680701516949243527989485095939267215334056209565926507227943481n, + input: { + address: "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", + chainId: 1n, + family: "bitcoin-vm", + }, + name: "Bitcoin", + }, + { + expectedValue: + 108890717977569292143568470585265267208172758058844132994285904278323093890885n, + input: { + address: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + chainId: 1n, + family: "solana-vm", + }, + name: "USDC on Solana", + }, + { + expectedValue: + 30815307311220170804965801606391678921022824512560571593430839734064343993402n, + input: { + address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + chainId: 8453n, + family: "ethereum-vm", + }, + name: "USDC on Base", + }, +] diff --git a/test/hub-utils/token.test.ts b/test/hub-utils/token.test.ts new file mode 100644 index 0000000..e36c630 --- /dev/null +++ b/test/hub-utils/token.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, test } from "vitest"; +import { + generateTokenId, + type TokenIdComponents, +} from "../../src/hub/hub-utils"; +import { tokenIdTestCases } from "./fixtures/tokenId"; + +describe("Token ID Generation", () => { + test.each(tokenIdTestCases)("$name", ({ input, expectedValue }) => { + const tokenId = generateTokenId(input); + + const differentInput = { + ...input, + chainId: input.chainId + 1n, + }; + const differentTokenId = generateTokenId(differentInput); + expect(tokenId).not.toBe(differentTokenId); + expect(tokenId).toBe(expectedValue); + }); + + test("should handle case-insensitive EVM addresses", () => { + const addr = "0xdAC17F958D2ee523a2206206994597C13D831ec7"; + const input1: TokenIdComponents = { + address: addr, + chainId: 1n, + family: "ethereum-vm", + }; + const input2: TokenIdComponents = { + address: addr.toLowerCase(), + chainId: 1n, + family: "ethereum-vm", + }; + + expect(generateTokenId(input1)).toBe(generateTokenId(input2)); + }); +});