Skip to content
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
13 changes: 13 additions & 0 deletions src/tempo/AuthorizationTempo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,19 @@ describe('from', () => {
}
})

test('tempo address input', () => {
const tempoAddr = 'tempo1qzlftsl42n5lep0v2xlxng7cq7sd2k709sxlwnsu'

const authorization = AuthorizationTempo.from({
address: tempoAddr,
chainId: 1,
nonce: 40n,
})
expect(authorization.address).toBe(
'0xBE95c3f554e9Fc85ec51bE69a3D807A0D55BCF2C',
)
})

test('options: signature (secp256k1)', () => {
const authorization = AuthorizationTempo.from({
address: '0xbe95c3f554e9fc85ec51be69a3d807a0d55bcf2c',
Expand Down
14 changes: 11 additions & 3 deletions src/tempo/AuthorizationTempo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as Hex from '../core/Hex.js'
import type { Compute, Mutable } from '../core/internal/types.js'
import * as Rlp from '../core/Rlp.js'
import * as SignatureEnvelope from './SignatureEnvelope.js'
import * as TempoAddress from './TempoAddress.js'

/**
* Root type for a Tempo Authorization.
Expand Down Expand Up @@ -248,15 +249,22 @@ export function from<
const authorization extends AuthorizationTempo | Rpc,
const signature extends SignatureEnvelope.from.Value | undefined = undefined,
>(
authorization: authorization | AuthorizationTempo,
authorization:
| authorization
| AuthorizationTempo
| (Omit<AuthorizationTempo, 'address'> & { address: TempoAddress.Address }),
options: from.Options<signature> = {},
): from.ReturnType<authorization, signature> {
if (typeof authorization.chainId === 'string')
return fromRpc(authorization as Rpc) as never
const resolved = {
...authorization,
address: TempoAddress.resolve(authorization.address as TempoAddress.Address),
}
if (options.signature) {
return { ...authorization, signature: options.signature } as never
return { ...resolved, signature: options.signature } as never
}
return authorization as never
return resolved as never
}

export declare namespace from {
Expand Down
25 changes: 25 additions & 0 deletions src/tempo/KeyAuthorization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,31 @@ describe('from', () => {
`)
})

test('tempo address input', () => {
const tempoAddr = 'tempo1qzlftsl42n5lep0v2xlxng7cq7sd2k709sxlwnsu'
const tempoToken = 'tempo1qqsvqqqqqqqqqqqqqqqqqqqqqqqqqqqqqyr9xgnd'

const authorization = KeyAuthorization.from({
address: tempoAddr,
chainId: 1n,
expiry,
type: 'secp256k1',
limits: [
{
token: tempoToken,
limit: Value.from('10', 6),
},
],
})

expect(authorization.address).toBe(
'0xBE95c3f554e9Fc85ec51bE69a3D807A0D55BCF2C',
)
expect(authorization.limits?.[0]?.token).toBe(
'0x20C0000000000000000000000000000000000001',
)
})

test('with signature (secp256k1)', () => {
const authorization = KeyAuthorization.from(
{
Expand Down
32 changes: 28 additions & 4 deletions src/tempo/KeyAuthorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as Hex from '../core/Hex.js'
import type { Compute } from '../core/internal/types.js'
import * as Rlp from '../core/Rlp.js'
import * as SignatureEnvelope from './SignatureEnvelope.js'
import * as TempoAddress from './TempoAddress.js'

/**
* Key authorization for provisioning access keys.
Expand Down Expand Up @@ -252,17 +253,40 @@ export function from<
const authorization extends KeyAuthorization | Rpc,
const signature extends SignatureEnvelope.from.Value | undefined = undefined,
>(
authorization: authorization | KeyAuthorization,
authorization:
| authorization
| KeyAuthorization
| (Omit<KeyAuthorization, 'address' | 'limits'> & {
address: TempoAddress.Address
limits?:
| readonly { token: TempoAddress.Address; limit: bigint }[]
| undefined
}),
options: from.Options<signature> = {},
): from.ReturnType<authorization, signature> {
if (typeof authorization.expiry === 'string')
if ('keyId' in authorization)
return fromRpc(authorization as Rpc) as never
const auth = authorization as KeyAuthorization & {
limits?: readonly { token: TempoAddress.Address; limit: bigint }[]
}
const resolved = {
...auth,
address: TempoAddress.resolve(auth.address as TempoAddress.Address),
...(auth.limits
? {
limits: auth.limits.map((l) => ({
...l,
token: TempoAddress.resolve(l.token as TempoAddress.Address),
})),
}
: {}),
}
if (options.signature)
return {
...authorization,
...resolved,
signature: SignatureEnvelope.from(options.signature),
} as never
return authorization as never
return resolved as never
}

export declare namespace from {
Expand Down
9 changes: 9 additions & 0 deletions src/tempo/PoolId.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,13 @@ test('from', () => {
validatorToken: 1n,
})
expect(poolId4).toBe(poolId1)

// Test with tempo address inputs
const tempoAddr0 = 'tempo1qqsvqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqv0ywuh'
const tempoAddr1 = 'tempo1qqsvqqqqqqqqqqqqqqqqqqqqqqqqqqqqqyr9xgnd'
const poolId5 = PoolId.from({
userToken: tempoAddr0,
validatorToken: tempoAddr1,
})
expect(poolId5).toBe(poolId1)
})
21 changes: 21 additions & 0 deletions src/tempo/TempoAddress.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,34 @@ const rawAddress = Address.checksum(
'0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28',
)

describe('resolve', () => {
test('hex address passthrough', () => {
expect(TempoAddress.resolve(rawAddress)).toBe(rawAddress)
})

test('tempo address', () => {
const tempoAddr = TempoAddress.format(rawAddress)
expect(TempoAddress.resolve(tempoAddr)).toBe(rawAddress)
})

test('tempo zone address', () => {
const tempoAddr = TempoAddress.format(rawAddress, { zoneId: 1 })
expect(TempoAddress.resolve(tempoAddr)).toBe(rawAddress)
})
})

describe('format', () => {
test('mainnet address', () => {
expect(TempoAddress.format(rawAddress)).toMatchInlineSnapshot(
`"tempo1qp6z6dwvvc6vq5efyk3ms39une6etu4a9qtj2kk0"`,
)
})

test('tempo address input', () => {
const tempoAddr = TempoAddress.format(rawAddress)
expect(TempoAddress.format(tempoAddr)).toBe(tempoAddr)
})

test('zone address (zone ID = 1)', () => {
expect(
TempoAddress.format(rawAddress, { zoneId: 1 }),
Expand Down
51 changes: 41 additions & 10 deletions src/tempo/TempoAddress.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,45 @@
import * as Address from '../core/Address.js'
import * as core_Address from '../core/Address.js'
import * as Bech32m from '../core/Bech32m.js'
import * as Bytes from '../core/Bytes.js'
import * as CompactSize from '../core/CompactSize.js'
import * as Errors from '../core/Errors.js'
import * as Hex from '../core/Hex.js'
import type { Compute } from '../core/internal/types.js'

/** An address that can be either an Ethereum hex address or a Tempo bech32m address. */
export type Address = core_Address.Address | Tempo

/** Root type for a Tempo Address. */
export type TempoAddress = `tempo1${string}` | `tempoz1${string}`
export type Tempo = Compute<`tempo1${string}` | `tempoz1${string}`>

/**
* Resolves an address input (either an Ethereum hex address or a Tempo bech32m address)
* to an Ethereum hex address.
*
* @example
* ```ts twoslash
* import { TempoAddress } from 'ox/tempo'
*
* const address = TempoAddress.resolve('tempo1qp6z6dwvvc6vq5efyk3ms39une6etu4a9qtj2kk0')
* // @log: '0x742d35CC6634c0532925a3B844bc9e7595F2Bd28'
* ```
*
* @example
* ### Hex Address Passthrough
* ```ts twoslash
* import { TempoAddress } from 'ox/tempo'
*
* const address = TempoAddress.resolve('0x742d35Cc6634C0532925a3b844Bc9e7595f2bD28')
* // @log: '0x742d35CC6634c0532925a3B844bc9e7595F2Bd28'
* ```
*
* @param address - An Ethereum hex address or Tempo bech32m address.
* @returns The resolved Ethereum hex address.
*/
export function resolve(address: Address): core_Address.Address {
if (address.startsWith('tempo')) return parse(address).address
return address as core_Address.Address
}

/**
* Formats a raw Ethereum address (and optional zone ID) into a Tempo address string.
Expand Down Expand Up @@ -35,18 +68,16 @@ export type TempoAddress = `tempo1${string}` | `tempoz1${string}`
* @param options - Options.
* @returns The encoded Tempo address string.
*/
export function format(
address: Address.Address,
options: format.Options = {},
): TempoAddress {
export function format(address: Address, options: format.Options = {}): Tempo {
const { zoneId } = options

const resolved = resolve(address)
const hrp = zoneId != null ? 'tempoz' : 'tempo'
const version = new Uint8Array([0x00])
const zone = zoneId != null ? CompactSize.toBytes(zoneId) : new Uint8Array()
const data = Bytes.concat(version, zone, Bytes.fromHex(address))
const data = Bytes.concat(version, zone, Bytes.fromHex(resolved))

return Bech32m.encode(hrp, data) as TempoAddress
return Bech32m.encode(hrp, data) as Tempo
}

export declare namespace format {
Expand Down Expand Up @@ -126,15 +157,15 @@ export function parse(tempoAddress: string): parse.ReturnType {
actual: rawAddress.length,
})

const address = Address.checksum(Hex.fromBytes(rawAddress) as Address.Address)
const address = core_Address.checksum(Hex.fromBytes(rawAddress) as never)

return { address, zoneId }
}

export declare namespace parse {
type ReturnType = {
/** The raw 20-byte Ethereum address. */
address: Address.Address
address: core_Address.Address
/** The zone ID, or `undefined` for mainnet addresses. */
zoneId: number | bigint | undefined
}
Expand Down
14 changes: 14 additions & 0 deletions src/tempo/TokenId.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ test('fromAddress', () => {
expect(
TokenId.fromAddress('0x20c0000000000000000000000000000000000def'),
).toBe(0xdefn)

// tempo address input
const tempoAddr = 'tempo1qqsvqqqqqqqqqqqqqqqqqqqqqqqqqqqqqyr9xgnd'
expect(TokenId.fromAddress(tempoAddr)).toBe(1n)
})

test('toAddress', () => {
Expand All @@ -38,6 +42,12 @@ test('toAddress', () => {
expect(TokenId.toAddress(0xdefn)).toBe(
'0x20c0000000000000000000000000000000000def',
)

// tempo address input
const tempoAddr = 'tempo1qqsvqqqqqqqqqqqqqqqqqqqqqqqqqqqqqyr9xgnd'
expect(TokenId.toAddress(tempoAddr)).toBe(
'0x20C0000000000000000000000000000000000001',
)
})

test('compute', () => {
Expand Down Expand Up @@ -76,4 +86,8 @@ test('compute', () => {
const otherSender = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'
const address3 = TokenId.compute({ sender: otherSender, salt: salt1 })
expect(address3).not.toBe(address1)

// tempo address input produces same result
const tempoSender = 'tempo1qqfrg4ncjqfrg4ncjqfrg4ncjqfrg4ncjqgmv79k'
expect(TokenId.compute({ sender: tempoSender, salt: salt1 })).toBe(id1)
})
21 changes: 12 additions & 9 deletions src/tempo/TokenId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import * as AbiParameters from '../core/AbiParameters.js'
import * as Address from '../core/Address.js'
import * as Hash from '../core/Hash.js'
import * as Hex from '../core/Hex.js'
import * as TempoAddress from './TempoAddress.js'

const tip20Prefix = '0x20c0'

export type TokenId = bigint
export type TokenIdOrAddress = TokenId | Address.Address
export type TokenIdOrAddress = TokenId | TempoAddress.Address

/**
* Converts a token ID or address to a token ID.
Expand Down Expand Up @@ -50,10 +51,11 @@ export function from(tokenIdOrAddress: TokenIdOrAddress | number): TokenId {
* @param address - The token address.
* @returns The token ID.
*/
export function fromAddress(address: Address.Address): TokenId {
if (!address.toLowerCase().startsWith(tip20Prefix))
export function fromAddress(address: TempoAddress.Address): TokenId {
const resolved = TempoAddress.resolve(address)
if (!resolved.toLowerCase().startsWith(tip20Prefix))
throw new Error('invalid tip20 address.')
return Hex.toBigInt(Hex.slice(address, tip20Prefix.length))
return Hex.toBigInt(Hex.slice(resolved, tip20Prefix.length))
}

/**
Expand All @@ -73,8 +75,9 @@ export function fromAddress(address: Address.Address): TokenId {
*/
export function toAddress(tokenId: TokenIdOrAddress): Address.Address {
if (typeof tokenId === 'string') {
Address.assert(tokenId)
return tokenId
const resolved = TempoAddress.resolve(tokenId as TempoAddress.Address)
Address.assert(resolved)
return resolved
}

const tokenIdHex = Hex.fromNumber(tokenId, { size: 18 })
Expand Down Expand Up @@ -104,7 +107,7 @@ export function toAddress(tokenId: TokenIdOrAddress): Address.Address {
export function compute(value: compute.Value): bigint {
const hash = Hash.keccak256(
AbiParameters.encode(AbiParameters.from('address, bytes32'), [
value.sender,
TempoAddress.resolve(value.sender),
value.salt,
]),
)
Expand All @@ -115,7 +118,7 @@ export declare namespace compute {
export type Value = {
/** The salt (32 bytes). */
salt: Hex.Hex
/** The sender address. */
sender: Address.Address
/** The sender address. Accepts both hex and Tempo bech32m addresses. */
sender: TempoAddress.Address
}
}
5 changes: 3 additions & 2 deletions src/tempo/TransactionRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { Compute } from '../core/internal/types.js'
import * as ox_TransactionRequest from '../core/TransactionRequest.js'
import * as AuthorizationTempo from './AuthorizationTempo.js'
import * as KeyAuthorization from './KeyAuthorization.js'
import * as TempoAddress from './TempoAddress.js'
import * as TokenId from './TokenId.js'
import * as Transaction from './Transaction.js'
import type { Call } from './TxEnvelopeTempo.js'
Expand All @@ -30,7 +31,7 @@ export type TransactionRequest<
authorizationList?:
| AuthorizationTempo.ListSigned<bigintType, numberType>
| undefined
calls?: readonly Call<bigintType>[] | undefined
calls?: readonly Call<bigintType, TempoAddress.Address>[] | undefined
keyAuthorization?: KeyAuthorization.KeyAuthorization<true> | undefined
keyData?: Hex.Hex | undefined
keyType?: KeyType | undefined
Expand Down Expand Up @@ -112,7 +113,7 @@ export function toRpc(request: TransactionRequest): Rpc {
)
if (request.calls)
request_rpc.calls = request.calls.map((call) => ({
to: call.to,
to: call.to ? TempoAddress.resolve(call.to) : call.to,
value: call.value ? Hex.fromNumber(call.value) : '0x',
data: call.data ?? '0x',
}))
Expand Down
Loading
Loading