Skip to content

Commit 8d9195d

Browse files
Add chain registry, EIP-1559/EIP-712, and dynamic EVM/UTXO support (#38)
Introduce ChainRegistry with EvmChainRegistration and UtxoChainRegistration plus CustomBitcoinStyleNetParams and ChainCatalogLoader for JSON bulk registration. ChainType.validate and WalletManager mnemonic import routes use the registry; AddressCreatorManager resolves registered EVM and UTXO chains. Identity derives registered EVM/UTXO wallets from default paths. Add Eip1559Transaction (type-2 raw tx), Eip712Hasher and TypedDataSigner, and EthereumSign.signDigest for reuse. Document global chain strategy in README and add unit tests for registry, BIP44 helpers, EIP-1559, and EIP-712. Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Galaxy <GalaxySciTech@users.noreply.github.com>
1 parent 20d3231 commit 8d9195d

21 files changed

Lines changed: 1250 additions & 55 deletions

README.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,34 @@ Tokencore is a Java multi-chain wallet core library for exchange backends, custo
1010
- Offline transaction signing for major chain families
1111

1212
Supported chains include:
13-
- **EVM**: Ethereum
14-
- **Bitcoin family**: Bitcoin, Litecoin, Dogecoin, Dash, Bitcoin Cash, Bitcoin SV
13+
- **EVM**: Ethereum (built-in), plus **any EVM chain** you register at runtime (`chainId`, default BIP44 path)
14+
- **Bitcoin family**: Bitcoin, Litecoin, Dogecoin, Dash, Bitcoin Cash, Bitcoin SV, plus **bitcoin-style UTXO chains** registered with address/BIP32 headers
1515
- **Others**: TRON, Filecoin, EOS
1616

17+
### Global chain support (registry + catalog)
18+
19+
Tokencore does not ship an authoritative list of every network in the world. Instead it provides:
20+
21+
- **`ChainRegistry`**: register EVM chains (`EvmChainRegistration`) and bitcoin-style UTXO chains (`UtxoChainRegistration` + `CustomBitcoinStyleNetParams`).
22+
- **`ChainCatalogLoader`**: bulk-register from a JSON array (e.g. app-shipped file or data filtered from [chainlist](https://chainlist.org)).
23+
- **Wallet identity**: multiple EVM chains share the same address for the same key; wallets are keyed by **`(chainType, address)`** — use a **unique `chainType` string per chain** (e.g. `POLYGON`, `ARBITRUM_ONE`).
24+
25+
```java
26+
import org.consenlabs.tokencore.wallet.chain.*;
27+
import org.consenlabs.tokencore.wallet.model.BIP44Util;
28+
29+
// Single EVM L2
30+
ChainRegistry.getInstance().registerEvm(
31+
new EvmChainRegistration("MYL2", 84532L, BIP44Util.defaultEvmAccountZeroPath(60)));
32+
33+
// Bulk from JSON: [ { "chainType": "...", "family": "EVM", "chainId": 1, "slip44": 60 } ]
34+
ChainCatalogLoader.registerAllFromJson("[...]");
35+
```
36+
37+
**EIP-1559 (type-2)**: use `org.consenlabs.tokencore.wallet.transaction.Eip1559Transaction` (raw tx = `0x02 || rlp(...)`).
38+
39+
**EIP-712**: `Eip712Hasher.hashTypedDataV4(JsonNode)` and `TypedDataSigner.signTypedDataV4(JsonNode, privateKeyBytes)`.
40+
1741
---
1842

1943
## Core Features (Recommended Minimum)

src/main/java/org/consenlabs/tokencore/wallet/Identity.java

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
import org.consenlabs.tokencore.foundation.utils.ByteUtil;
1414
import org.consenlabs.tokencore.foundation.utils.MnemonicUtil;
1515
import org.consenlabs.tokencore.foundation.utils.NumericUtil;
16+
import org.consenlabs.tokencore.wallet.chain.ChainFamily;
17+
import org.consenlabs.tokencore.wallet.chain.ChainRegistry;
18+
import org.consenlabs.tokencore.wallet.chain.EvmChainRegistration;
19+
import org.consenlabs.tokencore.wallet.chain.UtxoChainRegistration;
1620
import org.consenlabs.tokencore.wallet.keystore.*;
1721
import org.consenlabs.tokencore.wallet.model.*;
1822
import org.consenlabs.tokencore.wallet.transaction.EthereumSign;
@@ -165,35 +169,21 @@ public List<Wallet> deriveWalletsByMnemonics(List<String> chainTypes, String pas
165169
List<Wallet> wallets = new ArrayList<>();
166170
for (String chainType : chainTypes) {
167171
Wallet wallet;
168-
switch (chainType) {
169-
case ChainType.BITCOIN:
170-
wallet = deriveBitcoinWallet(mnemonics, password, this.getMetadata().getSegWit());
172+
ChainFamily family = ChainRegistry.getInstance().resolveFamily(chainType);
173+
switch (family) {
174+
case BITCOIN_STYLE_UTXO:
175+
wallet = deriveUtxoWalletByChainType(chainType, mnemonics, password, this.getMetadata().getSegWit());
171176
break;
172-
case ChainType.ETHEREUM:
173-
wallet = deriveEthereumWallet(mnemonics, password);
177+
case EVM:
178+
wallet = deriveEvmWalletByChainType(chainType, mnemonics, password);
174179
break;
175-
case ChainType.LITECOIN:
176-
wallet = deriveLitecoinWallet(mnemonics, password, this.getMetadata().getSegWit());
177-
break;
178-
case ChainType.DOGECOIN:
179-
wallet = deriveDogecoinWallet(mnemonics, password, this.getMetadata().getSegWit());
180-
break;
181-
case ChainType.DASH:
182-
wallet = deriveDashWallet(mnemonics, password, this.getMetadata().getSegWit());
183-
break;
184-
case ChainType.BITCOINSV:
185-
wallet = deriveBitcoinSVWallet(mnemonics, password, this.getMetadata().getSegWit());
186-
break;
187-
case ChainType.BITCOINCASH:
188-
wallet = deriveBitcoinCASHWallet(mnemonics, password, this.getMetadata().getSegWit());
189-
break;
190-
case ChainType.EOS:
180+
case EOS:
191181
wallet = deriveEOSWallet(mnemonics, password);
192182
break;
193-
case ChainType.TRON:
183+
case TRON:
194184
wallet = deriveTronWallet(mnemonics, password);
195185
break;
196-
case ChainType.FILECOIN:
186+
case FILECOIN:
197187
wallet = deriveFilecoinWallet(mnemonics, password);
198188
break;
199189
default:
@@ -206,6 +196,54 @@ public List<Wallet> deriveWalletsByMnemonics(List<String> chainTypes, String pas
206196
return wallets;
207197
}
208198

199+
private Wallet deriveEvmWalletByChainType(String chainType, List<String> mnemonics, String password) {
200+
EvmChainRegistration reg = ChainRegistry.getInstance().getEvmRegistration(chainType);
201+
if (reg == null) {
202+
throw new TokenException(String.format("Doesn't support deriving %s wallet", chainType));
203+
}
204+
Metadata walletMetadata = new Metadata();
205+
walletMetadata.setChainType(reg.getChainType());
206+
walletMetadata.setPasswordHint(this.getMetadata().getPasswordHint());
207+
walletMetadata.setSource(this.getMetadata().getSource());
208+
walletMetadata.setName(reg.getChainType());
209+
IMTKeystore keystore = V3MnemonicKeystore.create(walletMetadata, password, mnemonics, reg.getDefaultMnemonicPath());
210+
return WalletManager.createWallet(keystore);
211+
}
212+
213+
private Wallet deriveUtxoWalletByChainType(String chainType, List<String> mnemonics, String password, String segWit) {
214+
if (ChainType.BITCOIN.equalsIgnoreCase(chainType)) {
215+
return deriveBitcoinWallet(mnemonics, password, segWit);
216+
}
217+
if (ChainType.LITECOIN.equalsIgnoreCase(chainType)) {
218+
return deriveLitecoinWallet(mnemonics, password, segWit);
219+
}
220+
if (ChainType.DOGECOIN.equalsIgnoreCase(chainType)) {
221+
return deriveDogecoinWallet(mnemonics, password, segWit);
222+
}
223+
if (ChainType.DASH.equalsIgnoreCase(chainType)) {
224+
return deriveDashWallet(mnemonics, password, segWit);
225+
}
226+
if (ChainType.BITCOINSV.equalsIgnoreCase(chainType)) {
227+
return deriveBitcoinSVWallet(mnemonics, password, segWit);
228+
}
229+
if (ChainType.BITCOINCASH.equalsIgnoreCase(chainType)) {
230+
return deriveBitcoinCASHWallet(mnemonics, password, segWit);
231+
}
232+
UtxoChainRegistration reg = ChainRegistry.getInstance().getUtxoRegistration(chainType);
233+
if (reg == null) {
234+
throw new TokenException(String.format("Doesn't support deriving %s wallet", chainType));
235+
}
236+
Metadata walletMetadata = new Metadata();
237+
walletMetadata.setChainType(reg.getChainType());
238+
walletMetadata.setPasswordHint(this.getMetadata().getPasswordHint());
239+
walletMetadata.setSource(this.getMetadata().getSource());
240+
walletMetadata.setNetwork(this.getMetadata().getNetwork());
241+
walletMetadata.setName(reg.getChainType());
242+
walletMetadata.setSegWit(segWit);
243+
IMTKeystore keystore = HDMnemonicKeystore.create(walletMetadata, password, mnemonics, reg.getDefaultMnemonicPath());
244+
return WalletManager.createWallet(keystore);
245+
}
246+
209247

210248
private static Identity tryLoadFromFile() {
211249
try {

src/main/java/org/consenlabs/tokencore/wallet/WalletManager.java

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import org.consenlabs.tokencore.foundation.utils.NumericUtil;
1515
import org.consenlabs.tokencore.wallet.address.AddressCreatorManager;
1616
import org.consenlabs.tokencore.wallet.address.EthereumAddressCreator;
17+
import org.consenlabs.tokencore.wallet.chain.ChainFamily;
18+
import org.consenlabs.tokencore.wallet.chain.ChainRegistry;
1719
import org.consenlabs.tokencore.wallet.keystore.*;
1820
import org.consenlabs.tokencore.wallet.model.*;
1921
import org.consenlabs.tokencore.wallet.validators.PrivateKeyValidator;
@@ -165,21 +167,17 @@ public static Wallet importWalletFromMnemonic(Metadata metadata, @Nullable Strin
165167
IMTKeystore keystore = null;
166168
List<String> mnemonicCodes = MnemonicUtil.toMnemonicCodes(mnemonic);
167169
MnemonicUtil.validateMnemonics(mnemonicCodes);
168-
switch (metadata.getChainType()) {
169-
case ChainType.ETHEREUM:
170-
case ChainType.TRON:
171-
case ChainType.FILECOIN:
170+
ChainFamily family = ChainRegistry.getInstance().resolveFamily(metadata.getChainType());
171+
switch (family) {
172+
case EVM:
173+
case TRON:
174+
case FILECOIN:
172175
keystore = V3MnemonicKeystore.create(metadata, password, mnemonicCodes, path);
173176
break;
174-
case ChainType.BITCOIN:
175-
case ChainType.LITECOIN:
176-
case ChainType.DASH:
177-
case ChainType.DOGECOIN:
178-
case ChainType.BITCOINCASH:
179-
case ChainType.BITCOINSV:
177+
case BITCOIN_STYLE_UTXO:
180178
keystore = HDMnemonicKeystore.create(metadata, password, mnemonicCodes, path);
181179
break;
182-
case ChainType.EOS:
180+
case EOS:
183181
keystore = EOSKeystore.create(metadata, password, accountName, mnemonicCodes, path, permissions);
184182
break;
185183
default:
@@ -194,7 +192,7 @@ public static Wallet importWalletFromMnemonic(Metadata metadata, String mnemonic
194192
}
195193

196194
public static Wallet findWalletByPrivateKey(String chainType, String network, String privateKey, String segWit) {
197-
if (ChainType.ETHEREUM.equals(chainType)) {
195+
if (ChainRegistry.getInstance().isRegisteredEvm(chainType)) {
198196
new PrivateKeyValidator(privateKey).validate();
199197
}
200198
Network net = new Network(network);
@@ -403,15 +401,10 @@ private static String deriveAddressByChain(String chainType, Metadata metadata,
403401

404402

405403
private static boolean isBitcoinFamily(String chainType) {
406-
return ChainType.BITCOIN.equalsIgnoreCase(chainType)
407-
|| ChainType.LITECOIN.equalsIgnoreCase(chainType)
408-
|| ChainType.DOGECOIN.equalsIgnoreCase(chainType)
409-
|| ChainType.DASH.equalsIgnoreCase(chainType)
410-
|| ChainType.BITCOINCASH.equalsIgnoreCase(chainType)
411-
|| ChainType.BITCOINSV.equalsIgnoreCase(chainType);
404+
return ChainRegistry.getInstance().isRegisteredUtxo(chainType);
412405
}
413406
private static boolean isEvmFamily(String chainType) {
414-
return ChainType.ETHEREUM.equalsIgnoreCase(chainType) || chainType.toUpperCase().contains("EVM");
407+
return ChainRegistry.getInstance().isRegisteredEvm(chainType);
415408
}
416409

417410
private static IMTKeystore mustFindKeystoreById(String id) {

src/main/java/org/consenlabs/tokencore/wallet/address/AddressCreatorManager.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import org.bitcoinj.core.NetworkParameters;
44
import org.bitcoinj.params.MainNetParams;
55
import org.bitcoinj.params.TestNet3Params;
6+
import org.consenlabs.tokencore.wallet.chain.ChainRegistry;
7+
import org.consenlabs.tokencore.wallet.chain.UtxoChainRegistration;
68
import org.consenlabs.tokencore.wallet.model.ChainType;
79
import org.consenlabs.tokencore.wallet.model.Messages;
810
import org.consenlabs.tokencore.wallet.model.Metadata;
@@ -12,7 +14,7 @@
1214
public class AddressCreatorManager {
1315

1416
public static AddressCreator getInstance(String type, boolean isMainnet, String segWit) {
15-
if (ChainType.ETHEREUM.equals(type)) {
17+
if (ChainType.ETHEREUM.equals(type) || ChainRegistry.getInstance().isRegisteredEvm(type)) {
1618
return new EthereumAddressCreator();
1719
}else if (ChainType.FILECOIN.equals(type)) {
1820
return new FilecoinAddressCreator();
@@ -40,6 +42,10 @@ public static AddressCreator getInstance(String type, boolean isMainnet, String
4042
}
4143
return new BitcoinAddressCreator(network);
4244
} else {
45+
UtxoChainRegistration reg = ChainRegistry.getInstance().getUtxoRegistration(type);
46+
if (reg != null) {
47+
return new BitcoinAddressCreator(reg.getNetworkParameters());
48+
}
4349
throw new TokenException(Messages.WALLET_INVALID_TYPE);
4450
}
4551
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package org.consenlabs.tokencore.wallet.chain;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
6+
/**
7+
* JSON row for {@link ChainCatalogLoader}. Extra fields are ignored.
8+
*/
9+
@JsonIgnoreProperties(ignoreUnknown = true)
10+
public class ChainCatalogEntry {
11+
12+
private String chainType;
13+
14+
/**
15+
* EVM, UTXO (bitcoin-style), or built-in TRON, FILECOIN, EOS (no-op registration for catalog consistency).
16+
*/
17+
private String family;
18+
19+
private Long chainId;
20+
21+
private Integer slip44;
22+
23+
private String defaultMnemonicPath;
24+
25+
private Integer addressHeader;
26+
private Integer p2shHeader;
27+
private Integer dumpedPrivateKeyHeader;
28+
private Integer bip32HeaderPub;
29+
private Integer bip32HeaderPriv;
30+
private Boolean useMainnetDifficultyGenesis;
31+
32+
public String getChainType() {
33+
return chainType;
34+
}
35+
36+
public void setChainType(String chainType) {
37+
this.chainType = chainType;
38+
}
39+
40+
public String getFamily() {
41+
return family;
42+
}
43+
44+
public void setFamily(String family) {
45+
this.family = family;
46+
}
47+
48+
public Long getChainId() {
49+
return chainId;
50+
}
51+
52+
public void setChainId(Long chainId) {
53+
this.chainId = chainId;
54+
}
55+
56+
public Integer getSlip44() {
57+
return slip44;
58+
}
59+
60+
public void setSlip44(Integer slip44) {
61+
this.slip44 = slip44;
62+
}
63+
64+
public String getDefaultMnemonicPath() {
65+
return defaultMnemonicPath;
66+
}
67+
68+
public void setDefaultMnemonicPath(String defaultMnemonicPath) {
69+
this.defaultMnemonicPath = defaultMnemonicPath;
70+
}
71+
72+
public Integer getAddressHeader() {
73+
return addressHeader;
74+
}
75+
76+
public void setAddressHeader(Integer addressHeader) {
77+
this.addressHeader = addressHeader;
78+
}
79+
80+
public Integer getP2shHeader() {
81+
return p2shHeader;
82+
}
83+
84+
public void setP2shHeader(Integer p2shHeader) {
85+
this.p2shHeader = p2shHeader;
86+
}
87+
88+
public Integer getDumpedPrivateKeyHeader() {
89+
return dumpedPrivateKeyHeader;
90+
}
91+
92+
public void setDumpedPrivateKeyHeader(Integer dumpedPrivateKeyHeader) {
93+
this.dumpedPrivateKeyHeader = dumpedPrivateKeyHeader;
94+
}
95+
96+
public Integer getBip32HeaderPub() {
97+
return bip32HeaderPub;
98+
}
99+
100+
public void setBip32HeaderPub(Integer bip32HeaderPub) {
101+
this.bip32HeaderPub = bip32HeaderPub;
102+
}
103+
104+
public Integer getBip32HeaderPriv() {
105+
return bip32HeaderPriv;
106+
}
107+
108+
public void setBip32HeaderPriv(Integer bip32HeaderPriv) {
109+
this.bip32HeaderPriv = bip32HeaderPriv;
110+
}
111+
112+
public Boolean getUseMainnetDifficultyGenesis() {
113+
return useMainnetDifficultyGenesis;
114+
}
115+
116+
public void setUseMainnetDifficultyGenesis(Boolean useMainnetDifficultyGenesis) {
117+
this.useMainnetDifficultyGenesis = useMainnetDifficultyGenesis;
118+
}
119+
}

0 commit comments

Comments
 (0)