From 61c8fce101c51f371e100692ed4cea49612dafdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Sat, 13 Dec 2025 13:59:52 +0100 Subject: [PATCH 1/3] move hub utils to sdk --- src/hub/hub-utils.ts | 65 ++++++++++++++++++++++++++++++ test/hub-utils/address.test.ts | 21 ++++++++++ test/hub-utils/fixtures/address.ts | 44 ++++++++++++++++++++ test/hub-utils/fixtures/tokenId.ts | 47 +++++++++++++++++++++ test/hub-utils/token.test.ts | 36 +++++++++++++++++ 5 files changed, 213 insertions(+) create mode 100644 src/hub/hub-utils.ts create mode 100644 test/hub-utils/address.test.ts create mode 100644 test/hub-utils/fixtures/address.ts create mode 100644 test/hub-utils/fixtures/tokenId.ts create mode 100644 test/hub-utils/token.test.ts diff --git a/src/hub/hub-utils.ts b/src/hub/hub-utils.ts new file mode 100644 index 0000000..1ba73be --- /dev/null +++ b/src/hub/hub-utils.ts @@ -0,0 +1,65 @@ +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)); +} + +export const generateIntentAddress = (intentId: string): VirtualAddress => { + const addressHash = keccak256(encodePacked(["string"], [intentId])); + return getAddress("0x" + addressHash.slice(2).slice(-40)) as `0x${string}`; +}; 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)); + }); +}); From 8691f96938bb8790c382f0b611ca60d86054af7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Sat, 13 Dec 2025 14:01:39 +0100 Subject: [PATCH 2/3] add to index --- src/hub/hub-utils.ts | 5 ----- src/index.ts | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/hub/hub-utils.ts b/src/hub/hub-utils.ts index 1ba73be..5d19c7a 100644 --- a/src/hub/hub-utils.ts +++ b/src/hub/hub-utils.ts @@ -58,8 +58,3 @@ export function generateTokenId(components: TokenIdComponents): TokenId { ); return BigInt(keccak256(packedData)); } - -export const generateIntentAddress = (intentId: string): VirtualAddress => { - const addressHash = keccak256(encodePacked(["string"], [intentId])); - return getAddress("0x" + addressHash.slice(2).slice(-40)) as `0x${string}`; -}; diff --git a/src/index.ts b/src/index.ts index cb17687..432be44 100644 --- a/src/index.ts +++ b/src/index.ts @@ -66,6 +66,16 @@ import { getWithdrawalAddress, } from "./messages/v2.2/allocator"; +import { + TokenIdComponents, + VirtualAddressComponents, + TokenId, + VirtualAddress, + getCheckSummedAddress, + generateAddress, + generateTokenId, +} from "./hub/hub-utils"; + export { // Order Order, @@ -130,4 +140,13 @@ export { SubmitWithdrawRequest, getSubmitWithdrawRequestHash, getWithdrawalAddress, + + // + TokenIdComponents, + VirtualAddressComponents, + TokenId, + VirtualAddress, + getCheckSummedAddress, + generateAddress, + generateTokenId, }; From 31c50f1ccfa4377d7f879f29c5334092387c202a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Renaud?= Date: Sat, 13 Dec 2025 14:02:04 +0100 Subject: [PATCH 3/3] fix path --- src/hub/hub-utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hub/hub-utils.ts b/src/hub/hub-utils.ts index 5d19c7a..64fe4a4 100644 --- a/src/hub/hub-utils.ts +++ b/src/hub/hub-utils.ts @@ -1,5 +1,5 @@ import { getAddress, keccak256, encodePacked } from "viem"; -import { VmType } from "../../utils"; +import { VmType } from "../utils"; export interface TokenIdComponents { family: VmType;