diff --git a/.changeset/clever-parents-talk.md b/.changeset/clever-parents-talk.md new file mode 100644 index 00000000..2fc7556e --- /dev/null +++ b/.changeset/clever-parents-talk.md @@ -0,0 +1,7 @@ +--- +"@mimicprotocol/lib-ts": patch +"@mimicprotocol/cli": patch +"@mimicprotocol/test-ts": patch +--- + +Add getNativeBalance diff --git a/packages/lib-ts/as-pect.config.js b/packages/lib-ts/as-pect.config.js index bc186b7f..d41eb67e 100644 --- a/packages/lib-ts/as-pect.config.js +++ b/packages/lib-ts/as-pect.config.js @@ -37,6 +37,13 @@ export default { }, }, evm: { + _encode: (paramsPtr) => { + const paramsStr = exports.__getString(paramsPtr) + const params = JSON.parse(paramsStr)[0] + const key = `_evmDecode:${params.abiType}:${params.value}` + if (store.has(key)) return exports.__newString(store.get(key)) + throw new Error(`Encode value not found for key: ${key}`) + }, _decode: (paramsPtr) => { const paramsStr = exports.__getString(paramsPtr) const params = JSON.parse(paramsStr) @@ -117,6 +124,13 @@ export default { const key = `_evmCallQuery:${to}:${chainId}:${data}` store.set(key, result) }, + setEvmEncode: (abiTypePtr, dataPtr, encodedPtr) => { + const abiType = exports.__getString(abiTypePtr) + const data = exports.__getString(dataPtr) + const key = `_evmDecode:${abiType}:${data}` + const encoded = exports.__getString(encodedPtr) + store.set(key, encoded) + }, setEvmDecode: (abiTypePtr, hexPtr, decodedPtr) => { const abiType = exports.__getString(abiTypePtr) const hex = exports.__getString(hexPtr) diff --git a/packages/lib-ts/src/environment.ts b/packages/lib-ts/src/environment.ts index 09d9e49f..258b89f1 100644 --- a/packages/lib-ts/src/environment.ts +++ b/packages/lib-ts/src/environment.ts @@ -1,7 +1,8 @@ import { JSON } from 'json-as/assembly' import { Context, SerializableContext } from './context' -import { Consensus, ListType } from './helpers' +import { evm } from './evm' +import { Consensus, ListType, MIMIC_HELPER_ADDRESS } from './helpers' import { EvmCall, SvmCall, Swap, Transfer } from './intents' import { EvmCallQuery, @@ -19,7 +20,7 @@ import { TokenPriceQueryResponse, } from './queries' import { BlockchainToken, Token, TokenAmount, USD } from './tokens' -import { Address, ChainId, Result } from './types' +import { Address, BigInt, ChainId, EvmDecodeParam, EvmEncodeParam, Result } from './types' export namespace environment { @external('environment', '_evmCall') @@ -93,15 +94,23 @@ export namespace environment { * @param consensusFn - Optional. A function for aggregating the price values (default is median). * @returns A `Result` containing either the consensus USD price or an error string. */ - export function tokenPriceQuery(token: Token, timestamp: Date | null = null, consensusFn: (values: USD[]) => USD = Consensus.medianUSD): Result { + export function tokenPriceQuery( + token: Token, + timestamp: Date | null = null, + consensusFn: (values: USD[]) => USD = Consensus.medianUSD + ): Result { if (token.isUSD()) return Result.ok(USD.fromI32(1)) - if (!(token instanceof BlockchainToken)) return Result.err('Price query not supported for token ' + token.toString()) - - const responseStr = _tokenPriceQuery(JSON.stringify(TokenPriceQuery.fromToken(changetype(token), timestamp))) + if (!(token instanceof BlockchainToken)) { + return Result.err('Price query not supported for token ' + token.toString()) + } + + const responseStr = _tokenPriceQuery( + JSON.stringify(TokenPriceQuery.fromToken(changetype(token), timestamp)) + ) const pricesResult = TokenPriceQueryResponse.fromJson(responseStr).toResult() - + if (pricesResult.isError) return Result.err(pricesResult.error) - + const prices = pricesResult.unwrap() if (prices.length === 0) return Result.err('Prices not found for token ' + token.toString()) @@ -126,9 +135,11 @@ export namespace environment { listType: ListType = ListType.DenyList, consensusFn: (amounts: TokenBalanceQuery[][]) => TokenAmount[] = Consensus.uniqueTokenAmounts ): Result { - const responseStr = _relevantTokensQuery(JSON.stringify(RelevantTokensQuery.init(address, chainIds, usdMinAmount, tokensList, listType))) + const responseStr = _relevantTokensQuery( + JSON.stringify(RelevantTokensQuery.init(address, chainIds, usdMinAmount, tokensList, listType)) + ) const responseResult = RelevantTokensQueryResponse.fromJson(responseStr).toResult() - + if (responseResult.isError) return Result.err(responseResult.error) return Result.ok(consensusFn(responseResult.unwrap())) @@ -146,7 +157,7 @@ export namespace environment { to: Address, chainId: ChainId, data: string, - timestamp: Date | null = null, + timestamp: Date | null = null ): Result { const responseStr = _evmCallQuery(JSON.stringify(EvmCallQuery.from(to, chainId, timestamp, data))) return EvmCallQueryResponse.fromJson(responseStr).toResult() @@ -164,12 +175,12 @@ export namespace environment { chainId: ChainId, subgraphId: string, query: string, - timestamp: Date | null = null, + timestamp: Date | null = null ): Result { const responseStr = _subgraphQuery(JSON.stringify(SubgraphQuery.from(chainId, subgraphId, query, timestamp))) return SubgraphQueryResponse.fromJson(responseStr).toResult() } - + /** * Returns on-chain account info for Solana accounts. * @param publicKeys - Accounts to read from chain @@ -178,7 +189,7 @@ export namespace environment { */ export function svmAccountsInfoQuery( publicKeys: Address[], - timestamp: Date | null = null, + timestamp: Date | null = null ): Result { const responseStr = _svmAccountsInfoQuery(JSON.stringify(SvmAccountsInfoQuery.from(publicKeys, timestamp))) return SvmAccountsInfoQueryResponse.fromJson(responseStr).toResult() @@ -192,4 +203,20 @@ export namespace environment { const context = JSON.parse(_getContext()) return Context.fromSerializable(context) } + + /** + * Returns the native token balance of target account. + * @param chainId - Chain id to check balance + * @param target - Address to get balance from + * @returns The native token balance in wei + */ + export function getNativeTokenBalance(chainId: ChainId, target: Address): Result { + if (chainId === ChainId.SOLANA_MAINNET) return Result.err('Solana not supported') + const data = '0xeffd663c' + evm.encode([EvmEncodeParam.fromValue('address', target)]) + const response = evmCallQuery(Address.fromHexString(MIMIC_HELPER_ADDRESS), chainId, data) + if (response.isError) return Result.err(response.error) + const decodedResponse = evm.decode(new EvmDecodeParam('uint256', response.unwrap())) + const decoded = BigInt.fromString(decodedResponse) + return Result.ok(decoded) + } } diff --git a/packages/lib-ts/src/evm.ts b/packages/lib-ts/src/evm.ts index 88ff6595..6dcc3ba8 100644 --- a/packages/lib-ts/src/evm.ts +++ b/packages/lib-ts/src/evm.ts @@ -1,6 +1,7 @@ -import { EvmDecodeParam, EvmEncodeParam } from './types' import { JSON } from 'json-as/assembly' +import { EvmDecodeParam, EvmEncodeParam } from './types' + export namespace evm { @external('evm', '_encode') declare function _encode(params: string): string @@ -10,7 +11,7 @@ export namespace evm { @external('evm', '_keccak') declare function _keccak(params: string): string - + /** * Encodes parameters for EVM smart contract function calls using ABI encoding. * @param callParameters - Array of parameters to encode for the contract call diff --git a/packages/lib-ts/src/helpers/constants.ts b/packages/lib-ts/src/helpers/constants.ts index cbb2c369..b1d84d9c 100644 --- a/packages/lib-ts/src/helpers/constants.ts +++ b/packages/lib-ts/src/helpers/constants.ts @@ -11,3 +11,5 @@ export enum ListType { AllowList = 0, DenyList = 1, } + +export const MIMIC_HELPER_ADDRESS = '0x4766EF65d66E8D2d92F3089Ee42e5705e8817FF0' diff --git a/packages/lib-ts/src/log.ts b/packages/lib-ts/src/log.ts index e080b1c3..33172d0c 100644 --- a/packages/lib-ts/src/log.ts +++ b/packages/lib-ts/src/log.ts @@ -4,7 +4,7 @@ // Copyright (c) 2018 Graph Protocol, Inc. and contributors. // Modified by Mimic Protocol, 2025. -import { Stringable } from "./helpers" +import { Stringable } from './helpers' export namespace log { @external('log', '_log') @@ -69,7 +69,7 @@ export namespace log { function format(fmt: string, args: Array): string { let out = '' let argIndex = 0 - const argsStr = args.map(a => a.toString()) + const argsStr = args.map((a) => a.toString()) for (let i: i32 = 0, len: i32 = fmt.length; i < len; i++) { if (i < len - 1 && fmt.charCodeAt(i) == 0x7b /* '{' */ && fmt.charCodeAt(i + 1) == 0x7d /* '}' */) { if (argIndex >= argsStr.length) throw new Error('Too few arguments for format string: ' + fmt) diff --git a/packages/lib-ts/tests/environment.spec.ts b/packages/lib-ts/tests/environment.spec.ts new file mode 100644 index 00000000..b072ca78 --- /dev/null +++ b/packages/lib-ts/tests/environment.spec.ts @@ -0,0 +1,46 @@ +import { environment } from '../src/environment' +import { MIMIC_HELPER_ADDRESS } from '../src/helpers' +import { Address, ChainId } from '../src/types' + +import { randomSvmAddress, setContractCall, setEvmDecode, setEvmEncode } from './helpers' + +describe('environment', () => { + describe('getNativeTokenBalance', () => { + describe('when chainId is evm', () => { + const address = '0x9b6c444f5bbfe10680fb015e1a23bfc6193ae163' + const chainId = ChainId.OPTIMISM + + beforeEach(() => { + setEvmEncode('address', address, '0x0') + }) + + describe('when the evm call is an error', () => { + it('throws an error', () => { + const result = environment.getNativeTokenBalance(chainId, Address.fromHexString(address)) + expect(result.isError).toBe(true) + }) + }) + + describe('when evm call is successful', () => { + beforeEach(() => { + setContractCall(MIMIC_HELPER_ADDRESS, chainId, '0xeffd663c0x0', '0x100') + setEvmDecode('uint256', '0x100', '100') + }) + + it('returns the decoded value', () => { + const result = environment.getNativeTokenBalance(chainId, Address.fromHexString(address)) + expect(result.isError).toBe(false) + expect(result.unwrap().toString()).toBe('100') + }) + }) + }) + + describe('when chainId is solana', () => { + it('should throw an error', () => { + const result = environment.getNativeTokenBalance(ChainId.SOLANA_MAINNET, randomSvmAddress()) + expect(result.isError).toBe(true) + expect(result.error).toBe('Solana not supported') + }) + }) + }) +}) diff --git a/packages/lib-ts/tests/helpers.ts b/packages/lib-ts/tests/helpers.ts index 3c4ddbbe..466c848e 100644 --- a/packages/lib-ts/tests/helpers.ts +++ b/packages/lib-ts/tests/helpers.ts @@ -128,6 +128,8 @@ declare function _setFindProgramAddress(params: string, result: string): void export declare function setEvmDecode(abiType: string, hex: string, decoded: string): void +export declare function setEvmEncode(abiType: string, data: string, encoded: string): void + export declare function _setContext( timestamp: u64, consensusThreshold: u8,