Skip to content
Open
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

- added: Arc support (`arc`, EVM chain 5042)
- added: (EVM) `nativeErc20Interface` network option, counting ERC-20 interface transfers as native history

## 4.94.0 (2026-09-16)

- added: (Tron) Spend prebuilt `TriggerSmartContract` calls passed in `otherParams`
Expand Down
2 changes: 2 additions & 0 deletions src/ethereum/ethereumInfos.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { abstract } from './info/abstractInfo'
import { amoy } from './info/amoyInfo'
import { arbitrum } from './info/arbitrumInfo'
import { arc } from './info/arcInfo'
import { avalanche } from './info/avalancheInfo'
import { base } from './info/baseInfo'
import { binancesmartchain } from './info/binancesmartchainInfo'
Expand Down Expand Up @@ -31,6 +32,7 @@ export const ethereumPlugins = {
abstract,
amoy,
arbitrum,
arc,
avalanche,
base,
binancesmartchain,
Expand Down
12 changes: 12 additions & 0 deletions src/ethereum/ethereumTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,18 @@ export interface EthereumNetworkInfo {
nodeInterfaceAddress: string
}
disableEvmScanInternal?: boolean
/**
* A contract that exposes the native asset as an ERC-20 token at its own
* precision, such as Arc's USDC at 0x3600…0000 (native 18 decimals, the
* interface 6). A transfer through it moves the native balance without an
* external or internal value transfer, so native history must include the
* contract's Transfer events, scaled up to the native denomination.
*/
nativeErc20Interface?: {
contractAddress: string
/** One whole unit at the interface's precision, such as '1000000' */
multiplier: string
}
// Engine behavior flags (chain-specific quirks)
useRpcBalanceForMaxSpendNative?: boolean
nativeSendPrechargeWei?: string
Expand Down
161 changes: 161 additions & 0 deletions src/ethereum/info/arcInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import { EdgeCurrencyInfo, EdgeTokenMap } from 'edge-core-js/types'

import { makeOuterPlugin } from '../../common/innerPlugin'
import { createEvmTokenId, makeMetaTokens } from '../../common/tokenHelpers'
import type { EthereumTools } from '../EthereumTools'
import {
asEthereumInfoPayload,
EthereumFees,
EthereumInfoPayload,
EthereumNetworkInfo
} from '../ethereumTypes'
import {
evmCustomFeeTemplate,
evmCustomTokenTemplate,
evmMemoOptions,
makeEvmDefaultSettings
} from './ethereumCommonInfo'

// Addresses from Arc's published contract list, each confirmed against the
// contract's own name, symbol and decimals. USDC at 0x3600…0000 is not listed:
// it is the native balance seen through an ERC-20 interface, so as a token it
// would show the same money twice.
export const builtinTokens: EdgeTokenMap = {
bef5f6d51cb62b58e6a8f77868681825c6fe21c1: {
currencyCode: 'EURC',
displayName: 'EURC',
denominations: [{ name: 'EURC', multiplier: '1000000' }],
networkLocation: {
contractAddress: '0xbEf5f6d51CB62b58e6A8f77868681825C6fe21c1'
}
},
'171a4217b86a807a64eb94757db6849fb4bdbaa0': {
currencyCode: 'cirBTC',
displayName: 'Circle Wrapped Bitcoin',
denominations: [{ name: 'cirBTC', multiplier: '100000000' }],
networkLocation: {
contractAddress: '0x171A4217b86A807A64eB94757Db6849fb4bDbAA0'
}
}
}

// Fees are in USDC wei (18 decimals). The base fee holds near 20 gwei, about
// $0.0004 for a plain transfer, and most blocks tip a few gwei.
const networkFees: EthereumFees = {
default: {
baseFee: undefined,
baseFeeMultiplier: {
lowFee: '1',
standardFeeLow: '1.25',
standardFeeHigh: '1.5',
highFee: '1.75'
},
gasLimit: {
regularTransaction: '21000',
tokenTransaction: '300000',
minGasLimit: '21000'
},
gasPrice: {
lowFee: '20000000001',
standardFeeLow: '22000000001',
standardFeeHigh: '30000000001',
standardFeeLowAmount: '100000000000000000',
standardFeeHighAmount: '10000000000000000000',
highFee: '40000000001',
minGasPrice: '20000000000'
},
minPriorityFee: '1000000000'
}
}

const networkInfo: EthereumNetworkInfo = {
// Blocks arrive about every 0.5s, so this is the usual ~2 minute overlap
addressQueryLookbackBlocks: 240,
networkAdapterConfigs: [
{
// History source. Both this adapter and `evmscan` below add the native
// ERC-20 interface's transfers, which neither reports as native value.
type: 'alchemy',
servers: ['https://arc-mainnet.g.alchemy.com/v2/{{alchemyApiKey}}']
},
Comment thread
j0ntz marked this conversation as resolved.
{
type: 'rpc',
servers: [
'https://rpc.mainnet.arc.io',
'https://rpc.drpc.mainnet.arc.io',
'https://rpc.quicknode.mainnet.arc.io',
'https://arc-mainnet.g.alchemy.com/v2/{{alchemyApiKey}}'
]
},
{
// History fallback, and the only source in a build without an Alchemy
// key. Etherscan V2 serves chain 5042.
type: 'evmscan',
servers: ['https://api.etherscan.io']
}
Comment thread
j0ntz marked this conversation as resolved.
],
nativeErc20Interface: {
contractAddress: '0x3600000000000000000000000000000000000000',
multiplier: '1000000'
},
uriNetworks: ['arc'],
ercTokenStandard: 'ERC20',
chainParams: {
chainId: 5042,
name: 'Arc'
},
supportsEIP1559: true,
hdPathCoinType: 60,
pluginMnemonicKeyName: 'arcMnemonic',
pluginRegularKeyName: 'arcKey',
evmGasStationUrl: null,
networkFees
}

export const currencyInfo: EdgeCurrencyInfo = {
canReplaceByFee: true,
currencyCode: 'USDC',
evmChainId: 5042,
customFeeTemplate: evmCustomFeeTemplate,
customTokenTemplate: evmCustomTokenTemplate,
chainDisplayName: 'Arc',
assetDisplayName: 'USD Coin',
memoOptions: evmMemoOptions,
pluginId: 'arc',
walletType: 'wallet:arc',

// Explorers:
addressExplorer: 'https://arc.etherscan.io/address/%s',
transactionExplorer: 'https://arc.etherscan.io/tx/%s',

denominations: [
{
name: 'USDC',
multiplier: '1000000000000000000',
symbol: 'USDC'
}
],

usesChangeServer: true,

// Deprecated:
defaultSettings: makeEvmDefaultSettings(networkInfo),
displayName: 'Arc',
metaTokens: makeMetaTokens(builtinTokens)
}

export const arc = makeOuterPlugin<
EthereumNetworkInfo,
EthereumTools,
EthereumInfoPayload
>({
builtinTokens,
currencyInfo,
asInfoPayload: asEthereumInfoPayload,
createTokenId: createEvmTokenId,
networkInfo,

async getInnerPlugin() {
return await import('../EthereumTools')
}
})
83 changes: 76 additions & 7 deletions src/ethereum/networkAdapters/AlchemyAdapter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { add, gt, mul, sub } from 'biggystring'
import { add, div, gt, mul, sub } from 'biggystring'
import {
asArray,
asEither,
Expand All @@ -21,7 +21,7 @@ import {
EthereumNetworkUpdate,
getFeeRateUsed
} from '../EthereumNetwork'
import { EthereumTxOtherParams } from '../ethereumTypes'
import { EthereumNetworkInfo, EthereumTxOtherParams } from '../ethereumTypes'
import { resolveServerApiKey } from './apiKeyTemplate'
import { TransactionProcessingContext } from './EvmScanAdapter'
import { GetTxsParams, NetworkAdapter } from './networkAdapterTypes'
Expand Down Expand Up @@ -192,9 +192,14 @@ export class AlchemyAdapter extends NetworkAdapter<AlchemyAdapterConfig> {
contractAddress = location.contractAddress
}

// The native asset's ERC-20 interface moves native value without an
// external or internal transfer, so a native sync asks for its rows too.
const { nativeErc20Interface } = this.ethEngine.networkInfo
const nativeInterface = tokenId == null ? nativeErc20Interface : undefined

const { result, server } = await this.serialServers(async baseUrl => {
const hostname = parse(baseUrl).hostname
const [sent, received] = await Promise.all([
const [sent, received, interfaceTransfers] = await Promise.all([
this.fetchAssetTransfers(baseUrl, {
startBlock,
contractAddress,
Expand All @@ -204,7 +209,14 @@ export class AlchemyAdapter extends NetworkAdapter<AlchemyAdapterConfig> {
startBlock,
contractAddress,
toAddress: address
})
}),
nativeInterface == null
? []
: this.fetchNativeInterfaceTransfers(baseUrl, {
startBlock,
address,
nativeInterface
})
])

// A native query that kept the `internal` category through both
Expand All @@ -214,9 +226,12 @@ export class AlchemyAdapter extends NetworkAdapter<AlchemyAdapterConfig> {
// consistently external-only and the engine fetches them elsewhere.
const includesInternal =
sent.includedInternal && received.includedInternal
const transfers = [...sent.transfers, ...received.transfers].filter(
transfer => includesInternal || transfer.category !== 'internal'
)
const transfers = [
...[...sent.transfers, ...received.transfers].filter(
transfer => includesInternal || transfer.category !== 'internal'
),
...interfaceTransfers
]

// Only the wallet's own outgoing transactions need gas data:
const spendTxids = new Set<string>()
Expand Down Expand Up @@ -264,6 +279,39 @@ export class AlchemyAdapter extends NetworkAdapter<AlchemyAdapterConfig> {
}
}

/**
* Both directions of the native asset's ERC-20 interface transfers, with
* each value scaled from the interface's precision to the native one.
*/
private async fetchNativeInterfaceTransfers(
baseUrl: string,
query: {
startBlock: number
address: string
nativeInterface: NonNullable<EthereumNetworkInfo['nativeErc20Interface']>
}
): Promise<AlchemyAssetTransfer[]> {
const { startBlock, address, nativeInterface } = query
const { contractAddress } = nativeInterface
const [sent, received] = await Promise.all([
this.fetchAssetTransfers(baseUrl, {
startBlock,
contractAddress,
fromAddress: address
}),
this.fetchAssetTransfers(baseUrl, {
startBlock,
contractAddress,
toAddress: address
})
])
return scaleInterfaceTransfers(
[...sent.transfers, ...received.transfers],
this.ethEngine.currencyInfo.denominations[0].multiplier,
nativeInterface.multiplier
)
}

/**
* Pages through `alchemy_getAssetTransfers` for one direction of one asset.
* `includedInternal` reports whether the rows came from a query that kept
Expand Down Expand Up @@ -617,6 +665,27 @@ export function processAlchemyTransfers(
return edgeTransactions
}

/**
* Restates ERC-20 interface transfers of the native asset in native units,
* so they sum with the external and internal transfers of the same asset.
*/
export function scaleInterfaceTransfers(
transfers: AlchemyAssetTransfer[],
nativeMultiplier: string,
interfaceMultiplier: string
): AlchemyAssetTransfer[] {
const scale = div(nativeMultiplier, interfaceMultiplier)
return transfers.map(transfer => ({
...transfer,
rawContract: {
...transfer.rawContract,
value: decimalToHex(
mul(hexToDecimal(transfer.rawContract.value ?? '0x0'), scale)
)
}
}))
}

export type AlchemyAssetTransfer = ReturnType<typeof asAlchemyAssetTransfer>
export const asAlchemyAssetTransfer = asObject({
blockNum: asString,
Expand Down
Loading
Loading