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
7 changes: 7 additions & 0 deletions .changeset/funky-parrots-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@mimicprotocol/lib-ts": patch
"@mimicprotocol/cli": patch
"@mimicprotocol/test-ts": patch
---

Added Tokens class
54 changes: 54 additions & 0 deletions packages/lib-ts/src/tokens/TokenProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { ChainId } from '../types'

import { BlockchainToken } from './BlockchainToken'

/**
* Represents a token provider that can resolve to a specific BlockchainToken instance
* based on the provided chain ID.
* This allows for convenient access to common tokens across different chains.
* Supports both ERC20Token (EVM chains) and SPLToken (Solana).
*/
export class TokenProvider {
private _symbol: string
private _registry: Map<ChainId, BlockchainToken>

/**
* Creates a new TokenProvider instance.
* @param symbol - The token symbol (e.g., "USDC", "USDT")
*/
constructor(symbol: string) {
this._symbol = symbol
this._registry = new Map<ChainId, BlockchainToken>()
}

/**
* Registers a token for a specific chain.
* @param chainId - The chain ID to register the token for
* @param token - The BlockchainToken instance for this chain (ERC20Token or SPLToken)
* @returns The TokenProvider instance for method chaining
*/
register(chainId: ChainId, token: BlockchainToken): TokenProvider {
this._registry.set(chainId, token)
return this
}

/**
* Resolves the token provider to a specific BlockchainToken instance for the given chain.
* @param chainId - The chain ID to resolve the token for
* @returns The BlockchainToken instance for the specified chain
* @throws Error if the token is not supported on the requested chain
*/
on(chainId: i32): BlockchainToken {
if (!this.isSupported(chainId)) throw new Error(`Token ${this._symbol} is not registered on chain ${chainId}`)
return this._registry.get(chainId)
}

/**
* Checks if the token is supported on the given chain.
* @param chainId - The chain ID to check
* @returns True if the token is supported on the chain, false otherwise
*/
isSupported(chainId: i32): bool {
return this._registry.has(chainId)
}
}
129 changes: 129 additions & 0 deletions packages/lib-ts/src/tokens/Tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { Arbitrum, Base, BaseSepolia, Ethereum, Gnosis, Optimism, Sonic } from '../chains'

import { TokenProvider } from './TokenProvider'

/**
* Token providers that can resolve to chain-specific BlockchainToken instances.
* Use the `.on(chainId)` method to get the token for a specific chain.
*
* @example
* ```typescript
* import { Tokens, Ethereum } from '@mimicprotocol/lib-ts'
*
* const usdc = Tokens.USDC.on(inputs.chainId)
* const weth = Tokens.WETH.on(Ethereum.CHAIN_ID)
* ```
*/
export class Tokens {
private static _instance: Tokens | null = null

private readonly usdc: TokenProvider = new TokenProvider('USDC')
private readonly usdt: TokenProvider = new TokenProvider('USDT')
private readonly dai: TokenProvider = new TokenProvider('DAI')
private readonly wbtc: TokenProvider = new TokenProvider('WBTC')
private readonly weth: TokenProvider = new TokenProvider('WETH')
private readonly eth: TokenProvider = new TokenProvider('ETH')
private readonly xdai: TokenProvider = new TokenProvider('xDAI')
private readonly sonic: TokenProvider = new TokenProvider('SONIC')
private readonly wxdai: TokenProvider = new TokenProvider('WXDAI')
private readonly wsonic: TokenProvider = new TokenProvider('WSONIC')

private constructor() {
// Ethereum
this.usdc.register(Ethereum.CHAIN_ID, Ethereum.USDC)
this.usdt.register(Ethereum.CHAIN_ID, Ethereum.USDT)
this.dai.register(Ethereum.CHAIN_ID, Ethereum.DAI)
this.wbtc.register(Ethereum.CHAIN_ID, Ethereum.WBTC)
this.weth.register(Ethereum.CHAIN_ID, Ethereum.WETH)
this.eth.register(Ethereum.CHAIN_ID, Ethereum.ETH)

// Arbitrum
this.usdc.register(Arbitrum.CHAIN_ID, Arbitrum.USDC)
this.usdt.register(Arbitrum.CHAIN_ID, Arbitrum.USDT)
this.dai.register(Arbitrum.CHAIN_ID, Arbitrum.DAI)
this.wbtc.register(Arbitrum.CHAIN_ID, Arbitrum.WBTC)
this.weth.register(Arbitrum.CHAIN_ID, Arbitrum.WETH)
this.eth.register(Arbitrum.CHAIN_ID, Arbitrum.ETH)

// Base
this.usdc.register(Base.CHAIN_ID, Base.USDC)
this.usdt.register(Base.CHAIN_ID, Base.USDT)
this.dai.register(Base.CHAIN_ID, Base.DAI)
this.wbtc.register(Base.CHAIN_ID, Base.WBTC)
this.weth.register(Base.CHAIN_ID, Base.WETH)
this.eth.register(Base.CHAIN_ID, Base.ETH)

// Optimism
this.usdc.register(Optimism.CHAIN_ID, Optimism.USDC)
this.usdt.register(Optimism.CHAIN_ID, Optimism.USDT)
this.dai.register(Optimism.CHAIN_ID, Optimism.DAI)
this.wbtc.register(Optimism.CHAIN_ID, Optimism.WBTC)
this.weth.register(Optimism.CHAIN_ID, Optimism.WETH)
this.eth.register(Optimism.CHAIN_ID, Optimism.ETH)

// Gnosis
this.usdc.register(Gnosis.CHAIN_ID, Gnosis.USDC)
this.usdt.register(Gnosis.CHAIN_ID, Gnosis.USDT)
this.wbtc.register(Gnosis.CHAIN_ID, Gnosis.WBTC)
this.weth.register(Gnosis.CHAIN_ID, Gnosis.WETH)
this.xdai.register(Gnosis.CHAIN_ID, Gnosis.xDAI)
this.wxdai.register(Gnosis.CHAIN_ID, Gnosis.WXDAI)

// Sonic
this.usdc.register(Sonic.CHAIN_ID, Sonic.USDC)
this.usdt.register(Sonic.CHAIN_ID, Sonic.USDT)
this.weth.register(Sonic.CHAIN_ID, Sonic.WETH)
this.sonic.register(Sonic.CHAIN_ID, Sonic.SONIC)
this.wsonic.register(Sonic.CHAIN_ID, Sonic.WSONIC)

// BaseSepolia
this.eth.register(BaseSepolia.CHAIN_ID, BaseSepolia.ETH)
}

private static getInstance(): Tokens {
if (Tokens._instance === null) {
Tokens._instance = new Tokens()
}
return Tokens._instance!
}

static get USDC(): TokenProvider {
return Tokens.getInstance().usdc
}

static get USDT(): TokenProvider {
return Tokens.getInstance().usdt
}

static get DAI(): TokenProvider {
return Tokens.getInstance().dai
}

static get WBTC(): TokenProvider {
return Tokens.getInstance().wbtc
}

static get WETH(): TokenProvider {
return Tokens.getInstance().weth
}

static get ETH(): TokenProvider {
return Tokens.getInstance().eth
}

static get XDAI(): TokenProvider {
return Tokens.getInstance().xdai
}

static get SONIC(): TokenProvider {
return Tokens.getInstance().sonic
}

static get WXDAI(): TokenProvider {
return Tokens.getInstance().wxdai
}

static get WSONIC(): TokenProvider {
return Tokens.getInstance().wsonic
}
}
2 changes: 2 additions & 0 deletions packages/lib-ts/src/tokens/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ export * from './ERC20Token'
export * from './SPLToken'
export * from './Token'
export * from './TokenAmount'
export * from './TokenProvider'
export * from './Tokens'
export * from './USD'
74 changes: 74 additions & 0 deletions packages/lib-ts/tests/tokens/TokenProvider.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { ERC20Token, TokenProvider } from '../../src/tokens'
import { ChainId } from '../../src/types'

describe('TokenProvider', () => {
describe('register', () => {
describe('when registering a token for a chain', () => {
it('should register the token successfully', () => {
const provider = new TokenProvider('TEST')
const token = ERC20Token.native(ChainId.ETHEREUM)
provider.register(ChainId.ETHEREUM, token)

expect(provider.isSupported(ChainId.ETHEREUM)).toBe(true)
})

it('should allow method chaining', () => {
const provider = new TokenProvider('TEST')
const token1 = ERC20Token.native(ChainId.ETHEREUM)
const token2 = ERC20Token.native(ChainId.ARBITRUM)

provider.register(ChainId.ETHEREUM, token1).register(ChainId.ARBITRUM, token2)

expect(provider.isSupported(ChainId.ETHEREUM)).toBe(true)
expect(provider.isSupported(ChainId.ARBITRUM)).toBe(true)
})
})
})

describe('on', () => {
describe('when token is registered for the chain', () => {
it('should return the registered token', () => {
const provider = new TokenProvider('TEST')
const token = ERC20Token.native(ChainId.ETHEREUM)
provider.register(ChainId.ETHEREUM, token)

const resolved = provider.on(ChainId.ETHEREUM)

expect(resolved.address.toHexString()).toBe(token.address.toHexString())
expect(resolved.chainId).toBe(ChainId.ETHEREUM)
expect(resolved.symbol).toBe(token.symbol)
expect(resolved.decimals).toBe(token.decimals)
})
})

describe('when token is not registered for the chain', () => {
it('should throw an error', () => {
expect(() => {
const provider = new TokenProvider('TEST')
provider.on(ChainId.ETHEREUM)
}).toThrow()
})
})
})

describe('isSupported', () => {
describe('when token is registered for the chain', () => {
it('should return true', () => {
const provider = new TokenProvider('TEST')
const token = ERC20Token.native(ChainId.ETHEREUM)
provider.register(ChainId.ETHEREUM, token)

expect(provider.isSupported(ChainId.ETHEREUM)).toBe(true)
})
})

describe('when token is not registered for the chain', () => {
it('should return false', () => {
const provider = new TokenProvider('TEST')

expect(provider.isSupported(ChainId.ETHEREUM)).toBe(false)
expect(provider.isSupported(ChainId.ARBITRUM)).toBe(false)
})
})
})
})
65 changes: 65 additions & 0 deletions packages/lib-ts/tests/tokens/Tokens.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Arbitrum, Base, BaseSepolia, Ethereum, Gnosis, Optimism, Sonic } from '../../src/chains'
import { BlockchainToken, Tokens } from '../../src/tokens'

function expectTokenMatches(resolved: BlockchainToken, expected: BlockchainToken): void {
expect(resolved.chainId).toBe(expected.chainId)
expect(resolved.symbol).toBe(expected.symbol)
expect(resolved.address.toHexString()).toBe(expected.address.toHexString())
expect(resolved.decimals).toBe(expected.decimals)
}

describe('Tokens', () => {
describe('USDC', () => {
describe('when accessing the static getter', () => {
it('should return a TokenProvider instance', () => {
const provider = Tokens.USDC

expect(provider).not.toBeNull()
expect(provider.isSupported(Ethereum.CHAIN_ID)).toBe(true)
expect(provider.isSupported(BaseSepolia.CHAIN_ID)).toBe(false)
})
})

describe('when resolving token for a chain', () => {
describe('when token is supported on chain', () => {
it('should return token for Ethereum', () => {
const token = Tokens.USDC.on(Ethereum.CHAIN_ID)
expectTokenMatches(token, Ethereum.USDC)
})

it('should return token for Arbitrum', () => {
const token = Tokens.USDC.on(Arbitrum.CHAIN_ID)
expectTokenMatches(token, Arbitrum.USDC)
})

it('should return token for Base', () => {
const token = Tokens.USDC.on(Base.CHAIN_ID)
expectTokenMatches(token, Base.USDC)
})

it('should return token for Optimism', () => {
const token = Tokens.USDC.on(Optimism.CHAIN_ID)
expectTokenMatches(token, Optimism.USDC)
})

it('should return token for Gnosis', () => {
const token = Tokens.USDC.on(Gnosis.CHAIN_ID)
expectTokenMatches(token, Gnosis.USDC)
})

it('should return token for Sonic', () => {
const token = Tokens.USDC.on(Sonic.CHAIN_ID)
expectTokenMatches(token, Sonic.USDC)
})
})

describe('when token is not supported on chain', () => {
it('should throw an error', () => {
expect(() => {
Tokens.USDC.on(BaseSepolia.CHAIN_ID)
}).toThrow()
})
})
})
})
})