Documentation Index
Fetch the complete documentation index at: https://mintlify.com/1inch/cross-chain-sdk/llms.txt
Use this file to discover all available pages before exploring further.
What is a Hash Lock?
A hash lock is a cryptographic commitment that locks funds in an escrow until a secret value (preimage) is revealed. In 1inch Fusion+, hash locks ensure that cross-chain swaps execute atomically—both parties can claim their funds only after specific conditions are met.
Think of a hash lock like a cryptographic safe: you lock funds with a hash of a secret, and they can only be unlocked by revealing the original secret.
How Hash Locks Work
The hash lock mechanism uses a one-way cryptographic function:
- User generates a secret: A random 32-byte value
- Hash the secret: Apply keccak256 to create the hash lock
- Lock funds: Deploy escrow with the hash as a condition
- Unlock funds: Reveal the secret to claim tokens
import { HashLock } from '@1inch/cross-chain-sdk'
import { randomBytes } from 'crypto'
// Generate a secret
const secret = '0x' + randomBytes(32).toString('hex')
// Create hash lock for single fill
const hashLock = HashLock.forSingleFill(secret)
console.log('Secret:', secret)
console.log('Hash Lock:', hashLock.toString())
Single Fill vs Multiple Fills
1inch Fusion+ supports two types of hash locks depending on whether the swap can be split into multiple parts.
Single Fill Orders
For orders that must be filled atomically in one transaction:
import { HashLock } from '@1inch/cross-chain-sdk'
// Generate one secret
const secret = '0x' + randomBytes(32).toString('hex')
// Create hash lock from the secret's hash
const hashLock = HashLock.forSingleFill(secret)
// The hash lock is simply keccak256(secret)
Implementation: The forSingleFill() method creates a hash lock by hashing the secret:
// From src/domains/hash-lock/hash-lock.ts:66-68
public static forSingleFill(secret: string): HashLock {
return new HashLock(HashLock.hashSecret(secret))
}
public static hashSecret(secret: string): string {
return keccak256(secret) // 32-byte secret -> 32-byte hash
}
Multiple Fill Orders
For orders that can be partially filled by different resolvers:
import { HashLock } from '@1inch/cross-chain-sdk'
// Generate multiple secrets (one per potential fill)
const secrets = Array.from({ length: 3 }).map(
() => '0x' + randomBytes(32).toString('hex')
)
// Create merkle tree from secrets
const merkleLeaves = HashLock.getMerkleLeaves(secrets)
const hashLock = HashLock.forMultipleFills(merkleLeaves)
Implementation: The forMultipleFills() method uses a Merkle tree to commit to multiple secrets:
// From src/domains/hash-lock/hash-lock.ts:70-82
public static forMultipleFills(leaves: MerkleLeaf[]): HashLock {
assert(
leaves.length > 2,
'leaves array must be greater than 2. Or use HashLock.forSingleFill'
)
// Create merkle root from leaves
const root = SimpleMerkleTree.of(leaves).root
// Encode the number of fills in the highest bits
const rootWithCount = BN.fromHex(root).setMask(
new BitMask(240n, 256n),
BigInt(leaves.length - 1)
)
return new HashLock(rootWithCount.toHex(64))
}
Merkle Leaves: Each leaf is created by hashing the secret with its index:
keccak256(abi.encodePacked(uint64(index), bytes32(secretHash)))This ensures each fill has a unique proof even if secrets were accidentally reused.
Generating Merkle Leaves
// From src/domains/hash-lock/hash-lock.ts:26-42
public static getMerkleLeaves(secrets: string[]): MerkleLeaf[] {
return HashLock.getMerkleLeavesFromSecretHashes(
secrets.map(HashLock.hashSecret)
)
}
public static getMerkleLeavesFromSecretHashes(
secretHashes: string[]
): MerkleLeaf[] {
return secretHashes.map(
(s, idx) =>
solidityPackedKeccak256(
['uint64', 'bytes32'],
[idx, s]
) as MerkleLeaf
)
}
When Secrets are Revealed
The timing of secret revelation is critical to the atomic swap protocol:
Revelation Flow
- Order Creation: User creates hash lock and submits order (secret remains private)
- Escrow Deployment: Resolver deploys source and destination escrows
- Ready for Secrets: Escrows are deployed and verified
- Secret Submission: User reveals secret(s) to the 1inch relayer
- Resolver Claims: Resolver uses secret to claim source tokens
- User Receives: User automatically receives destination tokens
// Poll for escrows that are ready for secrets
while (true) {
const secretsToShare = await sdk.getReadyToAcceptSecretFills(orderHash)
if (secretsToShare.fills.length) {
for (const { idx } of secretsToShare.fills) {
// Submit the secret for this fill
await sdk.submitSecret(orderHash, secrets[idx])
console.log(`Submitted secret ${idx}`)
}
}
// Check order status
const { status } = await sdk.getOrderStatus(orderHash)
if (status === OrderStatus.Executed) break
await sleep(1000)
}
Important: Always verify escrow deployments before revealing secrets. The SDK’s getReadyToAcceptSecretFills() method helps ensure escrows are properly deployed, but you should implement additional verification for production systems.
Security Considerations
Secret Properties
- Length: Must be exactly 32 bytes (64 hex characters)
- Randomness: Use cryptographically secure random generation
- Uniqueness: Each secret must be unique per fill
- Privacy: Never reveal secrets before escrows are deployed
import { randomBytes } from 'crypto'
// ✅ Good: Cryptographically secure
const secret = '0x' + randomBytes(32).toString('hex')
// ❌ Bad: Not random enough
const badSecret = '0x' + '1'.repeat(64)
Hash Lock Validation
The SDK validates hash locks to ensure correctness:
// From src/domains/hash-lock/hash-lock.ts:17-24
public static hashSecret(secret: string): string {
assert(
isHexBytes(secret) && getBytesCount(secret) === 32n,
'secret length must be 32 bytes hex encoded'
)
return keccak256(secret)
}
Merkle Proofs for Multiple Fills
When using multiple fills, each fill requires a merkle proof:
const leaves = HashLock.getMerkleLeaves(secrets)
const proof = HashLock.getProof(leaves, fillIndex)
// From src/domains/hash-lock/hash-lock.ts:44-46
public static getProof(leaves: string[], idx: number): MerkleLeaf[] {
return SimpleMerkleTree.of(leaves).getProof(idx) as MerkleLeaf[]
}
Complete Example
Here’s a full example showing both single and multiple fill scenarios:
import { HashLock, SDK, PresetEnum } from '@1inch/cross-chain-sdk'
import { randomBytes } from 'crypto'
const sdk = new SDK({ /* config */ })
// Get quote
const quote = await sdk.getQuote({
amount: '10000000',
srcChainId: NetworkEnum.POLYGON,
dstChainId: NetworkEnum.BINANCE,
srcTokenAddress: '0xc2132d05d31c914a87c6611c10748aeb04b58e8f',
dstTokenAddress: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee',
walletAddress
})
const preset = PresetEnum.fast
// Generate secrets based on preset
const secrets = Array.from({
length: quote.presets[preset].secretsCount
}).map(() => '0x' + randomBytes(32).toString('hex'))
// Create appropriate hash lock
const hashLock = secrets.length === 1
? HashLock.forSingleFill(secrets[0])
: HashLock.forMultipleFills(HashLock.getMerkleLeaves(secrets))
// Hash secrets for order submission
const secretHashes = secrets.map(s => HashLock.hashSecret(s))
// Create and submit order
const { hash, quoteId, order } = await sdk.createOrder(quote, {
walletAddress,
hashLock,
preset,
secretHashes
})
- Atomic Swaps: How hash locks enable atomic cross-chain transfers
- Secrets: Best practices for secret generation and management
- Presets: Understanding how presets affect fill counts