diff --git a/modules/bitgo/src/v2/coinFactory.ts b/modules/bitgo/src/v2/coinFactory.ts index c08b4adad1..5a877a8897 100644 --- a/modules/bitgo/src/v2/coinFactory.ts +++ b/modules/bitgo/src/v2/coinFactory.ts @@ -42,6 +42,8 @@ import { PolyxTokenConfig, JettonTokenConfig, CantonTokenConfig, + Erc7984TokenConfig, + Erc7984Coin, } from '@bitgo/statics'; import { Ada, @@ -82,6 +84,7 @@ import { EosToken, Erc20Token, Erc721Token, + Erc7984Token, Etc, Eth, Ethw, @@ -419,6 +422,13 @@ export function registerCoinConstructors(coinFactory: CoinFactory, coinMap: Coin } ); + Erc7984Token.createTokenConstructors([ + ...tokens.bitcoin.eth.confidentialTokens, + ...tokens.testnet.eth.confidentialTokens, + ]).forEach(({ name, coinConstructor }) => { + coinFactory.register(name, coinConstructor); + }); + StellarToken.createTokenConstructors([...tokens.bitcoin.xlm.tokens, ...tokens.testnet.xlm.tokens]).forEach( ({ name, coinConstructor }) => { coinFactory.register(name, coinConstructor); @@ -960,12 +970,16 @@ export function getTokenConstructor(tokenConfig: TokenConfig): CoinConstructor | switch (tokenConfig.coin) { case 'eth': - case 'hteth': - if (tokenConfig.type.includes('erc721')) { + case 'hteth': { + const staticCoin = coins.get(tokenConfig.type); + if (staticCoin instanceof Erc7984Coin) { + return Erc7984Token.createTokenConstructor(tokenConfig as Erc7984TokenConfig); + } else if (tokenConfig.type.includes('erc721')) { return Erc721Token.createTokenConstructor(tokenConfig as EthLikeTokenConfig); } else { return Erc20Token.createTokenConstructor(tokenConfig as Erc20TokenConfig); } + } case 'xlm': case 'txlm': return StellarToken.createTokenConstructor(tokenConfig as StellarTokenConfig); diff --git a/modules/bitgo/src/v2/coins/index.ts b/modules/bitgo/src/v2/coins/index.ts index 9a6607a8da..53a096e12c 100644 --- a/modules/bitgo/src/v2/coins/index.ts +++ b/modules/bitgo/src/v2/coins/index.ts @@ -30,7 +30,7 @@ import { Doge, Tdoge } from '@bitgo/sdk-coin-doge'; import { Dot, Tdot } from '@bitgo/sdk-coin-dot'; import { Eos, EosToken, Teos } from '@bitgo/sdk-coin-eos'; import { Etc, Tetc } from '@bitgo/sdk-coin-etc'; -import { Erc20Token, Erc721Token, Eth, Gteth, Hteth, Teth } from '@bitgo/sdk-coin-eth'; +import { Erc20Token, Erc721Token, Erc7984Token, Eth, Gteth, Hteth, Teth } from '@bitgo/sdk-coin-eth'; import { EvmCoin, EthLikeErc20Token, EthLikeErc721Token } from '@bitgo/sdk-coin-evm'; import { Flr, Tflr, FlrToken } from '@bitgo/sdk-coin-flr'; import { Flrp } from '@bitgo/sdk-coin-flrp'; @@ -107,7 +107,7 @@ export { Doge, Tdoge }; export { Dot, Tdot }; export { Bcha, Tbcha }; export { Eos, EosToken, Teos }; -export { Erc20Token, Erc721Token, Eth, Gteth, Hteth, Teth }; +export { Erc20Token, Erc721Token, Erc7984Token, Eth, Gteth, Hteth, Teth }; export { Ethw }; export { EthLikeCoin, TethLikeCoin }; export { Etc, Tetc }; diff --git a/modules/bitgo/test/browser/browser.spec.ts b/modules/bitgo/test/browser/browser.spec.ts index 71cdbeb86f..1846ab9303 100644 --- a/modules/bitgo/test/browser/browser.spec.ts +++ b/modules/bitgo/test/browser/browser.spec.ts @@ -17,6 +17,7 @@ describe('Coins', () => { AdaToken: 1, Erc20Token: 1, Erc721Token: 1, + Erc7984Token: 1, EthLikeCoin: 1, TethLikeCoin: 1, OfcToken: 1, diff --git a/modules/sdk-coin-eth/src/erc7984Token.ts b/modules/sdk-coin-eth/src/erc7984Token.ts new file mode 100644 index 0000000000..97690b2afa --- /dev/null +++ b/modules/sdk-coin-eth/src/erc7984Token.ts @@ -0,0 +1,112 @@ +/** + * @prettier + */ +import { BitGoBase, CoinConstructor, MPCAlgorithm, NamedCoinConstructor } from '@bitgo/sdk-core'; + +import { coins, Erc7984TokenConfig, tokens } from '@bitgo/statics'; +import { CoinNames } from '@bitgo/abstract-eth'; + +import { Eth } from './eth'; +import { TransactionBuilder } from './lib'; + +export { Erc7984TokenConfig }; + +export class Erc7984Token extends Eth { + public readonly tokenConfig: Erc7984TokenConfig; + protected readonly sendMethodName: 'sendMultiSig' | 'sendMultiSigToken'; + static coinNames: CoinNames = { + Mainnet: 'eth', + Testnet: 'hteth', + }; + + constructor(bitgo: BitGoBase, tokenConfig: Erc7984TokenConfig) { + const staticsCoin = coins.get(Erc7984Token.coinNames[tokenConfig.network]); + super(bitgo, staticsCoin); + this.tokenConfig = tokenConfig; + this.sendMethodName = 'sendMultiSigToken'; + } + + static createTokenConstructor(config: Erc7984TokenConfig): CoinConstructor { + return (bitgo: BitGoBase) => new Erc7984Token(bitgo, config); + } + + static createTokenConstructors( + tokenConfigs: Erc7984TokenConfig[] = [ + ...tokens.bitcoin.eth.confidentialTokens, + ...tokens.testnet.eth.confidentialTokens, + ] + ): NamedCoinConstructor[] { + const tokensCtors: NamedCoinConstructor[] = []; + for (const token of tokenConfigs) { + const tokenConstructor = Erc7984Token.createTokenConstructor(token); + tokensCtors.push({ name: token.type, coinConstructor: tokenConstructor }); + tokensCtors.push({ name: token.tokenContractAddress, coinConstructor: tokenConstructor }); + } + return tokensCtors; + } + + get type() { + return this.tokenConfig.type; + } + + get name() { + return this.tokenConfig.name; + } + + get coin() { + return this.tokenConfig.coin; + } + + get network() { + return this.tokenConfig.network; + } + + get tokenContractAddress() { + return this.tokenConfig.tokenContractAddress; + } + + get decimalPlaces() { + return this.tokenConfig.decimalPlaces; + } + + getChain() { + return this.tokenConfig.type; + } + + getFullName() { + return 'ERC7984 Confidential Token'; + } + + getBaseFactor() { + return Math.pow(10, this.tokenConfig.decimalPlaces); + } + + /** + * Flag for sending value of 0. + * ERC-7984 confidential transfers always carry an encrypted amount; zero-value sends are not meaningful. + */ + valuelessTransferAllowed() { + return false; + } + + /** + * Flag for sending data along with transactions. + */ + transactionDataAllowed() { + return false; + } + + /** @inheritDoc */ + supportsTss(): boolean { + return true; + } + + /** @inheritDoc */ + getMPCAlgorithm(): MPCAlgorithm { + return 'ecdsa'; + } + + protected getTransactionBuilder(): TransactionBuilder { + return new TransactionBuilder(coins.get(this.getBaseChain())); + } +} diff --git a/modules/sdk-coin-eth/src/index.ts b/modules/sdk-coin-eth/src/index.ts index 6e77f9893d..db17a8ffb1 100644 --- a/modules/sdk-coin-eth/src/index.ts +++ b/modules/sdk-coin-eth/src/index.ts @@ -1,5 +1,6 @@ export * from './erc20Token'; export * from './erc721Token'; +export * from './erc7984Token'; export * from './eth'; export * from './gteth'; export * from './hteth'; diff --git a/modules/sdk-coin-eth/src/register.ts b/modules/sdk-coin-eth/src/register.ts index 4b51ff1c3b..393e1f550a 100644 --- a/modules/sdk-coin-eth/src/register.ts +++ b/modules/sdk-coin-eth/src/register.ts @@ -1,11 +1,12 @@ import { BitGoBase, GlobalCoinFactory } from '@bitgo/sdk-core'; import { Erc20Token } from './erc20Token'; +import { Erc721Token } from './erc721Token'; +import { Erc7984Token } from './erc7984Token'; import { Eth } from './eth'; import { Gteth } from './gteth'; import { Hteth } from './hteth'; import { Teth } from './teth'; -import { Erc721Token } from './erc721Token'; -import { type CoinMap, getFormattedErc20Tokens } from '@bitgo/statics'; +import { type CoinMap, getFormattedErc20Tokens, getFormattedErc7984Tokens } from '@bitgo/statics'; export const register = (sdk: BitGoBase): void => { sdk.register('eth', Eth.createInstance); @@ -18,6 +19,9 @@ export const register = (sdk: BitGoBase): void => { Erc721Token.createTokenConstructors().forEach(({ name, coinConstructor }) => { sdk.register(name, coinConstructor); }); + Erc7984Token.createTokenConstructors().forEach(({ name, coinConstructor }) => { + sdk.register(name, coinConstructor); + }); }; export const registerWithCoinMap = (sdk: BitGoBase, coinMap: CoinMap): void => { @@ -36,4 +40,12 @@ export const registerWithCoinMap = (sdk: BitGoBase, coinMap: CoinMap): void => { GlobalCoinFactory.registerToken(coinMap.get(name), coinConstructor); } }); + + // Registration for ERC-7984 confidential tokens from the coin map. + Erc7984Token.createTokenConstructors(getFormattedErc7984Tokens(coinMap)).forEach(({ name, coinConstructor }) => { + sdk.register(name, coinConstructor); + if (coinMap.has(name)) { + GlobalCoinFactory.registerToken(coinMap.get(name), coinConstructor); + } + }); }; diff --git a/modules/sdk-coin-eth/test/unit/register.ts b/modules/sdk-coin-eth/test/unit/register.ts index 5cfecadfa5..1a9f616eea 100644 --- a/modules/sdk-coin-eth/test/unit/register.ts +++ b/modules/sdk-coin-eth/test/unit/register.ts @@ -2,10 +2,11 @@ import sinon from 'sinon'; import assert from 'assert'; import { BitGoAPI } from '@bitgo/sdk-api'; import { GlobalCoinFactory } from '@bitgo/sdk-core'; -import { coins, Erc20Coin } from '@bitgo/statics'; +import { coins, Erc20Coin, Erc7984Coin } from '@bitgo/statics'; import { register, registerWithCoinMap } from '../../src/register'; import { Erc20Token } from '../../src/erc20Token'; import { Erc721Token } from '../../src/erc721Token'; +import { Erc7984Token } from '../../src/erc7984Token'; describe('ETH Register', function () { let bitgo: BitGoAPI; @@ -35,10 +36,11 @@ describe('ETH Register', function () { assert.ok(registeredNames.includes('teth')); assert.ok(registeredNames.includes('hteth')); - // ERC20 and ERC721 tokens should be registered + // ERC20, ERC721 and ERC7984 tokens should be registered const erc20Count = Erc20Token.createTokenConstructors().length; const erc721Count = Erc721Token.createTokenConstructors().length; - assert.strictEqual(registerSpy.callCount, 4 + erc20Count + erc721Count); + const erc7984Count = Erc7984Token.createTokenConstructors().length; + assert.strictEqual(registerSpy.callCount, 4 + erc20Count + erc721Count + erc7984Count); }); }); @@ -69,12 +71,12 @@ describe('ETH Register', function () { } }); - it('should not add tokens to the global coin map when coin map has no ERC20 tokens', function () { - const limitedCoinMap = coins.filter((coin) => !(coin instanceof Erc20Coin)); + it('should not add tokens to the global coin map when coin map has no contract-address tokens', function () { + const limitedCoinMap = coins.filter((coin) => !(coin instanceof Erc20Coin) && !(coin instanceof Erc7984Coin)); registerWithCoinMap(bitgo, limitedCoinMap); - // registerToken should not be called since no ERC20 tokens are in the map + // registerToken should not be called since no ERC20 or ERC7984 tokens are in the map assert.strictEqual(registerTokenSpy.callCount, 0); }); });