diff --git a/bitcore-test.config.json b/bitcore-test.config.json index a7c2793b048..166f24c8590 100644 --- a/bitcore-test.config.json +++ b/bitcore-test.config.json @@ -103,6 +103,22 @@ "threads": 0 } }, + "ARC": { + "testnet": { + "chainSource": "external", + "module": "./moralis", + "trustedPeers": [], + "providers": [ + { + "host": "rpc.testnet.arc.network", + "protocol": "https", + "port": "", + "dataType": "combined" + } + ], + "threads": 0 + } + }, "BASE": { "sepolia": { "chainSource": "external", diff --git a/packages/bitcore-client/src/wallet.ts b/packages/bitcore-client/src/wallet.ts index d06e28b9cd6..281540908c7 100644 --- a/packages/bitcore-client/src/wallet.ts +++ b/packages/bitcore-client/src/wallet.ts @@ -37,6 +37,7 @@ const chainLibs = { ARB: { Web3, ethers }, BASE: { Web3, ethers }, OP: { Web3, ethers }, + ARC: { Web3, ethers }, XRP: xrpl, SOL: { SolKit, SolanaProgram } }; @@ -303,7 +304,7 @@ export class Wallet { * @returns {Boolean} */ isEvmChain() { - return ['ETH', 'MATIC', 'ARB', 'OP', 'BASE'].includes(this.chain?.toUpperCase()); + return ['ETH', 'MATIC', 'ARB', 'OP', 'BASE', 'ARC'].includes(this.chain?.toUpperCase()); } isSolanaChain() { diff --git a/packages/bitcore-client/test/unit/wallet.arc.test.ts b/packages/bitcore-client/test/unit/wallet.arc.test.ts new file mode 100644 index 00000000000..9dd6b4d77ad --- /dev/null +++ b/packages/bitcore-client/test/unit/wallet.arc.test.ts @@ -0,0 +1,10 @@ +import { expect } from 'chai'; +import { Wallet } from '../../src/wallet'; + +describe('Wallet ARC support', function() { + it('should treat ARC as an EVM chain', function() { + const arcWallet = Object.assign(Object.create(Wallet.prototype), { chain: 'ARC' }) as Wallet; + expect(arcWallet.isEvmChain()).to.equal(true); + expect(arcWallet.getLib()).to.have.keys(['Web3', 'ethers']); + }); +}); diff --git a/packages/crypto-wallet-core/src/constants/chains.ts b/packages/crypto-wallet-core/src/constants/chains.ts index e46ec19cca2..356f664e7b4 100644 --- a/packages/crypto-wallet-core/src/constants/chains.ts +++ b/packages/crypto-wallet-core/src/constants/chains.ts @@ -1,6 +1,6 @@ export const UTXO_CHAINS = ['btc', 'bch', 'doge', 'ltc']; -export const EVM_CHAINS = ['eth', 'matic', 'arb', 'base', 'op']; +export const EVM_CHAINS = ['eth', 'matic', 'arb', 'base', 'op', 'arc']; export const SVM_CHAINS = ['sol']; export const RIPPLE_CHAINS = ['xrp']; export const CHAINS = [...UTXO_CHAINS, ...EVM_CHAINS, ...SVM_CHAINS, ...RIPPLE_CHAINS]; @@ -12,7 +12,8 @@ export const EVM_CHAIN_DEFAULT_TESTNET = { MATIC: 'amoy', ARB: 'sepolia', BASE: 'sepolia', - OP: 'sepolia' + OP: 'sepolia', + ARC: 'testnet' }; export const EVM_CHAIN_NETWORK_TO_CHAIN_ID = { @@ -22,6 +23,7 @@ export const EVM_CHAIN_NETWORK_TO_CHAIN_ID = { ARB_mainnet: 42161, BASE_mainnet: 8453, OP_mainnet: 10, + ARC_mainnet: 'unsupported', // ETH testnets ETH_holesky: 17000, ETH_sepolia: 11155111, @@ -41,6 +43,8 @@ export const EVM_CHAIN_NETWORK_TO_CHAIN_ID = { // OP testnets OP_sepolia: 11155420, OP_goerli: 28528, + // ARC testnets + ARC_testnet: 5042002, // Regtests ETH_regtest: 1337, MATIC_regtest: 13375, diff --git a/packages/crypto-wallet-core/src/constants/units.ts b/packages/crypto-wallet-core/src/constants/units.ts index a39256b5103..f8205b449a1 100644 --- a/packages/crypto-wallet-core/src/constants/units.ts +++ b/packages/crypto-wallet-core/src/constants/units.ts @@ -87,6 +87,17 @@ export const UNITS = { minDecimals: 2 } }, + arc: { + toSatoshis: 1e18, + full: { + maxDecimals: 6, + minDecimals: 6 + }, + short: { + maxDecimals: 6, + minDecimals: 2 + } + }, xrp: { toSatoshis: 1e6, full: { diff --git a/packages/crypto-wallet-core/src/derivation/index.ts b/packages/crypto-wallet-core/src/derivation/index.ts index 6994763a6cf..4a2742af3ff 100644 --- a/packages/crypto-wallet-core/src/derivation/index.ts +++ b/packages/crypto-wallet-core/src/derivation/index.ts @@ -23,6 +23,7 @@ const derivers: { [chain: string]: IDeriver } = { ARB: new ArbDeriver(), BASE: new BaseDeriver(), OP: new OpDeriver(), + ARC: new EthDeriver(), SOL: new SolDeriver() }; diff --git a/packages/crypto-wallet-core/src/derivation/paths.ts b/packages/crypto-wallet-core/src/derivation/paths.ts index 3183ef8261f..4cc8cdb99af 100644 --- a/packages/crypto-wallet-core/src/derivation/paths.ts +++ b/packages/crypto-wallet-core/src/derivation/paths.ts @@ -33,6 +33,9 @@ export const Paths = { BASE: { default: "m/44'/60'/", }, + ARC: { + default: "m/44'/60'/", + }, SOL: { default: "m/44'/501'/", }, diff --git a/packages/crypto-wallet-core/src/transactions/index.ts b/packages/crypto-wallet-core/src/transactions/index.ts index e1364c908cb..47fcbe0076f 100644 --- a/packages/crypto-wallet-core/src/transactions/index.ts +++ b/packages/crypto-wallet-core/src/transactions/index.ts @@ -41,6 +41,8 @@ const providers = { BASEERC20: new BASEERC20TxProvider(), OP: new OPTxProvider(), OPERC20: new OPERC20TxProvider(), + ARC: new ETHTxProvider('ARC'), + ARCERC20: new ERC20TxProvider('ARC'), SOL: new SOLTxProvider(), SOLSPL: new SPLTxProvider(), }; diff --git a/packages/crypto-wallet-core/src/validation/arc/index.ts b/packages/crypto-wallet-core/src/validation/arc/index.ts new file mode 100644 index 00000000000..0f2af0fea5e --- /dev/null +++ b/packages/crypto-wallet-core/src/validation/arc/index.ts @@ -0,0 +1,8 @@ +import { EthValidation } from '../eth'; + +export class ArcValidation extends EthValidation { + constructor() { + super(); + this.regex = /arc/i; + } +} diff --git a/packages/crypto-wallet-core/src/validation/index.ts b/packages/crypto-wallet-core/src/validation/index.ts index cd7a84a7b50..f43dddaf37e 100644 --- a/packages/crypto-wallet-core/src/validation/index.ts +++ b/packages/crypto-wallet-core/src/validation/index.ts @@ -1,4 +1,5 @@ import { ArbValidation } from './arb'; +import { ArcValidation } from './arc'; import { BaseValidation } from './base'; import { BchValidation } from './bch'; import { BtcValidation } from './btc'; @@ -22,6 +23,7 @@ const validation: { [chain: string]: IValidation } = { ARB: new ArbValidation(), BASE: new BaseValidation(), OP: new OpValidation(), + ARC: new ArcValidation(), SOL: new SolValidation(), }; diff --git a/packages/crypto-wallet-core/test/address.test.ts b/packages/crypto-wallet-core/test/address.test.ts index 163ef7c1bdb..23c4ea512dc 100644 --- a/packages/crypto-wallet-core/test/address.test.ts +++ b/packages/crypto-wallet-core/test/address.test.ts @@ -88,6 +88,18 @@ describe('Address Derivation', () => { expect(address).to.equal(expectedAddress); }); + it('should be able to generate a valid ARC address', () => { + const xPub = 'xpub6D8rChqkgFuaZULuq2n6VrS4zB5Cmv24gcRc889dFRRgYAH1CGQmQZ9kcPfMAfWGPnyMd1X5foBYFmJ5ZPfvwhm6tXjaY13ao1rQHRtkKDv'; + // 'select scout crash enforce riot rival spring whale hollow radar rule sentence'; + + const path = Deriver.pathFor('ARC', 'testnet'); + expect(path).to.equal("m/44'/60'/0'"); + + const address = Deriver.deriveAddress('ARC', 'testnet', xPub, 0, false); + const expectedAddress = '0x9dbfE221A6EEa27a0e2f52961B339e95426931F9'; + expect(address).to.equal(expectedAddress); + }); + it('should be able to generate a valid ETH address, privKey, pubKey', () => { const privKey = 'xprv9ypBjKErGMqCdzd44hfSdy1Vk6PGtU3si8ogZcow7rA23HTxMi9XfT99EKmiNdLMr9BAZ9S8ZKCYfN1eCmzYSmXYHje1jnYQseV1VJDDfdS'; diff --git a/packages/crypto-wallet-core/test/transactions.test.ts b/packages/crypto-wallet-core/test/transactions.test.ts index e78671d005e..deec0a30475 100644 --- a/packages/crypto-wallet-core/test/transactions.test.ts +++ b/packages/crypto-wallet-core/test/transactions.test.ts @@ -4,7 +4,7 @@ import bitcoreLib from '@bitpay-labs/bitcore-lib'; import bitcoreLibCash from '@bitpay-labs/bitcore-lib-cash'; import bitcoreLibDoge from '@bitpay-labs/bitcore-lib-doge'; import bitcoreLibLtc from '@bitpay-labs/bitcore-lib-ltc'; -import { Transactions } from '../src'; +import { Constants, Transactions } from '../src'; describe('Transaction', function() { describe('create', () => { @@ -1751,6 +1751,59 @@ describe('Transaction', function() { }); }); + describe('ARC EVM support', () => { + it('should get the correct testnet chainId', () => { + const testnetId = Transactions.get({ chain: 'ARC' }).getChainId('testnet'); + expect(testnetId).to.equal(5042002); + }); + + it('should create an ARC native transaction using 18-decimal base units', () => { + const tx = Transactions.create({ + chain: 'ARC', + recipients: [{ address: '0x37d7B3bBD88EFdE6a93cF74D2F5b0385D3E3B08A', amount: '1000000000000000000' }], + nonce: 0, + gasPrice: 1, + gasLimit: 21000, + network: 'testnet', + data: '0x' + }); + const parsed = ethers.Transaction.from(tx); + expect(parsed.chainId).to.equal(5042002n); + expect(parsed.value).to.equal(1000000000000000000n); + }); + + it('should create an ARC ERC20 transaction using ARC testnet chainId', () => { + const tokenAddress = '0x3600000000000000000000000000000000000000'; + const recipient = '0x37d7B3bBD88EFdE6a93cF74D2F5b0385D3E3B08A'; + const amount = '1000000'; + const tx = Transactions.create({ + chain: 'ARCERC20', + recipients: [{ address: recipient, amount }], + nonce: 0, + gasPrice: 1, + gasLimit: 200000, + network: 'testnet', + tokenAddress + }); + const parsed = ethers.Transaction.from(tx); + + expect(parsed.chainId).to.equal(5042002n); + expect(parsed.to).to.equal(ethers.getAddress(tokenAddress)); + expect(parsed.value).to.equal(0n); + expect(parsed.data).to.equal( + '0xa9059cbb' + + '00000000000000000000000037d7b3bbd88efde6a93cf74d2f5b0385d3e3b08a' + + '00000000000000000000000000000000000000000000000000000000000f4240' + ); + }); + + it('should define ARC native units as 18 decimals', () => { + expect(Constants.UNITS.arc.toSatoshis).to.equal(1e18); + expect(Constants.UNITS.arc.full.maxDecimals).to.equal(6); + expect(Constants.UNITS.arc.full.minDecimals).to.equal(6); + }); + }); + describe('ETH _toHex', function() { it('should convert number to hex string', function() { const ETHTxProvider = Transactions.get({ chain: 'ETH' }); diff --git a/packages/crypto-wallet-core/test/validation.test.ts b/packages/crypto-wallet-core/test/validation.test.ts index f2fdf801d9e..d73d14be4a4 100644 --- a/packages/crypto-wallet-core/test/validation.test.ts +++ b/packages/crypto-wallet-core/test/validation.test.ts @@ -239,6 +239,27 @@ describe('Address Validation', () => { expect(inValidMaticPrefix).to.equal(false); }); + it('should validate ARC addresses', async () => { + const address = '37d7B3bBD88EFdE6a93cF74D2F5b0385D3E3B08A'; + const prefixedAddress = `0x${address}`; + + expect(await Validation.validateAddress('ARC', 'testnet', address)).to.equal(true); + expect(await Validation.validateAddress('ARC', 'testnet', prefixedAddress)).to.equal(true); + expect(await Validation.validateAddress('ARC', 'testnet', address.slice(1))).to.equal(false); + }); + + it('should validate ARC URIs', async () => { + const address = '0x37d7B3bBD88EFdE6a93cF74D2F5b0385D3E3B08A'; + const uri = `arc:${address}`; + const uriParams = `${uri}?value=123&gasPrice=123&gas=123&gasLimit=123`; + + expect(await Validation.validateUri('ARC', uri)).to.equal(true); + expect(await Validation.validateUri('ARC', uriParams)).to.equal(true); + expect(await Validation.validateUri('ARC', `${uri}?value=123`)).to.equal(true); + expect(await Validation.validateUri('ARC', address)).to.equal(false); + expect(await Validation.validateUri('ARC', `${uri}?value=invalid&gasLimit=123&gas=123`)).to.equal(false); + }); + it('should be able to validate a SOL address', async () => { const isValidAddress = await Validation.validateAddress('SOL', 'mainnet', solAddress); expect(isValidAddress).to.equal(true);