Skip to content

Commit 95ebfcf

Browse files
authored
feat(tempo): add TokenId.compute for deterministic TIP-20 addresses (#153)
* feat(tempo): TokenId.compute * alpha
1 parent 7c0b2ba commit 95ebfcf

File tree

3 files changed

+82
-0
lines changed

3 files changed

+82
-0
lines changed

.changeset/bright-doors-fly.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"ox": minor
3+
---
4+
5+
Added `TokenId.compute` to compute deterministic TIP-20 token addresses from sender and salt.

src/tempo/TokenId.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,36 @@ test('toAddress', () => {
3838
'0x20c0000000000000000000000000000000000def',
3939
)
4040
})
41+
42+
test('compute', () => {
43+
const sender = '0x1234567890123456789012345678901234567890'
44+
const salt1 =
45+
'0x0000000000000000000000000000000000000000000000000000000000000001'
46+
const salt2 =
47+
'0x0000000000000000000000000000000000000000000000000000000000000002'
48+
49+
const address1 = TokenId.compute({ sender, salt: salt1 })
50+
const address2 = TokenId.compute({ sender, salt: salt2 })
51+
52+
// deterministic: same inputs produce same output
53+
expect(TokenId.compute({ sender, salt: salt1 })).toBe(address1)
54+
55+
// different salts produce different addresses
56+
expect(address1).not.toBe(address2)
57+
58+
// addresses have TIP-20 prefix (0x20c0 followed by zeroes for 12 bytes total)
59+
expect(address1.toLowerCase().startsWith('0x20c000000000000000000000')).toBe(
60+
true,
61+
)
62+
expect(address2.toLowerCase().startsWith('0x20c000000000000000000000')).toBe(
63+
true,
64+
)
65+
66+
// addresses are 20 bytes (40 hex chars + 0x prefix)
67+
expect(address1.length).toBe(42)
68+
69+
// different senders produce different addresses
70+
const otherSender = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'
71+
const address3 = TokenId.compute({ sender: otherSender, salt: salt1 })
72+
expect(address3).not.toBe(address1)
73+
})

src/tempo/TokenId.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import * as AbiParameters from '../core/AbiParameters.js'
12
import * as Address from '../core/Address.js'
3+
import * as Hash from '../core/Hash.js'
24
import * as Hex from '../core/Hex.js'
35

46
const tip20Prefix = '0x20c0'
@@ -78,3 +80,45 @@ export function toAddress(tokenId: TokenIdOrAddress): Address.Address {
7880
const tokenIdHex = Hex.fromNumber(tokenId, { size: 18 })
7981
return Hex.concat(tip20Prefix, tokenIdHex)
8082
}
83+
84+
/**
85+
* Computes a deterministic TIP-20 token address from a sender address and salt.
86+
*
87+
* The address is computed as: `TIP20_PREFIX (12 bytes) || keccak256(abi.encode(sender, salt))[:8]`
88+
*
89+
* [TIP-20 Token Standard](https://docs.tempo.xyz/protocol/tip20/overview)
90+
*
91+
* @example
92+
* ```ts twoslash
93+
* import { TokenId } from 'ox/tempo'
94+
*
95+
* const address = TokenId.compute({
96+
* sender: '0x1234567890123456789012345678901234567890',
97+
* salt: '0x0000000000000000000000000000000000000000000000000000000000000001',
98+
* })
99+
* ```
100+
*
101+
* @param value - The sender address and salt.
102+
* @returns The computed TIP-20 token address.
103+
*/
104+
export function compute(value: compute.Value): Address.Address {
105+
const hash = Hash.keccak256(
106+
AbiParameters.encode(AbiParameters.from('address, bytes32'), [
107+
value.sender,
108+
value.salt,
109+
]),
110+
)
111+
return Hex.concat(
112+
Hex.padRight(tip20Prefix, 12),
113+
Hex.slice(hash, 0, 8),
114+
) as Address.Address
115+
}
116+
117+
export declare namespace compute {
118+
export type Value = {
119+
/** The salt (32 bytes). */
120+
salt: Hex.Hex
121+
/** The sender address. */
122+
sender: Address.Address
123+
}
124+
}

0 commit comments

Comments
 (0)