Skip to content

Commit 6ef4239

Browse files
jxomampcode-com
andcommitted
feat(tempo): support bech32m TempoAddress format across tempo modules
- Add TempoAddress.resolve() to convert bech32m (tempo1.../tempoz1...) addresses to hex at input boundaries - Update AuthorizationTempo, KeyAuthorization, TxEnvelopeTempo, TokenId, TransactionRequest to accept TempoAddress inputs - Resolve addresses in serialize() to ensure RLP encoding receives hex strings - Use namespace imports (import * as TempoAddress) consistently - Replace TempoAddress.format() calls in tests with explicit bech32m literals Amp-Thread-ID: https://ampcode.com/threads/T-019cbba1-87d6-75f0-a887-8361637420ab Co-authored-by: Amp <amp@ampcode.com>
1 parent be805fa commit 6ef4239

12 files changed

+276
-37
lines changed

src/tempo/AuthorizationTempo.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,19 @@ describe('from', () => {
8484
}
8585
})
8686

87+
test('tempo address input', () => {
88+
const tempoAddr = 'tempo1qzlftsl42n5lep0v2xlxng7cq7sd2k709sxlwnsu'
89+
90+
const authorization = AuthorizationTempo.from({
91+
address: tempoAddr,
92+
chainId: 1,
93+
nonce: 40n,
94+
})
95+
expect(authorization.address).toBe(
96+
'0xBE95c3f554e9Fc85ec51bE69a3D807A0D55BCF2C',
97+
)
98+
})
99+
87100
test('options: signature (secp256k1)', () => {
88101
const authorization = AuthorizationTempo.from({
89102
address: '0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c',

src/tempo/AuthorizationTempo.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as Hex from '../core/Hex.js'
55
import type { Compute, Mutable } from '../core/internal/types.js'
66
import * as Rlp from '../core/Rlp.js'
77
import * as SignatureEnvelope from './SignatureEnvelope.js'
8+
import * as TempoAddress from './TempoAddress.js'
89

910
/**
1011
* Root type for a Tempo Authorization.
@@ -248,15 +249,22 @@ export function from<
248249
const authorization extends AuthorizationTempo | Rpc,
249250
const signature extends SignatureEnvelope.from.Value | undefined = undefined,
250251
>(
251-
authorization: authorization | AuthorizationTempo,
252+
authorization:
253+
| authorization
254+
| AuthorizationTempo
255+
| (Omit<AuthorizationTempo, 'address'> & { address: TempoAddress.Address }),
252256
options: from.Options<signature> = {},
253257
): from.ReturnType<authorization, signature> {
254258
if (typeof authorization.chainId === 'string')
255259
return fromRpc(authorization as Rpc) as never
260+
const resolved = {
261+
...authorization,
262+
address: TempoAddress.resolve(authorization.address as TempoAddress.Address),
263+
}
256264
if (options.signature) {
257-
return { ...authorization, signature: options.signature } as never
265+
return { ...resolved, signature: options.signature } as never
258266
}
259-
return authorization as never
267+
return resolved as never
260268
}
261269

262270
export declare namespace from {

src/tempo/KeyAuthorization.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,31 @@ describe('from', () => {
8383
`)
8484
})
8585

86+
test('tempo address input', () => {
87+
const tempoAddr = 'tempo1qzlftsl42n5lep0v2xlxng7cq7sd2k709sxlwnsu'
88+
const tempoToken = 'tempo1qqsvqqqqqqqqqqqqqqqqqqqqqqqqqqqqqyr9xgnd'
89+
90+
const authorization = KeyAuthorization.from({
91+
address: tempoAddr,
92+
chainId: 1n,
93+
expiry,
94+
type: 'secp256k1',
95+
limits: [
96+
{
97+
token: tempoToken,
98+
limit: Value.from('10', 6),
99+
},
100+
],
101+
})
102+
103+
expect(authorization.address).toBe(
104+
'0xBE95c3f554e9Fc85ec51bE69a3D807A0D55BCF2C',
105+
)
106+
expect(authorization.limits?.[0]?.token).toBe(
107+
'0x20C0000000000000000000000000000000000001',
108+
)
109+
})
110+
86111
test('with signature (secp256k1)', () => {
87112
const authorization = KeyAuthorization.from(
88113
{

src/tempo/KeyAuthorization.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as Hex from '../core/Hex.js'
55
import type { Compute } from '../core/internal/types.js'
66
import * as Rlp from '../core/Rlp.js'
77
import * as SignatureEnvelope from './SignatureEnvelope.js'
8+
import * as TempoAddress from './TempoAddress.js'
89

910
/**
1011
* Key authorization for provisioning access keys.
@@ -252,17 +253,40 @@ export function from<
252253
const authorization extends KeyAuthorization | Rpc,
253254
const signature extends SignatureEnvelope.from.Value | undefined = undefined,
254255
>(
255-
authorization: authorization | KeyAuthorization,
256+
authorization:
257+
| authorization
258+
| KeyAuthorization
259+
| (Omit<KeyAuthorization, 'address' | 'limits'> & {
260+
address: TempoAddress.Address
261+
limits?:
262+
| readonly { token: TempoAddress.Address; limit: bigint }[]
263+
| undefined
264+
}),
256265
options: from.Options<signature> = {},
257266
): from.ReturnType<authorization, signature> {
258-
if (typeof authorization.expiry === 'string')
267+
if ('keyId' in authorization)
259268
return fromRpc(authorization as Rpc) as never
269+
const auth = authorization as KeyAuthorization & {
270+
limits?: readonly { token: TempoAddress.Address; limit: bigint }[]
271+
}
272+
const resolved = {
273+
...auth,
274+
address: TempoAddress.resolve(auth.address as TempoAddress.Address),
275+
...(auth.limits
276+
? {
277+
limits: auth.limits.map((l) => ({
278+
...l,
279+
token: TempoAddress.resolve(l.token as TempoAddress.Address),
280+
})),
281+
}
282+
: {}),
283+
}
260284
if (options.signature)
261285
return {
262-
...authorization,
286+
...resolved,
263287
signature: SignatureEnvelope.from(options.signature),
264288
} as never
265-
return authorization as never
289+
return resolved as never
266290
}
267291

268292
export declare namespace from {

src/tempo/PoolId.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,13 @@ test('from', () => {
3030
validatorToken: 1n,
3131
})
3232
expect(poolId4).toBe(poolId1)
33+
34+
// Test with tempo address inputs
35+
const tempoAddr0 = 'tempo1qqsvqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqv0ywuh'
36+
const tempoAddr1 = 'tempo1qqsvqqqqqqqqqqqqqqqqqqqqqqqqqqqqqyr9xgnd'
37+
const poolId5 = PoolId.from({
38+
userToken: tempoAddr0,
39+
validatorToken: tempoAddr1,
40+
})
41+
expect(poolId5).toBe(poolId1)
3342
})

src/tempo/TempoAddress.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,34 @@ const rawAddress = Address.checksum(
66
'0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28',
77
)
88

9+
describe('resolve', () => {
10+
test('hex address passthrough', () => {
11+
expect(TempoAddress.resolve(rawAddress)).toBe(rawAddress)
12+
})
13+
14+
test('tempo address', () => {
15+
const tempoAddr = TempoAddress.format(rawAddress)
16+
expect(TempoAddress.resolve(tempoAddr)).toBe(rawAddress)
17+
})
18+
19+
test('tempo zone address', () => {
20+
const tempoAddr = TempoAddress.format(rawAddress, { zoneId: 1 })
21+
expect(TempoAddress.resolve(tempoAddr)).toBe(rawAddress)
22+
})
23+
})
24+
925
describe('format', () => {
1026
test('mainnet address', () => {
1127
expect(TempoAddress.format(rawAddress)).toMatchInlineSnapshot(
1228
`"tempo1qp6z6dwvvc6vq5efyk3ms39une6etu4a9qtj2kk0"`,
1329
)
1430
})
1531

32+
test('tempo address input', () => {
33+
const tempoAddr = TempoAddress.format(rawAddress)
34+
expect(TempoAddress.format(tempoAddr)).toBe(tempoAddr)
35+
})
36+
1637
test('zone address (zone ID = 1)', () => {
1738
expect(
1839
TempoAddress.format(rawAddress, { zoneId: 1 }),

src/tempo/TempoAddress.ts

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,45 @@
1-
import * as Address from '../core/Address.js'
1+
import * as core_Address from '../core/Address.js'
22
import * as Bech32m from '../core/Bech32m.js'
33
import * as Bytes from '../core/Bytes.js'
44
import * as CompactSize from '../core/CompactSize.js'
55
import * as Errors from '../core/Errors.js'
66
import * as Hex from '../core/Hex.js'
7+
import type { Compute } from '../core/internal/types.js'
8+
9+
/** An address that can be either an Ethereum hex address or a Tempo bech32m address. */
10+
export type Address = core_Address.Address | Tempo
711

812
/** Root type for a Tempo Address. */
9-
export type TempoAddress = `tempo1${string}` | `tempoz1${string}`
13+
export type Tempo = Compute<`tempo1${string}` | `tempoz1${string}`>
14+
15+
/**
16+
* Resolves an address input (either an Ethereum hex address or a Tempo bech32m address)
17+
* to an Ethereum hex address.
18+
*
19+
* @example
20+
* ```ts twoslash
21+
* import { TempoAddress } from 'ox/tempo'
22+
*
23+
* const address = TempoAddress.resolve('tempo1qp6z6dwvvc6vq5efyk3ms39une6etu4a9qtj2kk0')
24+
* // @log: '0x742d35CC6634c0532925a3B844bc9e7595F2Bd28'
25+
* ```
26+
*
27+
* @example
28+
* ### Hex Address Passthrough
29+
* ```ts twoslash
30+
* import { TempoAddress } from 'ox/tempo'
31+
*
32+
* const address = TempoAddress.resolve('0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28')
33+
* // @log: '0x742d35CC6634c0532925a3B844bc9e7595F2Bd28'
34+
* ```
35+
*
36+
* @param address - An Ethereum hex address or Tempo bech32m address.
37+
* @returns The resolved Ethereum hex address.
38+
*/
39+
export function resolve(address: Address): core_Address.Address {
40+
if (address.startsWith('tempo')) return parse(address).address
41+
return address as core_Address.Address
42+
}
1043

1144
/**
1245
* Formats a raw Ethereum address (and optional zone ID) into a Tempo address string.
@@ -35,18 +68,16 @@ export type TempoAddress = `tempo1${string}` | `tempoz1${string}`
3568
* @param options - Options.
3669
* @returns The encoded Tempo address string.
3770
*/
38-
export function format(
39-
address: Address.Address,
40-
options: format.Options = {},
41-
): TempoAddress {
71+
export function format(address: Address, options: format.Options = {}): Tempo {
4272
const { zoneId } = options
4373

74+
const resolved = resolve(address)
4475
const hrp = zoneId != null ? 'tempoz' : 'tempo'
4576
const version = new Uint8Array([0x00])
4677
const zone = zoneId != null ? CompactSize.toBytes(zoneId) : new Uint8Array()
47-
const data = Bytes.concat(version, zone, Bytes.fromHex(address))
78+
const data = Bytes.concat(version, zone, Bytes.fromHex(resolved))
4879

49-
return Bech32m.encode(hrp, data) as TempoAddress
80+
return Bech32m.encode(hrp, data) as Tempo
5081
}
5182

5283
export declare namespace format {
@@ -126,15 +157,15 @@ export function parse(tempoAddress: string): parse.ReturnType {
126157
actual: rawAddress.length,
127158
})
128159

129-
const address = Address.checksum(Hex.fromBytes(rawAddress) as Address.Address)
160+
const address = core_Address.checksum(Hex.fromBytes(rawAddress) as never)
130161

131162
return { address, zoneId }
132163
}
133164

134165
export declare namespace parse {
135166
type ReturnType = {
136167
/** The raw 20-byte Ethereum address. */
137-
address: Address.Address
168+
address: core_Address.Address
138169
/** The zone ID, or `undefined` for mainnet addresses. */
139170
zoneId: number | bigint | undefined
140171
}

src/tempo/TokenId.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ test('fromAddress', () => {
2323
expect(
2424
TokenId.fromAddress('0x20c0000000000000000000000000000000000def'),
2525
).toBe(0xdefn)
26+
27+
// tempo address input
28+
const tempoAddr = 'tempo1qqsvqqqqqqqqqqqqqqqqqqqqqqqqqqqqqyr9xgnd'
29+
expect(TokenId.fromAddress(tempoAddr)).toBe(1n)
2630
})
2731

2832
test('toAddress', () => {
@@ -38,6 +42,12 @@ test('toAddress', () => {
3842
expect(TokenId.toAddress(0xdefn)).toBe(
3943
'0x20c0000000000000000000000000000000000def',
4044
)
45+
46+
// tempo address input
47+
const tempoAddr = 'tempo1qqsvqqqqqqqqqqqqqqqqqqqqqqqqqqqqqyr9xgnd'
48+
expect(TokenId.toAddress(tempoAddr)).toBe(
49+
'0x20C0000000000000000000000000000000000001',
50+
)
4151
})
4252

4353
test('compute', () => {
@@ -76,4 +86,8 @@ test('compute', () => {
7686
const otherSender = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'
7787
const address3 = TokenId.compute({ sender: otherSender, salt: salt1 })
7888
expect(address3).not.toBe(address1)
89+
90+
// tempo address input produces same result
91+
const tempoSender = 'tempo1qqfrg4ncjqfrg4ncjqfrg4ncjqfrg4ncjqgmv79k'
92+
expect(TokenId.compute({ sender: tempoSender, salt: salt1 })).toBe(id1)
7993
})

src/tempo/TokenId.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import * as AbiParameters from '../core/AbiParameters.js'
22
import * as Address from '../core/Address.js'
33
import * as Hash from '../core/Hash.js'
44
import * as Hex from '../core/Hex.js'
5+
import * as TempoAddress from './TempoAddress.js'
56

67
const tip20Prefix = '0x20c0'
78

89
export type TokenId = bigint
9-
export type TokenIdOrAddress = TokenId | Address.Address
10+
export type TokenIdOrAddress = TokenId | TempoAddress.Address
1011

1112
/**
1213
* Converts a token ID or address to a token ID.
@@ -50,10 +51,11 @@ export function from(tokenIdOrAddress: TokenIdOrAddress | number): TokenId {
5051
* @param address - The token address.
5152
* @returns The token ID.
5253
*/
53-
export function fromAddress(address: Address.Address): TokenId {
54-
if (!address.toLowerCase().startsWith(tip20Prefix))
54+
export function fromAddress(address: TempoAddress.Address): TokenId {
55+
const resolved = TempoAddress.resolve(address)
56+
if (!resolved.toLowerCase().startsWith(tip20Prefix))
5557
throw new Error('invalid tip20 address.')
56-
return Hex.toBigInt(Hex.slice(address, tip20Prefix.length))
58+
return Hex.toBigInt(Hex.slice(resolved, tip20Prefix.length))
5759
}
5860

5961
/**
@@ -73,8 +75,9 @@ export function fromAddress(address: Address.Address): TokenId {
7375
*/
7476
export function toAddress(tokenId: TokenIdOrAddress): Address.Address {
7577
if (typeof tokenId === 'string') {
76-
Address.assert(tokenId)
77-
return tokenId
78+
const resolved = TempoAddress.resolve(tokenId as TempoAddress.Address)
79+
Address.assert(resolved)
80+
return resolved
7881
}
7982

8083
const tokenIdHex = Hex.fromNumber(tokenId, { size: 18 })
@@ -104,7 +107,7 @@ export function toAddress(tokenId: TokenIdOrAddress): Address.Address {
104107
export function compute(value: compute.Value): bigint {
105108
const hash = Hash.keccak256(
106109
AbiParameters.encode(AbiParameters.from('address, bytes32'), [
107-
value.sender,
110+
TempoAddress.resolve(value.sender),
108111
value.salt,
109112
]),
110113
)
@@ -115,7 +118,7 @@ export declare namespace compute {
115118
export type Value = {
116119
/** The salt (32 bytes). */
117120
salt: Hex.Hex
118-
/** The sender address. */
119-
sender: Address.Address
121+
/** The sender address. Accepts both hex and Tempo bech32m addresses. */
122+
sender: TempoAddress.Address
120123
}
121124
}

src/tempo/TransactionRequest.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { Compute } from '../core/internal/types.js'
44
import * as ox_TransactionRequest from '../core/TransactionRequest.js'
55
import * as AuthorizationTempo from './AuthorizationTempo.js'
66
import * as KeyAuthorization from './KeyAuthorization.js'
7+
import * as TempoAddress from './TempoAddress.js'
78
import * as TokenId from './TokenId.js'
89
import * as Transaction from './Transaction.js'
910
import type { Call } from './TxEnvelopeTempo.js'
@@ -30,7 +31,7 @@ export type TransactionRequest<
3031
authorizationList?:
3132
| AuthorizationTempo.ListSigned<bigintType, numberType>
3233
| undefined
33-
calls?: readonly Call<bigintType>[] | undefined
34+
calls?: readonly Call<bigintType, TempoAddress.Address>[] | undefined
3435
keyAuthorization?: KeyAuthorization.KeyAuthorization<true> | undefined
3536
keyData?: Hex.Hex | undefined
3637
keyType?: KeyType | undefined
@@ -112,7 +113,7 @@ export function toRpc(request: TransactionRequest): Rpc {
112113
)
113114
if (request.calls)
114115
request_rpc.calls = request.calls.map((call) => ({
115-
to: call.to,
116+
to: call.to ? TempoAddress.resolve(call.to) : call.to,
116117
value: call.value ? Hex.fromNumber(call.value) : '0x',
117118
data: call.data ?? '0x',
118119
}))

0 commit comments

Comments
 (0)