Skip to content

Commit 0c52dd8

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

File tree

11 files changed

+254
-5
lines changed

11 files changed

+254
-5
lines changed

modules/bitgo/src/v2/coinFactory.ts

Lines changed: 53 additions & 1 deletion
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,
@@ -565,6 +566,20 @@ export function registerCoinConstructors(coinFactory: CoinFactory, coinMap: Coin
565566
coinFactory.register(name, coinConstructor);
566567
});
567568
});
569+
570+
// Generic ERC721 token registration for coins with SUPPORTS_ERC721 feature
571+
coins
572+
.filter((coin) => coin.features.includes(CoinFeature.SUPPORTS_ERC721) && !coin.isToken)
573+
.forEach((coin) => {
574+
const coinNames = {
575+
Mainnet: `${coin.name}`,
576+
Testnet: `t${coin.name}`,
577+
};
578+
579+
EthLikeErc721Token.createTokenConstructors(coinNames).forEach(({ name, coinConstructor }) => {
580+
coinFactory.register(name, coinConstructor);
581+
});
582+
});
568583
}
569584

570585
export function getCoinConstructor(coinName: string): CoinConstructor | undefined {
@@ -926,8 +941,30 @@ export const buildEthLikeChainToTestnetMap = (): {
926941
return { mainnetToTestnetMap, testnetToMainnetMap };
927942
};
928943

944+
const buildEthLikeErc721ChainToTestnetMap = (): {
945+
mainnetToTestnetMapForErc721Tokens: Record<string, string>;
946+
testnetToMainnetMapForErc721Tokens: Record<string, string>;
947+
} => {
948+
const testnetToMainnetMapForErc721Tokens: Record<string, string> = {};
949+
const mainnetToTestnetMapForErc721Tokens: Record<string, string> = {};
950+
951+
coins.forEach((coin) => {
952+
if (coin.network.type === NetworkType.TESTNET && !coin.isToken) {
953+
if (coins.get(coin.family)?.features.includes(CoinFeature.SUPPORTS_ERC721)) {
954+
mainnetToTestnetMapForErc721Tokens[coin.family] = `${coin.name}`;
955+
testnetToMainnetMapForErc721Tokens[coin.name] = `${coin.family}`;
956+
}
957+
}
958+
});
959+
960+
return { mainnetToTestnetMapForErc721Tokens, testnetToMainnetMapForErc721Tokens };
961+
};
962+
929963
const { mainnetToTestnetMap, testnetToMainnetMap } = buildEthLikeChainToTestnetMap();
930964

965+
const { mainnetToTestnetMapForErc721Tokens, testnetToMainnetMapForErc721Tokens } =
966+
buildEthLikeErc721ChainToTestnetMap();
967+
931968
export function getTokenConstructor(tokenConfig: TokenConfig): CoinConstructor | undefined {
932969
const testnetCoin = mainnetToTestnetMap[tokenConfig.coin];
933970
if (testnetCoin) {
@@ -943,6 +980,21 @@ export function getTokenConstructor(tokenConfig: TokenConfig): CoinConstructor |
943980
Testnet: tokenConfig.coin,
944981
});
945982
}
983+
984+
if (mainnetToTestnetMapForErc721Tokens[tokenConfig.coin]) {
985+
return EthLikeErc721Token.createTokenConstructor(tokenConfig as EthLikeTokenConfig, {
986+
Mainnet: tokenConfig.coin,
987+
Testnet: mainnetToTestnetMapForErc721Tokens[tokenConfig.coin],
988+
});
989+
}
990+
991+
if (testnetToMainnetMapForErc721Tokens[tokenConfig.coin]) {
992+
return EthLikeErc721Token.createTokenConstructor(tokenConfig as EthLikeTokenConfig, {
993+
Mainnet: testnetToMainnetMapForErc721Tokens[tokenConfig.coin],
994+
Testnet: tokenConfig.coin,
995+
});
996+
}
997+
946998
switch (tokenConfig.coin) {
947999
case 'eth':
9481000
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: `${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: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2136,7 +2136,7 @@ export const allCoinsAndTokens = [
21362136
Networks.main.hederaEVM,
21372137
8,
21382138
UnderlyingAsset.HBAREVM,
2139-
BaseUnit.HBAR,
2139+
BaseUnit.ETH,
21402140
[
21412141
...EVM_FEATURES,
21422142
CoinFeature.SHARED_EVM_SIGNING,
@@ -2147,6 +2147,7 @@ export const allCoinsAndTokens = [
21472147
CoinFeature.EVM_NON_BITGO_RECOVERY,
21482148
CoinFeature.EVM_UNSIGNED_SWEEP_RECOVERY,
21492149
CoinFeature.SUPPORTS_ERC20,
2150+
CoinFeature.SUPPORTS_ERC721,
21502151
]
21512152
),
21522153
account(
@@ -2156,7 +2157,7 @@ export const allCoinsAndTokens = [
21562157
Networks.test.hederaEVM,
21572158
8,
21582159
UnderlyingAsset.HBAREVM,
2159-
BaseUnit.HBAR,
2160+
BaseUnit.ETH,
21602161
[
21612162
...EVM_FEATURES,
21622163
CoinFeature.SHARED_EVM_SIGNING,
@@ -2166,6 +2167,7 @@ export const allCoinsAndTokens = [
21662167
CoinFeature.EVM_COMPATIBLE_WP,
21672168
CoinFeature.EVM_NON_BITGO_RECOVERY,
21682169
CoinFeature.EVM_UNSIGNED_SWEEP_RECOVERY,
2170+
CoinFeature.SUPPORTS_ERC721,
21692171
]
21702172
),
21712173
account(

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
*/

modules/statics/src/coins.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
xrpToken,
2828
adaToken,
2929
erc20Token,
30+
erc721Token,
3031
} from './account';
3132
import { ofcToken } from './ofc';
3233
import { BaseCoin, CoinFeature } from './base';
@@ -64,6 +65,17 @@ allCoinsAndTokens.forEach((coin) => {
6465
}
6566
});
6667

68+
const erc721ChainToNameMap: Record<string, string> = {};
69+
allCoinsAndTokens.forEach((coin) => {
70+
if (
71+
coin.features.includes(CoinFeature.SUPPORTS_ERC721) &&
72+
coin.network.type === NetworkType.MAINNET &&
73+
!coin.isToken
74+
) {
75+
erc721ChainToNameMap[coin.family] = coin.name;
76+
}
77+
});
78+
6779
export function createToken(token: AmsTokenConfig): Readonly<BaseCoin> | undefined {
6880
const initializerMap: Record<string, unknown> = {
6981
algo: algoToken,
@@ -105,6 +117,10 @@ export function createToken(token: AmsTokenConfig): Readonly<BaseCoin> | undefin
105117
initializerMap[key] = erc20Token;
106118
});
107119

120+
Object.keys(erc721ChainToNameMap).forEach((key) => {
121+
initializerMap[key] = erc721Token;
122+
});
123+
108124
//return the BaseCoin from default coin map if present
109125
if (isCoinPresentInCoinMap({ ...token })) {
110126
if (coins.has(token.name)) {
@@ -148,6 +164,16 @@ export function createToken(token: AmsTokenConfig): Readonly<BaseCoin> | undefin
148164
token.suffix,
149165
token.primaryKeyCurve
150166
);
167+
case erc721ChainToNameMap[family]:
168+
return initializer(
169+
...commonArgs.slice(0, 3), // id, name, fullName
170+
token.contractAddress, // contractAddress
171+
token.network,
172+
token.features,
173+
token.prefix,
174+
token.suffix,
175+
token.primaryKeyCurve
176+
);
151177
case 'arbeth':
152178
case 'avaxc':
153179
case 'baseeth':

0 commit comments

Comments
 (0)