Skip to content

Commit eaebcde

Browse files
committed
feat: erc721 token support for evm coins
ticket: win-8154
1 parent 871ed17 commit eaebcde

File tree

12 files changed

+308
-55
lines changed

12 files changed

+308
-55
lines changed

modules/bitgo/src/v2/coinFactory.ts

Lines changed: 34 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ import { Near, TNear, Nep141Token } from '@bitgo/sdk-coin-near';
99
import { SolToken } from '@bitgo/sdk-coin-sol';
1010
import { TrxToken } from '@bitgo/sdk-coin-trx';
1111
import { CoinFactory, CoinConstructor } from '@bitgo/sdk-core';
12-
import { EthLikeErc20Token } from '@bitgo/sdk-coin-evm';
12+
import { EthLikeErc20Token, EthLikeErc721Token } from '@bitgo/sdk-coin-evm';
13+
1314
import {
1415
CoinMap,
1516
coins,
@@ -38,7 +39,6 @@ import {
3839
TaoTokenConfig,
3940
PolyxTokenConfig,
4041
JettonTokenConfig,
41-
NetworkType,
4242
} from '@bitgo/statics';
4343
import {
4444
Ada,
@@ -565,6 +565,20 @@ export function registerCoinConstructors(coinFactory: CoinFactory, coinMap: Coin
565565
coinFactory.register(name, coinConstructor);
566566
});
567567
});
568+
569+
// Generic ERC721 token registration for coins with SUPPORTS_ERC721 feature
570+
coins
571+
.filter((coin) => coin.features.includes(CoinFeature.SUPPORTS_ERC721) && !coin.isToken)
572+
.forEach((coin) => {
573+
const coinNames = {
574+
Mainnet: `${coin.name}`,
575+
Testnet: `t${coin.name}`,
576+
};
577+
578+
EthLikeErc721Token.createTokenConstructors(coinNames).forEach(({ name, coinConstructor }) => {
579+
coinFactory.register(name, coinConstructor);
580+
});
581+
});
568582
}
569583

570584
export function getCoinConstructor(coinName: string): CoinConstructor | undefined {
@@ -904,45 +918,31 @@ export function getCoinConstructor(coinName: string): CoinConstructor | undefine
904918
}
905919
}
906920

907-
export const buildEthLikeChainToTestnetMap = (): {
908-
mainnetToTestnetMap: Record<string, string>;
909-
testnetToMainnetMap: Record<string, string>;
910-
} => {
911-
const testnetToMainnetMap: Record<string, string> = {};
912-
const mainnetToTestnetMap: Record<string, string> = {};
913-
914-
const enabledEvmCoins = ['ip', 'hypeevm', 'plume'];
915-
916-
// TODO: remove ip and hypeeevm coins here and remove other evm coins from switch block, once changes are tested (Ticket: https://bitgoinc.atlassian.net/browse/WIN-7835)
917-
coins.forEach((coin) => {
918-
if (coin.network.type === NetworkType.TESTNET && !coin.isToken && enabledEvmCoins.includes(coin.family)) {
919-
if (coins.get(coin.family)?.features.includes(CoinFeature.SUPPORTS_ERC20)) {
920-
mainnetToTestnetMap[coin.family] = `${coin.name}`;
921-
testnetToMainnetMap[coin.name] = `${coin.family}`;
922-
}
923-
}
924-
});
925-
926-
return { mainnetToTestnetMap, testnetToMainnetMap };
927-
};
928-
929-
const { mainnetToTestnetMap, testnetToMainnetMap } = buildEthLikeChainToTestnetMap();
930-
931921
export function getTokenConstructor(tokenConfig: TokenConfig): CoinConstructor | undefined {
932-
const testnetCoin = mainnetToTestnetMap[tokenConfig.coin];
933-
if (testnetCoin) {
922+
const coin = coins.get(tokenConfig.coin);
923+
924+
if (
925+
'network' in tokenConfig &&
926+
tokenConfig.network === 'Mainnet' &&
927+
coin?.features.includes(CoinFeature.SUPPORTS_ERC20)
928+
) {
934929
return EthLikeErc20Token.createTokenConstructor(tokenConfig as EthLikeTokenConfig, {
935930
Mainnet: tokenConfig.coin,
936-
Testnet: testnetCoin,
931+
Testnet: `t${tokenConfig.coin}`,
937932
});
938933
}
939-
const mainnetCoin = testnetToMainnetMap[tokenConfig.coin];
940-
if (mainnetCoin) {
941-
return EthLikeErc20Token.createTokenConstructor(tokenConfig as EthLikeTokenConfig, {
942-
Mainnet: mainnetCoin,
943-
Testnet: tokenConfig.coin,
934+
935+
if (
936+
'network' in tokenConfig &&
937+
tokenConfig.network === 'Mainnet' &&
938+
coin?.features.includes(CoinFeature.SUPPORTS_ERC721)
939+
) {
940+
return EthLikeErc721Token.createTokenConstructor(tokenConfig as EthLikeTokenConfig, {
941+
Mainnet: tokenConfig.coin,
942+
Testnet: `t${tokenConfig.coin}`,
944943
});
945944
}
945+
946946
switch (tokenConfig.coin) {
947947
case 'eth':
948948
case 'hteth':

modules/bitgo/src/v2/coins/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import { Dot, Tdot } from '@bitgo/sdk-coin-dot';
3131
import { Eos, EosToken, Teos } from '@bitgo/sdk-coin-eos';
3232
import { Etc, Tetc } from '@bitgo/sdk-coin-etc';
3333
import { Erc20Token, Erc721Token, Eth, Gteth, Hteth, Teth } from '@bitgo/sdk-coin-eth';
34-
import { EvmCoin, EthLikeErc20Token } from '@bitgo/sdk-coin-evm';
34+
import { EvmCoin, EthLikeErc20Token, EthLikeErc721Token } from '@bitgo/sdk-coin-evm';
3535
import { Flr, Tflr, FlrToken } from '@bitgo/sdk-coin-flr';
3636
import { Flrp } from '@bitgo/sdk-coin-flrp';
3737
import { Ethw } from '@bitgo/sdk-coin-ethw';
@@ -109,7 +109,7 @@ export { Erc20Token, Erc721Token, Eth, Gteth, Hteth, Teth };
109109
export { Ethw };
110110
export { EthLikeCoin, TethLikeCoin };
111111
export { Etc, Tetc };
112-
export { EvmCoin, EthLikeErc20Token };
112+
export { EvmCoin, EthLikeErc20Token, EthLikeErc721Token };
113113
export { Flr, Tflr, FlrToken };
114114
export { Flrp };
115115
export { Hash, Thash, HashToken };

modules/bitgo/test/browser/browser.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ describe('Coins', () => {
5555
CosmosSharedCoin: 1,
5656
VetToken: 1,
5757
EthLikeErc20Token: 1,
58+
EthLikeErc721Token: 1,
5859
HashToken: 1,
5960
FlrToken: 1,
6061
JettonToken: 1,
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* @prettier
3+
*/
4+
import { coins, EthLikeTokenConfig } from '@bitgo/statics';
5+
import { BitGoBase, CoinConstructor, common, MPCAlgorithm, NamedCoinConstructor } from '@bitgo/sdk-core';
6+
import { CoinNames, EthLikeToken, recoveryBlockchainExplorerQuery } from '@bitgo/abstract-eth';
7+
import { TransactionBuilder } from './lib';
8+
import assert from 'assert';
9+
10+
export class EthLikeErc721Token extends EthLikeToken {
11+
public readonly tokenConfig: EthLikeTokenConfig;
12+
private readonly coinNames: CoinNames;
13+
14+
constructor(bitgo: BitGoBase, tokenConfig: EthLikeTokenConfig, coinNames: CoinNames) {
15+
super(bitgo, tokenConfig, coinNames);
16+
this.coinNames = coinNames;
17+
}
18+
19+
static createTokenConstructor(config: EthLikeTokenConfig, coinNames: CoinNames): CoinConstructor {
20+
return (bitgo: BitGoBase) => new this(bitgo, config, coinNames);
21+
}
22+
23+
static createTokenConstructors(coinNames: CoinNames): NamedCoinConstructor[] {
24+
return super.createTokenConstructors(coinNames);
25+
}
26+
27+
protected getTransactionBuilder(): TransactionBuilder {
28+
return new TransactionBuilder(coins.get(this.getBaseChain()));
29+
}
30+
31+
getMPCAlgorithm(): MPCAlgorithm {
32+
return 'ecdsa';
33+
}
34+
35+
supportsTss(): boolean {
36+
return true;
37+
}
38+
39+
async recoveryBlockchainExplorerQuery(query: Record<string, string>): Promise<Record<string, unknown>> {
40+
const family = this.getFamily();
41+
const evmConfig = common.Environments[this.bitgo.getEnv()].evm;
42+
assert(
43+
evmConfig && this.getFamily() in evmConfig,
44+
`env config is missing for ${this.getFamily()} in ${this.bitgo.getEnv()}`
45+
);
46+
const explorerUrl = evmConfig[family].baseUrl;
47+
const apiToken = evmConfig[family].apiToken;
48+
return await recoveryBlockchainExplorerQuery(query, explorerUrl as string, apiToken);
49+
}
50+
51+
getFullName(): string {
52+
return 'ERC721 Token';
53+
}
54+
}

modules/sdk-coin-evm/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './evmCoin';
22
export * from './lib';
33
export * from './register';
44
export * from './ethLikeErc20Token';
5+
export * from './ethLikeErc721Token';

modules/sdk-coin-evm/src/register.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { BitGoBase } from '@bitgo/sdk-core';
22
import { CoinFeature, coins, NetworkType } from '@bitgo/statics';
33
import { EvmCoin } from './evmCoin';
44
import { EthLikeErc20Token } from './ethLikeErc20Token';
5+
import { EthLikeErc721Token } from './ethLikeErc721Token';
56

67
export const registerAll = (sdk: BitGoBase): void => {
78
coins
@@ -35,5 +36,17 @@ export const register = (coinFamily: string, sdk: BitGoBase): void => {
3536
sdk.register(name, coinConstructor);
3637
});
3738
}
39+
40+
// Handle SUPPORTS_ERC721 registration
41+
if (coinFeatures.includes(CoinFeature.SUPPORTS_ERC721)) {
42+
const coinNames = {
43+
Mainnet: `${coin.name}`,
44+
Testnet: `t${coin.name}`,
45+
};
46+
47+
EthLikeErc721Token.createTokenConstructors(coinNames).forEach(({ name, coinConstructor }) => {
48+
sdk.register(name, coinConstructor);
49+
});
50+
}
3851
});
3952
};

modules/statics/src/account.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ export interface Erc20ConstructorOptions extends AccountConstructorOptions {
9090
contractAddress: string;
9191
}
9292

93+
export interface Erc721ConstructorOptions extends AccountConstructorOptions {
94+
contractAddress: string;
95+
}
96+
9397
export interface NFTCollectionIdConstructorOptions extends AccountConstructorOptions {
9498
nftCollectionId: string;
9599
}
@@ -430,6 +434,12 @@ export class EthLikeERC20Token extends ContractAddressDefinedToken {
430434
}
431435
}
432436

437+
export class EthLikeERC721Token extends ContractAddressDefinedToken {
438+
constructor(options: Erc721ConstructorOptions) {
439+
super(options);
440+
}
441+
}
442+
433443
/**
434444
* The AVAX C Chain network support tokens
435445
* AVAX C Chain Tokens are ERC20 coins
@@ -895,6 +905,49 @@ export function erc20Token(
895905
);
896906
}
897907

908+
/**
909+
* Factory function for erc721 token instances.
910+
*
911+
* @param id uuid v4
912+
* @param name unique identifier of the token
913+
* @param fullName Complete human-readable name of the token
914+
* @param contractAddress Contract address of this token
915+
* @param network network
916+
* @param features Features of this coin. Defaults to the DEFAULT_FEATURES defined in `AccountCoin`
917+
* @param prefix Optional token prefix
918+
* @param suffix Optional token suffix
919+
* @param primaryKeyCurve The elliptic curve for this chain/token
920+
*/
921+
export function erc721Token(
922+
id: string,
923+
name: string,
924+
fullName: string,
925+
contractAddress: string,
926+
network: AccountNetwork,
927+
features: CoinFeature[] = [...AccountCoin.DEFAULT_FEATURES, CoinFeature.EIP1559],
928+
prefix = '',
929+
suffix: string = name.toUpperCase(),
930+
primaryKeyCurve: KeyCurve = KeyCurve.Secp256k1
931+
): Readonly<EthLikeERC721Token> {
932+
return Object.freeze(
933+
new EthLikeERC721Token({
934+
id,
935+
name,
936+
fullName,
937+
network,
938+
contractAddress,
939+
decimalPlaces: 0, // ERC721 tokens are non-divisible
940+
asset: UnderlyingAsset.ERC721,
941+
features,
942+
prefix,
943+
suffix,
944+
primaryKeyCurve,
945+
isToken: true,
946+
baseUnit: BaseUnit.ETH,
947+
})
948+
);
949+
}
950+
898951
/**
899952
* Factory function for erc20 token instances.
900953
*

modules/statics/src/allCoinsAndTokens.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
erc20CompatibleAccountCoin,
1414
erc20Token,
1515
erc721,
16+
erc721Token,
1617
fiat,
1718
flrErc20,
1819
gasTankAccount,
@@ -2136,7 +2137,7 @@ export const allCoinsAndTokens = [
21362137
Networks.main.hederaEVM,
21372138
8,
21382139
UnderlyingAsset.HBAREVM,
2139-
BaseUnit.HBAR,
2140+
BaseUnit.ETH,
21402141
[
21412142
...EVM_FEATURES,
21422143
CoinFeature.SHARED_EVM_SIGNING,
@@ -2147,6 +2148,7 @@ export const allCoinsAndTokens = [
21472148
CoinFeature.EVM_NON_BITGO_RECOVERY,
21482149
CoinFeature.EVM_UNSIGNED_SWEEP_RECOVERY,
21492150
CoinFeature.SUPPORTS_ERC20,
2151+
CoinFeature.SUPPORTS_ERC721,
21502152
]
21512153
),
21522154
account(
@@ -2156,7 +2158,7 @@ export const allCoinsAndTokens = [
21562158
Networks.test.hederaEVM,
21572159
8,
21582160
UnderlyingAsset.HBAREVM,
2159-
BaseUnit.HBAR,
2161+
BaseUnit.ETH,
21602162
[
21612163
...EVM_FEATURES,
21622164
CoinFeature.SHARED_EVM_SIGNING,
@@ -2168,6 +2170,15 @@ export const allCoinsAndTokens = [
21682170
CoinFeature.EVM_UNSIGNED_SWEEP_RECOVERY,
21692171
]
21702172
),
2173+
2174+
erc721Token(
2175+
'a7604e03-7f40-41f0-8efa-2e7673ac2a9f',
2176+
'thbarevmnft',
2177+
'Testnet Hedera EVM NFT',
2178+
'0x00000000000000000000000000000000007103a5',
2179+
Networks.test.hederaEVM
2180+
),
2181+
21712182
account(
21722183
'8f6ed7e4-cce2-4686-bdab-ae8f54e2c05e',
21732184
'tfluenteth',

modules/statics/src/base.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,11 @@ export enum CoinFeature {
421421
*/
422422
SUPPORTS_ERC20 = 'supports-erc20-token',
423423

424+
/**
425+
* This coin supports erc721 tokens
426+
*/
427+
SUPPORTS_ERC721 = 'supports-erc721-token',
428+
424429
/**
425430
* This coin is a Cosmos coin and should use shared Cosmos SDK module
426431
*/

0 commit comments

Comments
 (0)