diff --git a/src/tempo/TokenAddress.ts b/src/tempo/TokenAddress.ts new file mode 100644 index 00000000..24b1b9e4 --- /dev/null +++ b/src/tempo/TokenAddress.ts @@ -0,0 +1,30 @@ +import * as Address from '../core/Address.js' +import * as TempoAddress from './TempoAddress.js' + +export const prefix = '0x20c000000000000000000000' + +/** + * Returns whether an address is a TIP-20 token address. + * + * [TIP-20 Token Standard](https://docs.tempo.xyz/protocol/tip20/overview) + * + * @example + * ```ts twoslash + * import { TokenAddress } from 'ox/tempo' + * + * const isTip20 = TokenAddress.isTip20('0x20c0000000000000000000000000000000000001') + * // @log: true + * ``` + * + * @param address - Address to check. + * @returns Whether the address is a TIP-20 token address. + */ +export function isTip20(address: TempoAddress.Address): boolean { + const resolved = TempoAddress.resolve(address) + Address.assert(resolved, { strict: false }) + return resolved.toLowerCase().startsWith(prefix) +} + +export declare namespace isTip20 { + type ErrorType = Address.assert.ErrorType | TempoAddress.parse.ErrorType +} diff --git a/src/tempo/TokenId.test.ts b/src/tempo/TokenId.test.ts index daae6b27..b212f433 100644 --- a/src/tempo/TokenId.test.ts +++ b/src/tempo/TokenId.test.ts @@ -1,5 +1,5 @@ import { Hex } from 'ox' -import { TokenId } from 'ox/tempo' +import { TokenAddress, TokenId } from 'ox/tempo' import { expect, test } from 'vitest' test('from', () => { @@ -29,6 +29,24 @@ test('fromAddress', () => { expect(TokenId.fromAddress(tempoAddr)).toBe(1n) }) +test('fromAddress: invalid address', () => { + expect(() => + TokenId.fromAddress('0x20c1000000000000000000000000000000000001'), + ).toThrow('invalid tip20 address.') +}) + +test('TokenAddress.isTip20', () => { + expect( + TokenAddress.isTip20('0x20c0000000000000000000000000000000000001'), + ).toBe(true) + expect( + TokenAddress.isTip20('tempox0x20c0000000000000000000000000000000000001'), + ).toBe(true) + expect( + TokenAddress.isTip20('0x20c1000000000000000000000000000000000001'), + ).toBe(false) +}) + test('toAddress', () => { expect(TokenId.toAddress(0n)).toBe( '0x20c0000000000000000000000000000000000000', diff --git a/src/tempo/TokenId.ts b/src/tempo/TokenId.ts index bed4d0e6..5ed6e6bb 100644 --- a/src/tempo/TokenId.ts +++ b/src/tempo/TokenId.ts @@ -3,8 +3,10 @@ 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' +import * as TokenAddress from './TokenAddress.js' -const tip20Prefix = '0x20c0' +const tip20Prefix = TokenAddress.prefix +const tip20PrefixSize = 12 export type TokenId = bigint export type TokenIdOrAddress = @@ -55,9 +57,8 @@ export function from(tokenIdOrAddress: TokenIdOrAddress | number): TokenId { */ 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(resolved, tip20Prefix.length)) + if (!TokenAddress.isTip20(resolved)) throw new Error('invalid tip20 address.') + return Hex.toBigInt(Hex.slice(resolved, tip20PrefixSize)) } /** @@ -84,7 +85,7 @@ export function toAddress( return resolved } - const tokenIdHex = Hex.fromNumber(tokenId, { size: 18 }) + const tokenIdHex = Hex.fromNumber(tokenId, { size: 8 }) return Hex.concat(tip20Prefix, tokenIdHex) } diff --git a/src/tempo/VirtualMaster.ts b/src/tempo/VirtualMaster.ts index 40e713d6..68ed814b 100644 --- a/src/tempo/VirtualMaster.ts +++ b/src/tempo/VirtualMaster.ts @@ -6,9 +6,9 @@ import * as Hash from '../core/Hash.js' import * as Hex from '../core/Hex.js' import * as VirtualMasterPool from './internal/virtualMasterPool.js' import * as TempoAddress from './TempoAddress.js' +import * as TokenAddress from './TokenAddress.js' import * as VirtualAddress from './VirtualAddress.js' -const tip20Prefix = '0x20c000000000000000000000' const zeroAddress = '0x0000000000000000000000000000000000000000' /** A valid salt input for TIP-1022 master registration. */ @@ -680,7 +680,7 @@ function assertValidMasterAddress(address: Address.Address) { 'Virtual master address cannot itself be a virtual address.', ) - if (normalized.startsWith(tip20Prefix)) + if (TokenAddress.isTip20(address)) throw new Errors.BaseError( 'Virtual master address cannot be a TIP-20 token address.', ) diff --git a/src/tempo/index.ts b/src/tempo/index.ts index 014e87c0..fe0e872a 100644 --- a/src/tempo/index.ts +++ b/src/tempo/index.ts @@ -283,6 +283,12 @@ export * as TempoAddress from './TempoAddress.js' * @category Reference */ export * as Tick from './Tick.js' +/** + * TIP-20 token address utilities. + * + * @category Reference + */ +export * as TokenAddress from './TokenAddress.js' /** * TIP-20 token ID utilities for converting between token IDs and addresses. *