Skip to content
This repository was archived by the owner on Dec 23, 2025. It is now read-only.
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
60 changes: 60 additions & 0 deletions src/hub/hub-utils.ts
Original file line number Diff line number Diff line change
@@ -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));
}
19 changes: 19 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -130,6 +140,15 @@ export {
encodeAction,
decodeAction,

// Hub utils
TokenIdComponents,
VirtualAddressComponents,
TokenId,
VirtualAddress,
getCheckSummedAddress,
generateAddress,
generateTokenId,

// Onchain withdrawals
SubmitWithdrawRequest,
getSubmitWithdrawRequestHash,
Expand Down
21 changes: 21 additions & 0 deletions test/hub-utils/address.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
44 changes: 44 additions & 0 deletions test/hub-utils/fixtures/address.ts
Original file line number Diff line number Diff line change
@@ -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",
},
]
47 changes: 47 additions & 0 deletions test/hub-utils/fixtures/tokenId.ts
Original file line number Diff line number Diff line change
@@ -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",
},
]
36 changes: 36 additions & 0 deletions test/hub-utils/token.test.ts
Original file line number Diff line number Diff line change
@@ -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));
});
});