1- import type { Hex } from "viem" ;
1+ import { type Hex , keccak256 } from "viem" ;
2+ import { isHex , toBytes } from "viem/utils" ;
23import type { ThirdwebClient } from "../client/client.js" ;
34import { NATIVE_TOKEN_ADDRESS } from "../constants/addresses.js" ;
45import { encodeInitialize } from "../extensions/tokens/__generated__/ERC20Asset/write/initialize.js" ;
@@ -12,6 +13,25 @@ import {
1213} from "./constants.js" ;
1314import type { PoolConfig , TokenParams } from "./types.js" ;
1415
16+ export const SaltFlag = {
17+ /** Mix in msg.sender */
18+ MIX_SENDER : 0x01 ,
19+ /** Mix in block.chainid */
20+ MIX_CHAIN_ID : 0x02 ,
21+ /** Mix in block.number */
22+ MIX_BLOCK_NUMBER : 0x04 ,
23+ /** Mix in contractInitData */
24+ MIX_CONTRACT_INIT_DATA : 0x08 ,
25+ /** Mix in hookInitData */
26+ MIX_HOOK_INIT_DATA : 0x10 ,
27+ /** Mix in creator address */
28+ MIX_CREATOR : 0x20 ,
29+ /** Bypass mode – disable all transformations */
30+ BYPASS : 0x80 ,
31+ } as const ;
32+
33+ export type SaltFlagType = ( typeof SaltFlag ) [ keyof typeof SaltFlag ] ;
34+
1535export async function encodeInitParams ( options : {
1636 client : ThirdwebClient ;
1737 params : TokenParams ;
@@ -75,3 +95,63 @@ export function encodePoolConfig(poolConfig: PoolConfig): Hex {
7595 poolConfig . referrerRewardBps || DEFAULT_REFERRER_REWARD_BPS ,
7696 ] ) ;
7797}
98+
99+ export function generateSalt (
100+ salt : Hex | string ,
101+ flags : SaltFlagType = SaltFlag . MIX_SENDER ,
102+ ) : Hex {
103+ /*
104+ * The salt layout follows the on-chain convention documented in the `guardSalt` Solidity helper.
105+ * [0x00] – 1 byte : flags (bits 0-7)
106+ * [0x01-0x1F] – 31 bytes : user-provided entropy
107+ *
108+ * This helper makes it easy to prepare a salt off-chain that can be passed to
109+ * the contract. It guarantees the returned value is always 32 bytes (66 hex
110+ * chars including the `0x` prefix) and allows callers to optionally pass a
111+ * custom salt and/or explicit flag byte.
112+ */
113+
114+ let flagByte : number = flags ;
115+
116+ // If the salt is already a valid 32-byte hex string, we can use it as is and extract the flag byte
117+ if ( salt && isHex ( salt ) ) {
118+ const hex = salt . replace ( / ^ 0 x / i, "" ) ;
119+ if ( hex . length === 64 ) {
120+ flagByte = parseInt ( hex . slice ( 0 , 2 ) , 16 ) as SaltFlagType ;
121+ salt = `0x${ hex . slice ( 2 ) } ` as Hex ;
122+ } else if ( hex . length < 64 ) {
123+ // If the salt is less than 31 bytes, we need to pad it with zeros (first 2 bytes are the flag byte)
124+ salt = `0x${ hex . padStart ( 62 , "0" ) } ` as Hex ;
125+ } else if ( hex . length > 64 ) {
126+ // If the salt is greater than 32 bytes, we need to keccak256 it.
127+ // first 2 bytes are the flag byte, and truncate to 31 bytes
128+ salt = `0x${ keccak256 ( toBytes ( hex ) ) . slice ( 4 ) } ` as Hex ;
129+ }
130+ } else if ( salt && ! isHex ( salt ) ) {
131+ // 31 bytes of salt data
132+ salt = `0x${ keccak256 ( toBytes ( salt ) ) . slice ( 4 ) } ` as Hex ;
133+ }
134+
135+ // If the flag byte is not a valid 8-bit unsigned integer, throw an error
136+ if ( flagByte < 0 || flagByte > 0xff ) {
137+ throw new Error ( "flags must be an 8-bit unsigned integer (0-255)" ) ;
138+ }
139+
140+ const saltData = salt . replace ( / ^ 0 x / i, "" ) ;
141+ if ( saltData . length !== 62 ) {
142+ // 31 bytes * 2 hex chars
143+ throw new Error (
144+ "salt data (excluding flag byte) cannot exceed 31 bytes (62 hex characters)" ,
145+ ) ;
146+ }
147+
148+ const result =
149+ `0x${ flagByte . toString ( 16 ) . padStart ( 2 , "0" ) } ${ saltData } ` as Hex ;
150+
151+ // Final sanity check – should always be 32 bytes / 66 hex chars (including 0x)
152+ if ( result . length !== 66 ) {
153+ throw new Error ( "generated salt must be 32 bytes" ) ;
154+ }
155+
156+ return result ;
157+ }
0 commit comments