diff --git a/packages/core/src/blockchains/tokens/registry.ts b/packages/core/src/blockchains/tokens/registry.ts index 159bc4236..8e6444483 100644 --- a/packages/core/src/blockchains/tokens/registry.ts +++ b/packages/core/src/blockchains/tokens/registry.ts @@ -119,16 +119,16 @@ export class Token implements TokenData { return isWrappedToken(this); } + getAmount(amount: BigNumber | string | number): TokenAmount { + return new TokenAmount(amount, this.getUnits(), this); + } + getUnits(): Units { const { decimals, name, symbol } = this; return new Units([new Unit(decimals, name, symbol)]); } - getAmount(amount: BigNumber | string | number): TokenAmount { - return new TokenAmount(amount, this.getUnits(), this); - } - toPlain(): TokenData { return { ...this }; } @@ -210,7 +210,11 @@ export class TokenRegistry { return (instances?.size ?? 0) > 0; } - getStablecoins(blockchain: BlockchainCode): Token[] { + hasWrappedToken(blockchain: BlockchainCode): boolean { + return WRAPPED_TOKENS[blockchain] != null; + } + + getPinned(blockchain: BlockchainCode): Token[] { const instances = this.instances.get(blockchain); if (instances == null) { @@ -218,7 +222,7 @@ export class TokenRegistry { } return [...instances.values()].reduce((carry, token) => { - if (token.stablecoin) { + if (token.pinned) { return [...carry, token]; } @@ -226,7 +230,7 @@ export class TokenRegistry { }, []); } - getPinned(blockchain: BlockchainCode): Token[] { + getStablecoins(blockchain: BlockchainCode): Token[] { const instances = this.instances.get(blockchain); if (instances == null) { @@ -234,11 +238,33 @@ export class TokenRegistry { } return [...instances.values()].reduce((carry, token) => { - if (token.pinned) { + if (token.stablecoin) { return [...carry, token]; } return carry; }, []); } + + getWrapped(blockchain: BlockchainCode): Token { + const address = WRAPPED_TOKENS[blockchain]; + + if (address == null) { + throw new Error(`Wrapped token not found for ${blockchain} blockchain`); + } + + const instances = this.instances.get(blockchain); + + if (instances == null) { + throw new Error(`Can't find wrapped token by blockchain ${blockchain}`); + } + + const instance = instances.get(address.toLowerCase()); + + if (instance == null) { + throw new Error(`Can't find wrapped token by address ${address} in ${blockchain} blockchain`); + } + + return instance; + } } diff --git a/packages/core/src/transaction/workflow/TxBuilder.ts b/packages/core/src/transaction/workflow/TxBuilder.ts index 44af8591b..ce1d84d3b 100644 --- a/packages/core/src/transaction/workflow/TxBuilder.ts +++ b/packages/core/src/transaction/workflow/TxBuilder.ts @@ -18,7 +18,7 @@ import { isEthereum, } from '../../blockchains'; import { EthereumTransactionType } from '../ethereum'; -import { CreateBitcoinTx, CreateErc20ApproveTx, CreateErc20Tx, CreateEtherTx } from './create-tx'; +import { CreateBitcoinTx, CreateErc20ApproveTx, CreateErc20ConvertTx, CreateErc20Tx, CreateEtherTx } from './create-tx'; import { AnyCreateTx, AnyErc20CreateTx, @@ -28,6 +28,7 @@ import { fromEthereumPlainTx, isAnyErc20CreateTx, isErc20ApproveCreateTx, + isErc20ConvertCreateTx, isErc20CreateTx, isEtherCreateTx, } from './create-tx/types'; @@ -111,10 +112,9 @@ export class TxBuilder implements BuilderOrigin { createTx.priorityGasPrice = feeRange.stdPriorityGasPrice; } - createTx.type = - Blockchains[blockchain].params.eip1559 ?? false - ? EthereumTransactionType.EIP1559 - : EthereumTransactionType.LEGACY; + const { eip1559: supportEip1559 = false } = Blockchains[blockchain].params; + + createTx.type = supportEip1559 ? EthereumTransactionType.EIP1559 : EthereumTransactionType.LEGACY; } } else { if (isBitcoinPlainTx(transaction)) { @@ -130,8 +130,10 @@ export class TxBuilder implements BuilderOrigin { createTx = fromEthereumPlainTx(transaction, tokenRegistry); + const isChanged = asset !== createTx.getAsset() || blockchain !== createTx.blockchain; + if (isEtherCreateTx(createTx) || isErc20CreateTx(createTx)) { - if (asset !== createTx.getAsset() || blockchain !== createTx.blockchain) { + if (isChanged) { return this.convertEthereumTx(createTx); } @@ -139,11 +141,15 @@ export class TxBuilder implements BuilderOrigin { this.mergeEthereumTx(transaction, createTx); } - if (isErc20ApproveCreateTx(createTx)) { - if (asset !== createTx.getAsset()) { + if (isChanged) { + if (isErc20ApproveCreateTx(createTx)) { createTx = this.transformErc20ApproveTx(createTx); } + if (isErc20ConvertCreateTx(createTx)) { + createTx = this.transformErc20ConvertTx(createTx); + } + this.mergeEthereumFee(createTx); } } @@ -152,62 +158,12 @@ export class TxBuilder implements BuilderOrigin { return createTx; } - private initBitcoinTx(entry: BitcoinEntry): CreateBitcoinTx { - const { - changeAddress, - feeRange, - dataProvider: { getUtxo }, - } = this; - - const createTx = new CreateBitcoinTx( - { - changeAddress, - blockchain: blockchainIdToCode(entry.blockchain), - entryId: entry.id, - }, - getUtxo(entry), - ); - - if (isBitcoinFeeRange(feeRange)) { - createTx.feePrice = feeRange.std; - } - - return createTx; - } - - private initEthereumTx(entry: EthereumEntry): CreateEtherTx { - const { asset } = this; - const { getBalance } = this.dataProvider; - - const createTx = new CreateEtherTx(null, blockchainIdToCode(entry.blockchain)); - - createTx.totalBalance = getBalance(entry, asset) as WeiAny; - - return createTx; - } - - private initErc20Tx(entry: EthereumEntry): CreateErc20Tx { - const { asset, ownerAddress, tokenRegistry } = this; - const { getBalance } = this.dataProvider; - - const blockchain = blockchainIdToCode(entry.blockchain); - - const { coinTicker } = Blockchains[blockchain].params; - - const createTx = new CreateErc20Tx(tokenRegistry, asset, blockchain); - - createTx.totalBalance = getBalance(entry, coinTicker) as WeiAny; - createTx.totalTokenBalance = getBalance(entry, asset, ownerAddress); - createTx.transferFrom = ownerAddress; - - return createTx; - } - private convertEthereumTx(oldCreateTx: EthereumBasicCreateTx): EthereumBasicCreateTx { const { asset, entry, feeRange, ownerAddress, tokenRegistry } = this; const { getBalance } = this.dataProvider; const blockchain = blockchainIdToCode(entry.blockchain); + const { coinTicker, eip1559: supportEip1559 = false } = Blockchains[blockchain].params; const type = supportEip1559 ? oldCreateTx.type : EthereumTransactionType.LEGACY; @@ -215,7 +171,7 @@ export class TxBuilder implements BuilderOrigin { let newCreateTx: EthereumBasicCreateTx; if (tokenRegistry.hasAddress(blockchain, asset)) { - newCreateTx = new CreateErc20Tx(tokenRegistry, asset, blockchain, type); + newCreateTx = new CreateErc20Tx(asset, tokenRegistry, blockchain, type); newCreateTx.totalBalance = getBalance(entry, coinTicker) as WeiAny; newCreateTx.totalTokenBalance = getBalance(entry, asset, newCreateTx.transferFrom); @@ -259,20 +215,53 @@ export class TxBuilder implements BuilderOrigin { return newCreateTx; } - private transformErc20ApproveTx(createTx: CreateErc20ApproveTx): CreateErc20ApproveTx { - const { asset, entry, tokenRegistry } = this; + private initBitcoinTx(entry: BitcoinEntry): CreateBitcoinTx { + const { + changeAddress, + feeRange, + dataProvider: { getUtxo }, + } = this; + + const createTx = new CreateBitcoinTx( + { + changeAddress, + blockchain: blockchainIdToCode(entry.blockchain), + entryId: entry.id, + }, + getUtxo(entry), + ); + + if (isBitcoinFeeRange(feeRange)) { + createTx.feePrice = feeRange.std; + } + + return createTx; + } + + private initEthereumTx(entry: EthereumEntry): CreateEtherTx { + const { asset } = this; + const { getBalance } = this.dataProvider; + + const createTx = new CreateEtherTx(null, blockchainIdToCode(entry.blockchain)); + + createTx.totalBalance = getBalance(entry, asset) as WeiAny; + + return createTx; + } + + private initErc20Tx(entry: EthereumEntry): CreateErc20Tx { + const { asset, ownerAddress, tokenRegistry } = this; const { getBalance } = this.dataProvider; const blockchain = blockchainIdToCode(entry.blockchain); - const { coinTicker, eip1559: supportEip1559 = false } = Blockchains[blockchain].params; + const { coinTicker } = Blockchains[blockchain].params; - createTx.setToken( - tokenRegistry.byAddress(blockchain, asset), - getBalance(entry, coinTicker) as WeiAny, - getBalance(entry, asset) as TokenAmount, - supportEip1559, - ); + const createTx = new CreateErc20Tx(asset, tokenRegistry, blockchain); + + createTx.totalBalance = getBalance(entry, coinTicker) as WeiAny; + createTx.totalTokenBalance = getBalance(entry, asset, ownerAddress); + createTx.transferFrom = ownerAddress; return createTx; } @@ -321,4 +310,54 @@ export class TxBuilder implements BuilderOrigin { } } } + + private transformErc20ApproveTx(createTx: CreateErc20ApproveTx): CreateErc20ApproveTx { + const { asset, entry, tokenRegistry } = this; + const { getBalance } = this.dataProvider; + + const blockchain = blockchainIdToCode(entry.blockchain); + + let tokenAddress = asset; + + if (!tokenRegistry.hasAddress(blockchain, tokenAddress)) { + [{ address: tokenAddress }] = tokenRegistry.byBlockchain(blockchain); + } + + const { coinTicker, eip1559: supportEip1559 = false } = Blockchains[blockchain].params; + + createTx.setToken( + tokenRegistry.byAddress(blockchain, tokenAddress), + getBalance(entry, coinTicker) as WeiAny, + getBalance(entry, tokenAddress) as TokenAmount, + supportEip1559, + ); + + return createTx; + } + + private transformErc20ConvertTx(createTx: CreateErc20ConvertTx): CreateErc20ConvertTx { + const { asset, entry, tokenRegistry } = this; + const { getBalance } = this.dataProvider; + + const blockchain = blockchainIdToCode(entry.blockchain); + + createTx.asset = asset; + + let tokenAddress = asset; + + if (!tokenRegistry.hasAddress(blockchain, tokenAddress)) { + tokenAddress = tokenRegistry.getWrapped(blockchain).address; + } + + const { coinTicker, eip1559: supportEip1559 = false } = Blockchains[blockchain].params; + + createTx.setToken( + tokenRegistry.byAddress(blockchain, tokenAddress), + getBalance(entry, coinTicker) as WeiAny, + getBalance(entry, tokenAddress) as TokenAmount, + supportEip1559, + ); + + return createTx; + } } diff --git a/packages/core/src/transaction/workflow/TxSigner.ts b/packages/core/src/transaction/workflow/TxSigner.ts index 7ff0b8b61..a5677b381 100644 --- a/packages/core/src/transaction/workflow/TxSigner.ts +++ b/packages/core/src/transaction/workflow/TxSigner.ts @@ -17,7 +17,7 @@ import { BlockchainCode, Blockchains } from '../../blockchains'; import { EthereumTx } from '../../blockchains/ethereum'; import { EthereumAddress } from '../../blockchains/ethereum/EthereumAddress'; import { EthereumTransaction, EthereumTransactionType } from '../ethereum'; -import { AnyCreateTx, isAnyBitcoinCreateTx, isErc20ApproveCreateTx } from './create-tx/types'; +import { AnyCreateTx, isAnyBitcoinCreateTx, isErc20ApproveCreateTx, isErc20ConvertCreateTx } from './create-tx/types'; interface SignerOrigin { createTx: AnyCreateTx; @@ -143,7 +143,15 @@ export class TxSigner implements SignerOrigin { } if (transaction.verifySignature()) { - const sender = isErc20ApproveCreateTx(createTx) ? createTx.approveBy : createTx.from; + let sender: string | undefined; + + if (isErc20ApproveCreateTx(createTx)) { + sender = createTx.approveBy; + } else if (isErc20ConvertCreateTx(createTx)) { + sender = createTx.address; + } else { + sender = createTx.from; + } if (sender != null && !transaction.getSenderAddress().equals(new EthereumAddress(sender))) { throw new Error('Emerald Vault returned signature from wrong Sender'); diff --git a/packages/core/src/transaction/workflow/create-tx/CreateBitcoinTx.ts b/packages/core/src/transaction/workflow/create-tx/CreateBitcoinTx.ts index 506b97d51..8c8d500cd 100644 --- a/packages/core/src/transaction/workflow/create-tx/CreateBitcoinTx.ts +++ b/packages/core/src/transaction/workflow/create-tx/CreateBitcoinTx.ts @@ -181,6 +181,15 @@ export class CreateBitcoinTx implements BitcoinTx { this.setAmount(value); } + /** + * @deprecated + * Added to make one logic for Bitcoin and Ethereum flow. + * Create getter after refactoring Ethereum create transaction class. + */ + getAsset(): string { + return this.amount.units.top.code; + } + /** * @deprecated * Added to make one logic for Bitcoin and Ethereum flow. diff --git a/packages/core/src/transaction/workflow/create-tx/CreateErc20ApproveTx.spec.ts b/packages/core/src/transaction/workflow/create-tx/CreateErc20ApproveTx.spec.ts index b4742dbbb..cd03c94b3 100644 --- a/packages/core/src/transaction/workflow/create-tx/CreateErc20ApproveTx.spec.ts +++ b/packages/core/src/transaction/workflow/create-tx/CreateErc20ApproveTx.spec.ts @@ -29,14 +29,17 @@ describe('CreateErc20ApproveTx', () => { const weenusToken = tokenRegistry.byAddress(BlockchainCode.Goerli, '0xaFF4481D10270F50f203E0763e2597776068CBc5'); test('should create legacy approve tx', () => { - const tx = new CreateErc20ApproveTx(tokenRegistry, { - amount: wethToken.getAmount(0), - asset: wethToken.address, - blockchain: BlockchainCode.Goerli, - meta: { type: TxMetaType.ERC20_APPROVE }, - target: ApproveTarget.MANUAL, - type: EthereumTransactionType.LEGACY, - }); + const tx = new CreateErc20ApproveTx( + { + amount: wethToken.getAmount(0), + asset: wethToken.address, + blockchain: BlockchainCode.Goerli, + meta: { type: TxMetaType.ERC20_APPROVE }, + target: ApproveTarget.MANUAL, + type: EthereumTransactionType.LEGACY, + }, + tokenRegistry, + ); expect(tx.amount.number.toNumber()).toEqual(0); expect(tx.gas).toBe(DEFAULT_GAS_LIMIT_ERC20); @@ -45,14 +48,17 @@ describe('CreateErc20ApproveTx', () => { }); test('should create eip1559 approve tx', () => { - const tx = new CreateErc20ApproveTx(tokenRegistry, { - amount: wethToken.getAmount(0), - asset: wethToken.address, - blockchain: BlockchainCode.Goerli, - meta: { type: TxMetaType.ERC20_APPROVE }, - target: ApproveTarget.MANUAL, - type: EthereumTransactionType.EIP1559, - }); + const tx = new CreateErc20ApproveTx( + { + amount: wethToken.getAmount(0), + asset: wethToken.address, + blockchain: BlockchainCode.Goerli, + meta: { type: TxMetaType.ERC20_APPROVE }, + target: ApproveTarget.MANUAL, + type: EthereumTransactionType.EIP1559, + }, + tokenRegistry, + ); expect(tx.amount.number.toNumber()).toEqual(0); expect(tx.gas).toEqual(DEFAULT_GAS_LIMIT_ERC20); @@ -64,26 +70,32 @@ describe('CreateErc20ApproveTx', () => { test('should set target', () => { const amount = wethToken.getAmount(3); - let tx = new CreateErc20ApproveTx(tokenRegistry, { - amount: wethToken.getAmount(0), - asset: wethToken.address, - blockchain: BlockchainCode.Goerli, - meta: { type: TxMetaType.ERC20_APPROVE }, - target: ApproveTarget.MAX_AVAILABLE, - totalTokenBalance: amount, - type: EthereumTransactionType.EIP1559, - }); + let tx = new CreateErc20ApproveTx( + { + amount: wethToken.getAmount(0), + asset: wethToken.address, + blockchain: BlockchainCode.Goerli, + meta: { type: TxMetaType.ERC20_APPROVE }, + target: ApproveTarget.MAX_AVAILABLE, + totalTokenBalance: amount, + type: EthereumTransactionType.EIP1559, + }, + tokenRegistry, + ); expect(tx.amount.equals(amount)).toBeTruthy(); - tx = new CreateErc20ApproveTx(tokenRegistry, { - amount: wethToken.getAmount(0), - asset: wethToken.address, - blockchain: BlockchainCode.Goerli, - meta: { type: TxMetaType.ERC20_APPROVE }, - target: ApproveTarget.INFINITE, - type: EthereumTransactionType.EIP1559, - }); + tx = new CreateErc20ApproveTx( + { + amount: wethToken.getAmount(0), + asset: wethToken.address, + blockchain: BlockchainCode.Goerli, + meta: { type: TxMetaType.ERC20_APPROVE }, + target: ApproveTarget.INFINITE, + type: EthereumTransactionType.EIP1559, + }, + tokenRegistry, + ); expect(tx.amount.number.eq(INFINITE_ALLOWANCE)).toBeTruthy(); }); @@ -91,15 +103,18 @@ describe('CreateErc20ApproveTx', () => { test('should change amount and target', () => { const amount = wethToken.getAmount(1); - const tx = new CreateErc20ApproveTx(tokenRegistry, { - amount: wethToken.getAmount(0), - asset: wethToken.address, - blockchain: BlockchainCode.Goerli, - meta: { type: TxMetaType.ERC20_APPROVE }, - target: ApproveTarget.MANUAL, - totalTokenBalance: amount, - type: EthereumTransactionType.EIP1559, - }); + const tx = new CreateErc20ApproveTx( + { + amount: wethToken.getAmount(0), + asset: wethToken.address, + blockchain: BlockchainCode.Goerli, + meta: { type: TxMetaType.ERC20_APPROVE }, + target: ApproveTarget.MANUAL, + totalTokenBalance: amount, + type: EthereumTransactionType.EIP1559, + }, + tokenRegistry, + ); tx.target = ApproveTarget.MAX_AVAILABLE; @@ -117,15 +132,18 @@ describe('CreateErc20ApproveTx', () => { test('should change token', () => { const amount = wethToken.getAmount(1); - const tx = new CreateErc20ApproveTx(tokenRegistry, { - amount, - asset: wethToken.address, - blockchain: BlockchainCode.Goerli, - meta: { type: TxMetaType.ERC20_APPROVE }, - target: ApproveTarget.MANUAL, - totalTokenBalance: amount, - type: EthereumTransactionType.EIP1559, - }); + const tx = new CreateErc20ApproveTx( + { + amount, + asset: wethToken.address, + blockchain: BlockchainCode.Goerli, + meta: { type: TxMetaType.ERC20_APPROVE }, + target: ApproveTarget.MANUAL, + totalTokenBalance: amount, + type: EthereumTransactionType.EIP1559, + }, + tokenRegistry, + ); const weenusAmount = weenusToken.getAmount(1); @@ -137,14 +155,17 @@ describe('CreateErc20ApproveTx', () => { }); test('should validate tx', () => { - const tx = new CreateErc20ApproveTx(tokenRegistry, { - amount: wethToken.getAmount(0), - asset: wethToken.address, - blockchain: BlockchainCode.Goerli, - meta: { type: TxMetaType.ERC20_APPROVE }, - target: ApproveTarget.MANUAL, - type: EthereumTransactionType.EIP1559, - }); + const tx = new CreateErc20ApproveTx( + { + amount: wethToken.getAmount(0), + asset: wethToken.address, + blockchain: BlockchainCode.Goerli, + meta: { type: TxMetaType.ERC20_APPROVE }, + target: ApproveTarget.MANUAL, + type: EthereumTransactionType.EIP1559, + }, + tokenRegistry, + ); expect(tx.validate()).toEqual(ValidationResult.NO_FROM); @@ -158,16 +179,19 @@ describe('CreateErc20ApproveTx', () => { }); test('should build tx', () => { - const tx = new CreateErc20ApproveTx(tokenRegistry, { - amount: wethToken.getAmount(0), - approveBy: '0xe62c6f33a58d7f49e6b782aab931450e53d01f12', - allowFor: '0x3f54eb67fea225d0a21263f1a7cb456e342cb1e8', - asset: wethToken.address, - blockchain: BlockchainCode.Goerli, - meta: { type: TxMetaType.ERC20_APPROVE }, - target: ApproveTarget.MANUAL, - type: EthereumTransactionType.EIP1559, - }); + const tx = new CreateErc20ApproveTx( + { + amount: wethToken.getAmount(0), + approveBy: '0xe62c6f33a58d7f49e6b782aab931450e53d01f12', + allowFor: '0x3f54eb67fea225d0a21263f1a7cb456e342cb1e8', + asset: wethToken.address, + blockchain: BlockchainCode.Goerli, + meta: { type: TxMetaType.ERC20_APPROVE }, + target: ApproveTarget.MANUAL, + type: EthereumTransactionType.EIP1559, + }, + tokenRegistry, + ); expect(tx.build().data?.length).toBeGreaterThan(2); }); diff --git a/packages/core/src/transaction/workflow/create-tx/CreateErc20ApproveTx.ts b/packages/core/src/transaction/workflow/create-tx/CreateErc20ApproveTx.ts index cc6a79bed..97e397b02 100644 --- a/packages/core/src/transaction/workflow/create-tx/CreateErc20ApproveTx.ts +++ b/packages/core/src/transaction/workflow/create-tx/CreateErc20ApproveTx.ts @@ -37,7 +37,7 @@ export interface Erc20ApproveTxDetails extends CommonTx { type: EthereumTransactionType; } -function fromPlainTx(tokenRegistry: TokenRegistry, plain: Erc20ApprovePlainTx): Erc20ApproveTxDetails { +function fromPlainTx(plain: Erc20ApprovePlainTx, tokenRegistry: TokenRegistry): Erc20ApproveTxDetails { const token = tokenRegistry.byAddress(plain.blockchain, plain.asset); const decoder = amountDecoder(plain.blockchain) as CreateAmount; @@ -105,8 +105,8 @@ export class CreateErc20ApproveTx implements Erc20ApproveTxDetails { private readonly tokenContract = new Contract(tokenAbi); constructor( - tokenRegistry: TokenRegistry, source: Erc20ApproveTxDetails | string, + tokenRegistry: TokenRegistry, blockchain?: BlockchainCode, type = EthereumTransactionType.EIP1559, ) { @@ -119,8 +119,8 @@ export class CreateErc20ApproveTx implements Erc20ApproveTxDetails { details = { type, - amount: this._token.getAmount(0), - asset: this._token.address, + amount: this.token.getAmount(0), + asset: this.token.address, blockchain: blockchainCode, meta: this.meta, target: ApproveTarget.MANUAL, @@ -131,11 +131,11 @@ export class CreateErc20ApproveTx implements Erc20ApproveTxDetails { details = source; } - const zeroTokenAmount = this._token.getAmount(0); + const zeroTokenAmount = this.token.getAmount(0); switch (details.target) { case ApproveTarget.INFINITE: - this._amount = this._token.getAmount(INFINITE_ALLOWANCE); + this._amount = this.token.getAmount(INFINITE_ALLOWANCE); break; case ApproveTarget.MAX_AVAILABLE: @@ -154,17 +154,16 @@ export class CreateErc20ApproveTx implements Erc20ApproveTxDetails { this.allowFor = details.allowFor; this.blockchain = details.blockchain; this.gas = details.gas ?? DEFAULT_GAS_LIMIT_ERC20; - this.totalBalance = details.totalBalance ?? this.zeroAmount; - this.totalTokenBalance = details.totalTokenBalance ?? zeroTokenAmount; - this.type = details.type; - this.gasPrice = details.gasPrice ?? this.zeroAmount; this.maxGasPrice = details.maxGasPrice ?? this.zeroAmount; this.priorityGasPrice = details.priorityGasPrice ?? this.zeroAmount; + this.totalBalance = details.totalBalance ?? this.zeroAmount; + this.totalTokenBalance = details.totalTokenBalance ?? zeroTokenAmount; + this.type = details.type; } - static fromPlain(tokenRegistry: TokenRegistry, plain: Erc20ApprovePlainTx): CreateErc20ApproveTx { - return new CreateErc20ApproveTx(tokenRegistry, fromPlainTx(tokenRegistry, plain)); + static fromPlain(plain: Erc20ApprovePlainTx, tokenRegistry: TokenRegistry): CreateErc20ApproveTx { + return new CreateErc20ApproveTx(fromPlainTx(plain, tokenRegistry), tokenRegistry); } get amount(): TokenAmount { @@ -175,14 +174,14 @@ export class CreateErc20ApproveTx implements Erc20ApproveTxDetails { if (TokenAmount.is(value)) { this._amount = value; } else { - this._amount = this._token.getAmount(value); + this._amount = this.token.getAmount(value); } this._target = ApproveTarget.MANUAL; } get asset(): string { - return this._token.address; + return this.token.address; } get target(): ApproveTarget { @@ -198,7 +197,7 @@ export class CreateErc20ApproveTx implements Erc20ApproveTxDetails { break; case ApproveTarget.INFINITE: - this._amount = this._token.getAmount(INFINITE_ALLOWANCE); + this._amount = this.token.getAmount(INFINITE_ALLOWANCE); break; } @@ -212,7 +211,7 @@ export class CreateErc20ApproveTx implements Erc20ApproveTxDetails { * @deprecated Use getter */ getAsset(): string { - return this._token.address; + return this.token.address; } /** @@ -262,6 +261,7 @@ export class CreateErc20ApproveTx implements Erc20ApproveTxDetails { setToken(token: Token, totalBalance: WeiAny, totalTokenBalance: TokenAmount, iep1559 = false): void { this._amount = new TokenAmount(this.amount, token.getUnits(), token); this._token = token; + this.totalBalance = totalBalance; this.totalTokenBalance = totalTokenBalance; this.type = iep1559 ? EthereumTransactionType.EIP1559 : EthereumTransactionType.LEGACY; diff --git a/packages/core/src/transaction/workflow/create-tx/CreateErc20CancelTx.ts b/packages/core/src/transaction/workflow/create-tx/CreateErc20CancelTx.ts index d5cab4a34..019c9ee9d 100644 --- a/packages/core/src/transaction/workflow/create-tx/CreateErc20CancelTx.ts +++ b/packages/core/src/transaction/workflow/create-tx/CreateErc20CancelTx.ts @@ -6,8 +6,8 @@ import { CreateErc20Tx, fromPlainDetails } from './CreateErc20Tx'; export class CreateErc20CancelTx extends CreateErc20Tx { meta = { type: TxMetaType.ERC20_CANCEL }; - static fromPlain(tokenRegistry: TokenRegistry, details: EthereumBasicPlainTx): CreateErc20CancelTx { - return new CreateErc20CancelTx(tokenRegistry, fromPlainDetails(tokenRegistry, details)); + static fromPlain(details: EthereumBasicPlainTx, tokenRegistry: TokenRegistry): CreateErc20CancelTx { + return new CreateErc20CancelTx(fromPlainDetails(details, tokenRegistry), tokenRegistry); } build(): EthereumTransaction { diff --git a/packages/core/src/transaction/workflow/create-tx/CreateErc20ConvertTx.spec.ts b/packages/core/src/transaction/workflow/create-tx/CreateErc20ConvertTx.spec.ts new file mode 100644 index 000000000..8b0135b3c --- /dev/null +++ b/packages/core/src/transaction/workflow/create-tx/CreateErc20ConvertTx.spec.ts @@ -0,0 +1,119 @@ +import { Wei, WeiAny } from '@emeraldpay/bigamount-crypto'; +import { BlockchainCode, CoinTicker, TokenRegistry, amountFactory } from '../../../blockchains'; +import { DEFAULT_GAS_LIMIT_ERC20, EthereumTransactionType } from '../../ethereum'; +import { TxMetaType, TxTarget } from '../types'; +import { CreateErc20ConvertTx } from './CreateErc20ConvertTx'; + +describe('CreateErc20ConvertTx', () => { + const tokenRegistry = new TokenRegistry([ + { + name: 'Wrapped Ether', + blockchain: 100, + address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', + symbol: 'WETH', + decimals: 18, + type: 'ERC20', + }, + ]); + + const token = tokenRegistry.byAddress(BlockchainCode.ETH, '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'); + + it('creates legacy tx', () => { + const tx = new CreateErc20ConvertTx( + { + address: '0x2C80BfA8E69fdd12853Fd010A520B29cfa01E2cD', + amount: new Wei(0), + asset: token.address, + blockchain: BlockchainCode.ETH, + meta: { type: TxMetaType.ERC20_CONVERT }, + target: TxTarget.MANUAL, + totalBalance: new Wei(0), + totalTokenBalance: token.getAmount(0), + type: EthereumTransactionType.LEGACY, + }, + tokenRegistry, + ); + + expect(tx.amount.number.toNumber()).toBe(0); + expect(tx.gas).toBe(DEFAULT_GAS_LIMIT_ERC20); + expect(tx.gasPrice?.number.toNumber()).toBe(0); + expect(tx.totalBalance.number.toNumber()).toBe(0); + }); + + it('creates eip1559 tx', () => { + const tx = new CreateErc20ConvertTx( + { + address: '0x2C80BfA8E69fdd12853Fd010A520B29cfa01E2cD', + amount: new Wei(0), + asset: token.address, + blockchain: BlockchainCode.ETH, + meta: { type: TxMetaType.ERC20_CONVERT }, + target: TxTarget.MANUAL, + totalBalance: new Wei(0), + totalTokenBalance: token.getAmount(0), + type: EthereumTransactionType.EIP1559, + }, + tokenRegistry, + ); + + expect(tx.amount.number.toNumber()).toBe(0); + expect(tx.gas).toBe(DEFAULT_GAS_LIMIT_ERC20); + expect(tx.maxGasPrice?.number.toNumber()).toBe(0); + expect(tx.priorityGasPrice?.number.toNumber()).toBe(0); + expect(tx.totalBalance.number.toNumber()).toBe(0); + }); + + it('correctly rebalance ETH', () => { + const totalBalance = amountFactory(BlockchainCode.ETH)(10 ** 18) as WeiAny; + + const tx = new CreateErc20ConvertTx( + { + totalBalance, + address: '0x2C80BfA8E69fdd12853Fd010A520B29cfa01E2cD', + amount: new Wei(0), + asset: CoinTicker.ETH, + blockchain: BlockchainCode.ETH, + meta: { type: TxMetaType.ERC20_CONVERT }, + target: TxTarget.MANUAL, + totalTokenBalance: token.getAmount(0), + type: EthereumTransactionType.EIP1559, + }, + tokenRegistry, + ); + + expect(tx.target).toBe(TxTarget.MANUAL); + expect(tx.amount.number.toNumber()).toBe(0); + + tx.target = TxTarget.SEND_ALL; + tx.rebalance(); + + expect(tx.target).toBe(TxTarget.SEND_ALL); + expect(tx.amount.equals(totalBalance)).toBeTruthy(); + }); + + it('correctly rebalance WETH', () => { + const tx = new CreateErc20ConvertTx( + { + address: '0x2C80BfA8E69fdd12853Fd010A520B29cfa01E2cD', + amount: token.getAmount(0), + asset: token.address, + blockchain: BlockchainCode.ETH, + meta: { type: TxMetaType.ERC20_CONVERT }, + target: TxTarget.MANUAL, + totalBalance: new Wei(0), + totalTokenBalance: token.getAmount(1), + type: EthereumTransactionType.EIP1559, + }, + tokenRegistry, + ); + + expect(tx.target).toBe(TxTarget.MANUAL); + expect(tx.amount.equals(token.getAmount(0))).toBeTruthy(); + + tx.target = TxTarget.SEND_ALL; + tx.rebalance(); + + expect(tx.target).toBe(TxTarget.SEND_ALL); + expect(tx.amount.equals(token.getAmount(1))).toBeTruthy(); + }); +}); diff --git a/packages/core/src/transaction/workflow/create-tx/CreateErc20ConvertTx.ts b/packages/core/src/transaction/workflow/create-tx/CreateErc20ConvertTx.ts new file mode 100644 index 000000000..291237503 --- /dev/null +++ b/packages/core/src/transaction/workflow/create-tx/CreateErc20ConvertTx.ts @@ -0,0 +1,306 @@ +import { BigAmount, CreateAmount } from '@emeraldpay/bigamount'; +import { WeiAny } from '@emeraldpay/bigamount-crypto'; +import BigNumber from 'bignumber.js'; +import { + BlockchainCode, + Blockchains, + Token, + TokenAmount, + TokenRegistry, + amountDecoder, + amountFactory, + wrapTokenAbi, +} from '../../../blockchains'; +import { Contract } from '../../../Contract'; +import { DEFAULT_GAS_LIMIT_ERC20, EthereumTransaction, EthereumTransactionType } from '../../ethereum'; +import { CommonTx, Erc20ConvertPlainTx, TxMetaType, TxTarget, ValidationResult } from '../types'; + +export interface Erc20ConvertTxDetails extends CommonTx { + address?: string; + asset: string; + amount: BigAmount; + blockchain: BlockchainCode; + gas?: number; + gasPrice?: WeiAny; + maxGasPrice?: WeiAny; + priorityGasPrice?: WeiAny; + target: TxTarget; + totalBalance?: WeiAny; + totalTokenBalance?: TokenAmount; + type: EthereumTransactionType; +} + +function fromPlainTx(plain: Erc20ConvertPlainTx, tokenRegistry: TokenRegistry): Erc20ConvertTxDetails { + const token = tokenRegistry.getWrapped(plain.blockchain); + + const decoder = amountDecoder(plain.blockchain) as CreateAmount; + + const tokenDecoder = (value: string): TokenAmount => + token.getAmount(TokenAmount.decode(value, token.getUnits()).number); + + return { + address: plain.address, + amount: tokenRegistry.hasAddress(plain.blockchain, plain.asset) + ? tokenDecoder(plain.amount) + : decoder(plain.amount), + asset: plain.asset, + blockchain: plain.blockchain, + gas: plain.gas, + gasPrice: plain.gasPrice == null ? undefined : decoder(plain.gasPrice), + maxGasPrice: plain.maxGasPrice == null ? undefined : decoder(plain.maxGasPrice), + meta: plain.meta, + priorityGasPrice: plain.priorityGasPrice == null ? undefined : decoder(plain.priorityGasPrice), + target: plain.target, + totalBalance: plain.totalBalance == null ? undefined : decoder(plain.totalBalance), + totalTokenBalance: plain.totalTokenBalance == null ? undefined : tokenDecoder(plain.totalTokenBalance), + type: parseInt(plain.type, 16) === 2 ? EthereumTransactionType.EIP1559 : EthereumTransactionType.LEGACY, + }; +} + +function toPlainTx(details: Erc20ConvertTxDetails): Erc20ConvertPlainTx { + return { + address: details.address, + amount: details.amount.encode(), + asset: details.asset, + blockchain: details.blockchain, + gas: details.gas, + gasPrice: details.gasPrice?.encode(), + maxGasPrice: details.maxGasPrice?.encode(), + meta: details.meta, + priorityGasPrice: details.priorityGasPrice?.encode(), + target: details.target?.valueOf(), + totalBalance: details.totalBalance?.encode(), + totalTokenBalance: details.totalTokenBalance?.encode(), + type: `0x${details.type.toString(16)}`, + }; +} + +export class CreateErc20ConvertTx implements Erc20ConvertTxDetails { + meta = { type: TxMetaType.ERC20_CONVERT }; + + private _amount: BigAmount; + private _asset: string; + private _token: Token; + + address?: string; + blockchain: BlockchainCode; + gas: number; + gasPrice?: WeiAny; + maxGasPrice?: WeiAny; + priorityGasPrice?: WeiAny; + target: TxTarget; + totalBalance: WeiAny; + totalTokenBalance: TokenAmount; + type: EthereumTransactionType; + + private readonly zeroAmount: WeiAny; + + private readonly tokenContract = new Contract(wrapTokenAbi); + + constructor( + source: Erc20ConvertTxDetails | BlockchainCode, + tokenRegistry: TokenRegistry, + type = EthereumTransactionType.EIP1559, + ) { + let details: Erc20ConvertTxDetails; + + if (typeof source === 'string') { + this._token = tokenRegistry.getWrapped(source); + + this.zeroAmount = amountFactory(source)(0) as WeiAny; + + const { coinTicker } = Blockchains[source].params; + + details = { + type, + amount: this.zeroAmount, + asset: coinTicker, + blockchain: source, + meta: this.meta, + target: TxTarget.MANUAL, + }; + } else { + this._token = tokenRegistry.getWrapped(source.blockchain); + + this.zeroAmount = amountFactory(source.blockchain)(0) as WeiAny; + + details = source; + } + + this._amount = details.amount; + this._asset = details.asset; + + this.address = details.address; + this.blockchain = details.blockchain; + this.gas = details.gas ?? DEFAULT_GAS_LIMIT_ERC20; + this.gasPrice = details.gasPrice ?? this.zeroAmount; + this.maxGasPrice = details.maxGasPrice ?? this.zeroAmount; + this.priorityGasPrice = details.priorityGasPrice ?? this.zeroAmount; + this.target = details.target; + this.totalBalance = details.totalBalance ?? this.zeroAmount; + this.totalTokenBalance = details.totalTokenBalance ?? this.token.getAmount(0); + this.type = details.type; + } + + static fromPlain(plain: Erc20ConvertPlainTx, tokenRegistry: TokenRegistry): CreateErc20ConvertTx { + return new CreateErc20ConvertTx(fromPlainTx(plain, tokenRegistry), tokenRegistry); + } + + get amount(): BigAmount { + return this._amount; + } + + set amount(value: WeiAny | TokenAmount | BigNumber) { + if (WeiAny.is(value) || TokenAmount.is(value)) { + this._amount = value; + } else { + const { asset, token } = this; + + const { units } = this.amount; + + let amount: BigAmount; + + if (asset.toLowerCase() === token.address.toLowerCase()) { + amount = token.getAmount(1); + } else { + amount = new WeiAny(1, units); + } + + this._amount = amount.multiply(units.top.multiplier).multiply(value); + } + + this.target = TxTarget.MANUAL; + } + + get asset(): string { + return this._asset; + } + + set asset(value: string) { + this._asset = value; + + const { token } = this; + + if (value.toLowerCase() === token.address.toLowerCase()) { + this._amount = token.getAmount(this.amount.number); + } else { + this._amount = amountFactory(this.blockchain)(this.amount.number); + } + } + + get token(): Token { + return this._token; + } + + /** + * @deprecated Use getter + */ + getAsset(): string { + return this.asset; + } + + /** + * @deprecated Use setter + */ + setAmount(value: TokenAmount | BigNumber): void { + this.amount = value; + } + + build(): EthereumTransaction { + const { + asset, + amount, + blockchain, + gas, + gasPrice, + maxGasPrice, + priorityGasPrice, + token, + tokenContract, + type, + zeroAmount, + address: from = '', + } = this; + + const isDeposit = asset.toLowerCase() !== token.address.toLowerCase(); + + const data = isDeposit + ? tokenContract.functionToData('deposit', {}) + : tokenContract.functionToData('withdraw', { _value: amount.number.toFixed() }); + + return { + blockchain, + data, + gas, + type, + from, + gasPrice, + maxGasPrice, + priorityGasPrice, + to: token.address, + value: isDeposit ? amount : zeroAmount, + }; + } + + dump(): Erc20ConvertPlainTx { + return toPlainTx(this); + } + + getFees(): WeiAny { + const { gas, gasPrice, maxGasPrice, type, zeroAmount } = this; + + const price = (type === EthereumTransactionType.EIP1559 ? maxGasPrice : gasPrice) ?? zeroAmount; + + return price.multiply(gas); + } + + rebalance(): void { + const { asset, target, token, totalBalance, totalTokenBalance } = this; + + if (target === TxTarget.SEND_ALL) { + if (asset.toLowerCase() === token.address.toLowerCase()) { + this._amount = totalTokenBalance; + } else { + const amount = totalBalance.minus(this.getFees()); + + if (amount.isZero() || amount.isPositive()) { + this._amount = amount; + } + } + } + } + + setToken(token: Token, totalBalance: WeiAny, totalTokenBalance: TokenAmount, iep1559 = false): void { + this._token = token; + + this.totalBalance = totalBalance; + this.totalTokenBalance = totalTokenBalance; + this.type = iep1559 ? EthereumTransactionType.EIP1559 : EthereumTransactionType.LEGACY; + } + + validate(): ValidationResult { + const { amount, asset, token, totalBalance, totalTokenBalance } = this; + + if (amount.isZero()) { + return ValidationResult.NO_AMOUNT; + } + + if (asset.toLowerCase() === token.address.toLowerCase()) { + if (this.getFees().isGreaterThan(totalBalance)) { + return ValidationResult.INSUFFICIENT_FUNDS; + } + + if (amount.isGreaterThan(totalTokenBalance)) { + return ValidationResult.INSUFFICIENT_TOKEN_FUNDS; + } + } else { + const total = amount.plus(this.getFees()); + + if (total.isGreaterThan(totalBalance)) { + return ValidationResult.INSUFFICIENT_FUNDS; + } + } + + return ValidationResult.OK; + } +} diff --git a/packages/core/src/transaction/workflow/create-tx/CreateErc20SpeedUpTx.ts b/packages/core/src/transaction/workflow/create-tx/CreateErc20SpeedUpTx.ts index 2dc2ce2bf..22398d1da 100644 --- a/packages/core/src/transaction/workflow/create-tx/CreateErc20SpeedUpTx.ts +++ b/packages/core/src/transaction/workflow/create-tx/CreateErc20SpeedUpTx.ts @@ -5,8 +5,8 @@ import { CreateErc20Tx, fromPlainDetails } from './CreateErc20Tx'; export class CreateErc20SpeedUpTx extends CreateErc20Tx { meta = { type: TxMetaType.ERC20_SPEEDUP }; - static fromPlain(tokenRegistry: TokenRegistry, details: EthereumBasicPlainTx): CreateErc20SpeedUpTx { - return new CreateErc20SpeedUpTx(tokenRegistry, fromPlainDetails(tokenRegistry, details)); + static fromPlain(details: EthereumBasicPlainTx, tokenRegistry: TokenRegistry): CreateErc20SpeedUpTx { + return new CreateErc20SpeedUpTx(fromPlainDetails(details, tokenRegistry), tokenRegistry); } validate(): ValidationResult { diff --git a/packages/core/src/transaction/workflow/create-tx/CreateErc20Tx.spec.ts b/packages/core/src/transaction/workflow/create-tx/CreateErc20Tx.spec.ts index 3efd1f4bc..1ae60b527 100644 --- a/packages/core/src/transaction/workflow/create-tx/CreateErc20Tx.spec.ts +++ b/packages/core/src/transaction/workflow/create-tx/CreateErc20Tx.spec.ts @@ -19,7 +19,7 @@ describe('CreateErc20Tx', () => { const daiToken = tokenRegistry.byAddress(BlockchainCode.ETH, '0x6B175474E89094C44Da98b954EedeAC495271d0F'); it('creates tx', () => { - const tx = new CreateErc20Tx(tokenRegistry, '0x6B175474E89094C44Da98b954EedeAC495271d0F', BlockchainCode.ETH); + const tx = new CreateErc20Tx('0x6B175474E89094C44Da98b954EedeAC495271d0F', tokenRegistry, BlockchainCode.ETH); expect(tx.validate()).toBe(ValidationResult.NO_AMOUNT); expect(tx.target).toBe(TxTarget.MANUAL); @@ -27,7 +27,7 @@ describe('CreateErc20Tx', () => { }); it('invalid without from', () => { - const tx = new CreateErc20Tx(tokenRegistry, '0x6B175474E89094C44Da98b954EedeAC495271d0F', BlockchainCode.ETH); + const tx = new CreateErc20Tx('0x6B175474E89094C44Da98b954EedeAC495271d0F', tokenRegistry, BlockchainCode.ETH); tx.amount = daiToken.getAmount(1); tx.setFrom('0x2C80BfA8E69fdd12853Fd010A520B29cfa01E2cD', Wei.fromEther(1), daiToken.getAmount(100)); tx.from = undefined; @@ -36,7 +36,7 @@ describe('CreateErc20Tx', () => { }); it('invalid without balance', () => { - const tx = new CreateErc20Tx(tokenRegistry, '0x6B175474E89094C44Da98b954EedeAC495271d0F', BlockchainCode.ETH); + const tx = new CreateErc20Tx('0x6B175474E89094C44Da98b954EedeAC495271d0F', tokenRegistry, BlockchainCode.ETH); tx.amount = daiToken.getAmount(1); tx.setFrom('0x2C80BfA8E69fdd12853Fd010A520B29cfa01E2cD', Wei.fromEther(1), daiToken.getAmount(100)); tx.totalBalance = undefined; @@ -45,7 +45,7 @@ describe('CreateErc20Tx', () => { }); it('invalid without token balance', () => { - const tx = new CreateErc20Tx(tokenRegistry, '0x6B175474E89094C44Da98b954EedeAC495271d0F', BlockchainCode.ETH); + const tx = new CreateErc20Tx('0x6B175474E89094C44Da98b954EedeAC495271d0F', tokenRegistry, BlockchainCode.ETH); tx.amount = daiToken.getAmount(1); tx.setFrom('0x2C80BfA8E69fdd12853Fd010A520B29cfa01E2cD', Wei.fromEther(1), daiToken.getAmount(100)); tx.totalTokenBalance = undefined; @@ -54,7 +54,7 @@ describe('CreateErc20Tx', () => { }); it('invalid without to', () => { - const tx = new CreateErc20Tx(tokenRegistry, '0x6B175474E89094C44Da98b954EedeAC495271d0F', BlockchainCode.ETH); + const tx = new CreateErc20Tx('0x6B175474E89094C44Da98b954EedeAC495271d0F', tokenRegistry, BlockchainCode.ETH); tx.amount = daiToken.getAmount(1); tx.setFrom('0x2C80BfA8E69fdd12853Fd010A520B29cfa01E2cD', Wei.fromEther(1), daiToken.getAmount(100)); @@ -62,7 +62,7 @@ describe('CreateErc20Tx', () => { }); it('invalid without enough tokens', () => { - const tx = new CreateErc20Tx(tokenRegistry, '0x6B175474E89094C44Da98b954EedeAC495271d0F', BlockchainCode.ETH); + const tx = new CreateErc20Tx('0x6B175474E89094C44Da98b954EedeAC495271d0F', tokenRegistry, BlockchainCode.ETH); tx.setFrom('0x2C80BfA8E69fdd12853Fd010A520B29cfa01E2cD', Wei.fromEther(1), daiToken.getAmount(100)); tx.to = '0x2af2d8be60ca2c0f21497bb57b0037d44b8df3bd'; tx.amount = daiToken.getAmount(101); @@ -71,7 +71,7 @@ describe('CreateErc20Tx', () => { }); it('invalid without enough ether', () => { - const tx = new CreateErc20Tx(tokenRegistry, '0x6B175474E89094C44Da98b954EedeAC495271d0F', BlockchainCode.ETH); + const tx = new CreateErc20Tx('0x6B175474E89094C44Da98b954EedeAC495271d0F', tokenRegistry, BlockchainCode.ETH); tx.setFrom('0x2C80BfA8E69fdd12853Fd010A520B29cfa01E2cD', new Wei(1), daiToken.getAmount(100)); tx.to = '0x2af2d8be60ca2c0f21497bb57b0037d44b8df3bd'; tx.maxGasPrice = new Wei(10000, 'MWEI'); @@ -82,7 +82,7 @@ describe('CreateErc20Tx', () => { }); it('valid', () => { - const tx = new CreateErc20Tx(tokenRegistry, '0x6B175474E89094C44Da98b954EedeAC495271d0F', BlockchainCode.ETH); + const tx = new CreateErc20Tx('0x6B175474E89094C44Da98b954EedeAC495271d0F', tokenRegistry, BlockchainCode.ETH); tx.setFrom('0x2C80BfA8E69fdd12853Fd010A520B29cfa01E2cD', Wei.fromEther(1), daiToken.getAmount(100)); tx.to = '0x2af2d8be60ca2c0f21497bb57b0037d44b8df3bd'; tx.maxGasPrice = new Wei(10000, 'MWEI'); @@ -93,7 +93,7 @@ describe('CreateErc20Tx', () => { }); it('zero change', () => { - const tx = new CreateErc20Tx(tokenRegistry, '0x6B175474E89094C44Da98b954EedeAC495271d0F', BlockchainCode.ETH); + const tx = new CreateErc20Tx('0x6B175474E89094C44Da98b954EedeAC495271d0F', tokenRegistry, BlockchainCode.ETH); tx.setFrom('0x2C80BfA8E69fdd12853Fd010A520B29cfa01E2cD', Wei.fromEther(1), daiToken.getAmount(100)); tx.to = '0x2af2d8be60ca2c0f21497bb57b0037d44b8df3bd'; tx.maxGasPrice = new Wei(10000, 'MWEI'); @@ -105,7 +105,7 @@ describe('CreateErc20Tx', () => { }); it('has change', () => { - const tx = new CreateErc20Tx(tokenRegistry, '0x6B175474E89094C44Da98b954EedeAC495271d0F', BlockchainCode.ETH); + const tx = new CreateErc20Tx('0x6B175474E89094C44Da98b954EedeAC495271d0F', tokenRegistry, BlockchainCode.ETH); tx.setFrom('0x2C80BfA8E69fdd12853Fd010A520B29cfa01E2cD', Wei.fromEther(1), daiToken.getAmount(100)); tx.to = '0x2af2d8be60ca2c0f21497bb57b0037d44b8df3bd'; tx.maxGasPrice = new Wei(10000, 'MWEI'); @@ -117,7 +117,7 @@ describe('CreateErc20Tx', () => { }); it('change is null if total not set', () => { - const tx = new CreateErc20Tx(tokenRegistry, '0x6B175474E89094C44Da98b954EedeAC495271d0F', BlockchainCode.ETH); + const tx = new CreateErc20Tx('0x6B175474E89094C44Da98b954EedeAC495271d0F', tokenRegistry, BlockchainCode.ETH); tx.setFrom('0x2C80BfA8E69fdd12853Fd010A520B29cfa01E2cD', Wei.fromEther(1), daiToken.getAmount(100)); tx.totalTokenBalance = undefined; tx.to = '0x2af2d8be60ca2c0f21497bb57b0037d44b8df3bd'; @@ -129,7 +129,7 @@ describe('CreateErc20Tx', () => { }); it('fees', () => { - const tx = new CreateErc20Tx(tokenRegistry, '0x6B175474E89094C44Da98b954EedeAC495271d0F', BlockchainCode.ETH); + const tx = new CreateErc20Tx('0x6B175474E89094C44Da98b954EedeAC495271d0F', tokenRegistry, BlockchainCode.ETH); tx.setFrom('0x2C80BfA8E69fdd12853Fd010A520B29cfa01E2cD', Wei.fromEther(1), daiToken.getAmount(100)); tx.to = '0x2af2d8be60ca2c0f21497bb57b0037d44b8df3bd'; tx.maxGasPrice = new Wei(10000, 'MWEI'); @@ -142,7 +142,7 @@ describe('CreateErc20Tx', () => { }); it('fees are calculated if total not set', () => { - const tx = new CreateErc20Tx(tokenRegistry, '0x6B175474E89094C44Da98b954EedeAC495271d0F', BlockchainCode.ETH); + const tx = new CreateErc20Tx('0x6B175474E89094C44Da98b954EedeAC495271d0F', tokenRegistry, BlockchainCode.ETH); tx.setFrom('0x2C80BfA8E69fdd12853Fd010A520B29cfa01E2cD', Wei.fromEther(1), daiToken.getAmount(100)); tx.totalBalance = undefined; tx.to = '0x2af2d8be60ca2c0f21497bb57b0037d44b8df3bd'; @@ -156,7 +156,7 @@ describe('CreateErc20Tx', () => { }); it('fees change', () => { - const tx = new CreateErc20Tx(tokenRegistry, '0x6B175474E89094C44Da98b954EedeAC495271d0F', BlockchainCode.ETH); + const tx = new CreateErc20Tx('0x6B175474E89094C44Da98b954EedeAC495271d0F', tokenRegistry, BlockchainCode.ETH); tx.setFrom('0x2C80BfA8E69fdd12853Fd010A520B29cfa01E2cD', Wei.fromEther(1), daiToken.getAmount(100)); tx.to = '0x2af2d8be60ca2c0f21497bb57b0037d44b8df3bd'; tx.maxGasPrice = new Wei(10000, 'MWEI'); @@ -169,7 +169,7 @@ describe('CreateErc20Tx', () => { }); it('fees change are null if total not set', () => { - const tx = new CreateErc20Tx(tokenRegistry, '0x6B175474E89094C44Da98b954EedeAC495271d0F', BlockchainCode.ETH); + const tx = new CreateErc20Tx('0x6B175474E89094C44Da98b954EedeAC495271d0F', tokenRegistry, BlockchainCode.ETH); tx.setFrom('0x2C80BfA8E69fdd12853Fd010A520B29cfa01E2cD', Wei.fromEther(1), daiToken.getAmount(100)); tx.totalBalance = undefined; tx.to = '0x2af2d8be60ca2c0f21497bb57b0037d44b8df3bd'; @@ -182,7 +182,7 @@ describe('CreateErc20Tx', () => { }); it('rebalance to max sets max', () => { - const tx = new CreateErc20Tx(tokenRegistry, '0x6B175474E89094C44Da98b954EedeAC495271d0F', BlockchainCode.ETH); + const tx = new CreateErc20Tx('0x6B175474E89094C44Da98b954EedeAC495271d0F', tokenRegistry, BlockchainCode.ETH); tx.setFrom('0x2C80BfA8E69fdd12853Fd010A520B29cfa01E2cD', Wei.fromEther(1), daiToken.getAmount(100)); tx.to = '0x2af2d8be60ca2c0f21497bb57b0037d44b8df3bd'; tx.amount = daiToken.getAmount(20); @@ -194,7 +194,7 @@ describe('CreateErc20Tx', () => { }); it('rebalance to manual does not change amount', () => { - const tx = new CreateErc20Tx(tokenRegistry, '0x6B175474E89094C44Da98b954EedeAC495271d0F', BlockchainCode.ETH); + const tx = new CreateErc20Tx('0x6B175474E89094C44Da98b954EedeAC495271d0F', tokenRegistry, BlockchainCode.ETH); tx.setFrom('0x2C80BfA8E69fdd12853Fd010A520B29cfa01E2cD', Wei.fromEther(1), daiToken.getAmount(100)); tx.to = '0x2af2d8be60ca2c0f21497bb57b0037d44b8df3bd'; tx.amount = daiToken.getAmount(20); @@ -206,7 +206,7 @@ describe('CreateErc20Tx', () => { }); it('doesnt rebalance if total not set', () => { - const tx = new CreateErc20Tx(tokenRegistry, '0x6B175474E89094C44Da98b954EedeAC495271d0F', BlockchainCode.ETH); + const tx = new CreateErc20Tx('0x6B175474E89094C44Da98b954EedeAC495271d0F', tokenRegistry, BlockchainCode.ETH); tx.setFrom('0x2C80BfA8E69fdd12853Fd010A520B29cfa01E2cD', Wei.fromEther(1), daiToken.getAmount(100)); tx.totalTokenBalance = undefined; tx.to = '0x2af2d8be60ca2c0f21497bb57b0037d44b8df3bd'; @@ -219,7 +219,7 @@ describe('CreateErc20Tx', () => { }); it('dumps plain', () => { - const tx = new CreateErc20Tx(tokenRegistry, '0x6B175474E89094C44Da98b954EedeAC495271d0F', BlockchainCode.ETH); + const tx = new CreateErc20Tx('0x6B175474E89094C44Da98b954EedeAC495271d0F', tokenRegistry, BlockchainCode.ETH); tx.setFrom('0x2C80BfA8E69fdd12853Fd010A520B29cfa01E2cD', Wei.fromEther(1), daiToken.getAmount(100)); tx.to = '0x2af2d8be60ca2c0f21497bb57b0037d44b8df3bd'; tx.amount = daiToken.getAmount(20); @@ -258,7 +258,7 @@ describe('CreateErc20Tx', () => { type: '0x2', }; - const tx = CreateErc20Tx.fromPlain(tokenRegistry, dump); + const tx = CreateErc20Tx.fromPlain(dump, tokenRegistry); expect(tx.from).toEqual('0x2C80BfA8E69fdd12853Fd010A520B29cfa01E2cD'); expect(tx.totalBalance != null ? tx.totalBalance : null).toEqual(new Wei('1000000000057', 'WEI')); diff --git a/packages/core/src/transaction/workflow/create-tx/CreateErc20Tx.ts b/packages/core/src/transaction/workflow/create-tx/CreateErc20Tx.ts index e344b923d..3925b631b 100644 --- a/packages/core/src/transaction/workflow/create-tx/CreateErc20Tx.ts +++ b/packages/core/src/transaction/workflow/create-tx/CreateErc20Tx.ts @@ -25,7 +25,7 @@ export interface Erc20TxDetails extends CommonTx { type: EthereumTransactionType; } -export function fromPlainDetails(tokenRegistry: TokenRegistry, plain: EthereumBasicPlainTx): Erc20TxDetails { +export function fromPlainDetails(plain: EthereumBasicPlainTx, tokenRegistry: TokenRegistry): Erc20TxDetails { const units = tokenRegistry.byAddress(plain.blockchain, plain.asset).getUnits(); const decoder: (value: string) => WeiAny = amountDecoder(plain.blockchain); @@ -97,8 +97,8 @@ export class CreateErc20Tx implements Erc20TxDetails, EthereumTx { private tokenContract = new Contract(tokenAbi); constructor( - tokenRegistry: TokenRegistry, source: Erc20TxDetails | string, + tokenRegistry: TokenRegistry, blockchain?: BlockchainCode | null, type = EthereumTransactionType.EIP1559, ) { @@ -123,31 +123,28 @@ export class CreateErc20Tx implements Erc20TxDetails, EthereumTx { details = source; } + this.zeroAmount = amountFactory(details.blockchain)(0) as WeiAny; + this.zeroTokenAmount = tokenRegistry.byAddress(details.blockchain, details.asset).getAmount(0); + this.amount = details.amount; this.asset = details.asset; this.blockchain = details.blockchain; this.from = details.from; this.gas = details.gas ?? DEFAULT_GAS_LIMIT_ERC20; + this.gasPrice = details.gasPrice ?? this.zeroAmount; + this.maxGasPrice = details.maxGasPrice ?? this.zeroAmount; this.nonce = details.nonce; + this.priorityGasPrice = details.priorityGasPrice ?? this.zeroAmount; this.target = details.target; this.to = details.to; this.totalBalance = details.totalBalance; this.totalTokenBalance = details.totalTokenBalance; this.transferFrom = details.transferFrom; this.type = details.type; - - const zeroAmount = amountFactory(details.blockchain)(0) as WeiAny; - - this.gasPrice = details.gasPrice ?? zeroAmount; - this.maxGasPrice = details.maxGasPrice ?? zeroAmount; - this.priorityGasPrice = details.priorityGasPrice ?? zeroAmount; - - this.zeroAmount = zeroAmount; - this.zeroTokenAmount = tokenRegistry.byAddress(this.blockchain, this.asset).getAmount(0); } - public static fromPlain(tokenRegistry: TokenRegistry, details: EthereumBasicPlainTx): CreateErc20Tx { - return new CreateErc20Tx(tokenRegistry, fromPlainDetails(tokenRegistry, details)); + public static fromPlain(details: EthereumBasicPlainTx, tokenRegistry: TokenRegistry): CreateErc20Tx { + return new CreateErc20Tx(fromPlainDetails(details, tokenRegistry), tokenRegistry); } public getAmount(): BigAmount { diff --git a/packages/core/src/transaction/workflow/create-tx/CreateErc20WrappedTx.spec.ts b/packages/core/src/transaction/workflow/create-tx/CreateErc20WrappedTx.spec.ts deleted file mode 100644 index 45371cacf..000000000 --- a/packages/core/src/transaction/workflow/create-tx/CreateErc20WrappedTx.spec.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { Wei } from '@emeraldpay/bigamount-crypto'; -import { BlockchainCode, TokenData, TokenRegistry, amountFactory } from '../../../blockchains'; -import { DEFAULT_GAS_LIMIT_ERC20, EthereumTransactionType } from '../../ethereum'; -import { TxMetaType, TxTarget } from '../types'; -import { CreateErc20WrappedTx } from './CreateErc20WrappedTx'; - -describe('CreateErc20WrappedTx', () => { - const tokenData: TokenData = { - name: 'Wrapped Ether', - blockchain: 100, - address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', - symbol: 'WETH', - decimals: 18, - type: 'ERC20', - }; - - const tokenRegistry = new TokenRegistry([tokenData]); - - const token = tokenRegistry.byAddress(BlockchainCode.ETH, '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'); - - it('creates legacy tx', () => { - const tx = new CreateErc20WrappedTx({ - token: tokenData, - address: '0x2C80BfA8E69fdd12853Fd010A520B29cfa01E2cD', - blockchain: BlockchainCode.Goerli, - meta: { type: TxMetaType.ETHEREUM_WRAP }, - totalBalance: new Wei(0), - totalTokenBalance: token.getAmount(0), - type: EthereumTransactionType.LEGACY, - }); - - expect(tx.amount.number.toNumber()).toBe(0); - expect(tx.gas).toBe(DEFAULT_GAS_LIMIT_ERC20); - expect(tx.gasPrice?.number.toNumber()).toBe(0); - expect(tx.totalBalance.number.toNumber()).toBe(0); - }); - - it('creates eip1559 tx', () => { - const tx = new CreateErc20WrappedTx({ - token: tokenData, - address: '0x2C80BfA8E69fdd12853Fd010A520B29cfa01E2cD', - blockchain: BlockchainCode.Goerli, - meta: { type: TxMetaType.ETHEREUM_WRAP }, - totalBalance: new Wei(0), - totalTokenBalance: token.getAmount(0), - type: EthereumTransactionType.EIP1559, - }); - - expect(tx.amount.number.toNumber()).toBe(0); - expect(tx.gas).toBe(DEFAULT_GAS_LIMIT_ERC20); - expect(tx.maxGasPrice?.number.toNumber()).toBe(0); - expect(tx.priorityGasPrice?.number.toNumber()).toBe(0); - expect(tx.totalBalance.number.toNumber()).toBe(0); - }); - - it('correctly rebalance ETH', () => { - const totalBalance = amountFactory(BlockchainCode.Goerli)(10 ** 18); - - const tx = new CreateErc20WrappedTx({ - totalBalance, - address: '0x2C80BfA8E69fdd12853Fd010A520B29cfa01E2cD', - blockchain: BlockchainCode.Goerli, - meta: { type: TxMetaType.ETHEREUM_WRAP }, - token: tokenData, - totalTokenBalance: token.getAmount(0), - type: EthereumTransactionType.EIP1559, - }); - - expect(tx.target).toBe(TxTarget.MANUAL); - expect(tx.amount.number.toNumber()).toBe(0); - expect(tx.amount.units.equals(totalBalance.units)).toBeTruthy(); - - tx.target = TxTarget.SEND_ALL; - tx.rebalance(); - - expect(tx.target).toBe(TxTarget.SEND_ALL); - expect(tx.amount.number.toString()).toBe('1000000000000000000'); - expect(tx.amount.units.equals(totalBalance.units)).toBeTruthy(); - }); - - it('correctly rebalance WETH', () => { - const tx = new CreateErc20WrappedTx({ - token: tokenData, - address: '0x2C80BfA8E69fdd12853Fd010A520B29cfa01E2cD', - amount: token.getAmount(0), - blockchain: BlockchainCode.Goerli, - meta: { type: TxMetaType.ETHEREUM_WRAP }, - totalBalance: new Wei(0), - totalTokenBalance: token.getAmount(1), - type: EthereumTransactionType.EIP1559, - }); - - expect(tx.target).toBe(TxTarget.MANUAL); - expect(tx.amount.number.toNumber()).toBe(0); - expect(tx.amount.units.equals(token.getUnits())).toBeTruthy(); - - tx.target = TxTarget.SEND_ALL; - tx.rebalance(); - - expect(tx.target).toBe(TxTarget.SEND_ALL); - expect(tx.amount.number.toNumber()).toBe(1); - expect(tx.amount.units.equals(token.getUnits())).toBeTruthy(); - }); -}); diff --git a/packages/core/src/transaction/workflow/create-tx/CreateErc20WrappedTx.ts b/packages/core/src/transaction/workflow/create-tx/CreateErc20WrappedTx.ts deleted file mode 100644 index 2173fc467..000000000 --- a/packages/core/src/transaction/workflow/create-tx/CreateErc20WrappedTx.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { BigAmount } from '@emeraldpay/bigamount'; -import { BlockchainCode, Token, TokenAmount, TokenData, amountFactory, wrapTokenAbi } from '../../../blockchains'; -import { Contract } from '../../../Contract'; -import { DEFAULT_GAS_LIMIT_ERC20, EthereumTransaction, EthereumTransactionType } from '../../ethereum'; -import { CommonTx, TxMetaType, TxTarget, ValidationResult } from '../types'; - -export interface Erc20WrappedTxDetails extends CommonTx { - address?: string; - amount?: BigAmount; - blockchain: BlockchainCode; - gas?: number; - gasPrice?: BigAmount; - maxGasPrice?: BigAmount; - priorityGasPrice?: BigAmount; - target?: TxTarget; - token: TokenData; - totalBalance: BigAmount; - totalTokenBalance: TokenAmount; - type: EthereumTransactionType; -} - -export class CreateErc20WrappedTx implements Erc20WrappedTxDetails { - meta = { type: TxMetaType.ERC20_WRAP }; - - address?: string; - amount: BigAmount; - blockchain: BlockchainCode; - gas: number; - gasPrice?: BigAmount; - maxGasPrice?: BigAmount; - priorityGasPrice?: BigAmount; - target: TxTarget; - totalBalance: BigAmount; - totalTokenBalance: TokenAmount; - type: EthereumTransactionType; - - readonly token: Token; - - private readonly zeroAmount: BigAmount; - - private tokenContract = new Contract(wrapTokenAbi); - - constructor(details: Erc20WrappedTxDetails) { - const zeroAmount = amountFactory(details.blockchain)(0); - - this.address = details.address; - this.amount = details.amount ?? zeroAmount; - this.blockchain = details.blockchain; - this.gas = details.gas ?? DEFAULT_GAS_LIMIT_ERC20; - this.target = details.target ?? TxTarget.MANUAL; - this.totalBalance = details.totalBalance; - this.totalTokenBalance = details.totalTokenBalance; - this.type = details.type; - - this.gasPrice = details.gasPrice ?? zeroAmount; - this.maxGasPrice = details.maxGasPrice ?? zeroAmount; - this.priorityGasPrice = details.priorityGasPrice ?? zeroAmount; - - this.token = new Token(details.token); - this.zeroAmount = zeroAmount; - } - - static fromPlain(details: Erc20WrappedTxDetails): CreateErc20WrappedTx { - return new CreateErc20WrappedTx(details); - } - - build(): EthereumTransaction { - const { amount, blockchain, gas, gasPrice, maxGasPrice, priorityGasPrice, type, address: from = '' } = this; - - const isDeposit = amount.units.equals(this.totalBalance.units); - - const data = isDeposit - ? this.tokenContract.functionToData('deposit', {}) - : this.tokenContract.functionToData('withdraw', { _value: amount.number.toFixed() }); - - return { - blockchain, - data, - gas, - type, - from, - gasPrice, - maxGasPrice, - priorityGasPrice, - to: this.token.address, - value: isDeposit ? amount : this.zeroAmount, - }; - } - - dump(): Erc20WrappedTxDetails { - return { - address: this.address, - amount: this.amount, - blockchain: this.blockchain, - gas: this.gas, - gasPrice: this.gasPrice, - maxGasPrice: this.maxGasPrice, - meta: this.meta, - priorityGasPrice: this.priorityGasPrice, - target: this.target, - token: this.token.toPlain(), - totalBalance: this.totalBalance, - totalTokenBalance: this.totalTokenBalance, - type: this.type, - }; - } - - getFees(): BigAmount { - const gasPrice = - (this.type === EthereumTransactionType.EIP1559 ? this.maxGasPrice : this.gasPrice) ?? this.zeroAmount; - - return gasPrice.multiply(this.gas); - } - - rebalance(): void { - if (this.target === TxTarget.SEND_ALL) { - if (this.amount.units.equals(this.totalBalance.units)) { - const amount = this.totalBalance.minus(this.getFees()); - - if (amount.isZero() || amount.isPositive()) { - this.amount = amountFactory(this.blockchain)(amount.number); - } - } else { - this.amount = this.totalTokenBalance; - } - } - } - - validate(): ValidationResult { - if (this.amount.isZero()) { - return ValidationResult.NO_AMOUNT; - } - - if (this.amount.units.equals(this.totalBalance.units)) { - const total = this.amount.plus(this.getFees()); - - if (total.isGreaterThan(this.totalBalance)) { - return ValidationResult.INSUFFICIENT_FUNDS; - } - } else if (this.amount.isGreaterThan(this.totalTokenBalance)) { - return ValidationResult.INSUFFICIENT_TOKEN_FUNDS; - } - - return ValidationResult.OK; - } -} diff --git a/packages/core/src/transaction/workflow/create-tx/index.ts b/packages/core/src/transaction/workflow/create-tx/index.ts index a577de2c6..21bcf3cbd 100644 --- a/packages/core/src/transaction/workflow/create-tx/index.ts +++ b/packages/core/src/transaction/workflow/create-tx/index.ts @@ -22,6 +22,7 @@ export { isBitcoinSpeedUpCreateTx, isErc20ApproveCreateTx, isErc20CancelCreateTx, + isErc20ConvertCreateTx, isErc20CreateTx, isErc20SpeedUpCreateTx, isEtherCancelCreateTx, @@ -34,9 +35,9 @@ export { BitcoinTx, BitcoinTxDetails, BitcoinTxOrigin, CreateBitcoinTx } from '. export { CreateBitcoinCancelTx } from './CreateBitcoinCancelTx'; export { CreateBitcoinSpeedUpTx } from './CreateBitcoinSpeedUpTx'; export { CreateErc20CancelTx } from './CreateErc20CancelTx'; +export { CreateErc20ConvertTx, Erc20ConvertTxDetails } from './CreateErc20ConvertTx'; export { CreateErc20SpeedUpTx } from './CreateErc20SpeedUpTx'; export { CreateErc20Tx, Erc20TxDetails } from './CreateErc20Tx'; -export { CreateErc20WrappedTx, Erc20WrappedTxDetails } from './CreateErc20WrappedTx'; export { CreateEtherCancelTx } from './CreateEtherCancelTx'; export { CreateEtherSpeedUpTx } from './CreateEtherSpeedUpTx'; export { CreateEtherTx, TxDetails } from './CreateEtherTx'; diff --git a/packages/core/src/transaction/workflow/create-tx/types.ts b/packages/core/src/transaction/workflow/create-tx/types.ts index b8b901671..b17daae40 100644 --- a/packages/core/src/transaction/workflow/create-tx/types.ts +++ b/packages/core/src/transaction/workflow/create-tx/types.ts @@ -1,12 +1,22 @@ import { BigAmount } from '@emeraldpay/bigamount'; import BigNumber from 'bignumber.js'; import { TokenRegistry } from '../../../blockchains'; -import { AnyPlainTx, BitcoinPlainTx, CommonTx, EthereumPlainTx, TxMetaType, isBitcoinPlainTx } from '../types'; +import { + AnyPlainTx, + BitcoinPlainTx, + CommonTx, + EthereumPlainTx, + TxMetaType, + isBitcoinPlainTx, + isErc20ApprovePlainTx, + isErc20ConvertPlainTx, +} from '../types'; import { CreateBitcoinCancelTx } from './CreateBitcoinCancelTx'; import { CreateBitcoinSpeedUpTx } from './CreateBitcoinSpeedUpTx'; import { BitcoinTxOrigin, CreateBitcoinTx } from './CreateBitcoinTx'; import { CreateErc20ApproveTx } from './CreateErc20ApproveTx'; import { CreateErc20CancelTx } from './CreateErc20CancelTx'; +import { CreateErc20ConvertTx } from './CreateErc20ConvertTx'; import { CreateErc20SpeedUpTx } from './CreateErc20SpeedUpTx'; import { CreateErc20Tx } from './CreateErc20Tx'; import { CreateEtherCancelTx } from './CreateEtherCancelTx'; @@ -25,7 +35,7 @@ export interface EthereumTx extends CommonTx { export type AnyBitcoinCreateTx = CreateBitcoinTx | CreateBitcoinCancelTx | CreateBitcoinSpeedUpTx; export type AnyEtherCreateTx = CreateEtherTx | CreateEtherCancelTx | CreateEtherSpeedUpTx; export type AnyErc20CreateTx = CreateErc20Tx | CreateErc20CancelTx | CreateErc20SpeedUpTx; -export type AnyContractCreateTx = AnyErc20CreateTx | CreateErc20ApproveTx; +export type AnyContractCreateTx = AnyErc20CreateTx | CreateErc20ApproveTx | CreateErc20ConvertTx; export type AnyEthereumCreateTx = AnyEtherCreateTx | AnyContractCreateTx; export type AnyCreateTx = AnyBitcoinCreateTx | AnyEthereumCreateTx; @@ -50,7 +60,7 @@ const erc20TxMetaTypes: Readonly = [ const contractTxMetaTypes: Readonly = [ ...erc20TxMetaTypes, TxMetaType.ERC20_APPROVE, - TxMetaType.ERC20_WRAP, + TxMetaType.ERC20_CONVERT, ]; export function isAnyBitcoinCreateTx(createTx: AnyCreateTx): createTx is CreateBitcoinTx { @@ -105,6 +115,10 @@ export function isErc20CancelCreateTx(createTx: AnyCreateTx): createTx is Create return createTx.meta.type === TxMetaType.ERC20_CANCEL; } +export function isErc20ConvertCreateTx(createTx: AnyCreateTx): createTx is CreateErc20ConvertTx { + return createTx.meta.type === TxMetaType.ERC20_CONVERT; +} + export function isErc20SpeedUpCreateTx(createTx: AnyCreateTx): createTx is CreateErc20SpeedUpTx { return createTx.meta.type === TxMetaType.ERC20_SPEEDUP; } @@ -138,20 +152,26 @@ export function fromEtherPlainTx(transaction: EthereumPlainTx): AnyEtherCreateTx export function fromErc20PlainTx(transaction: EthereumPlainTx, tokenRegistry: TokenRegistry): AnyContractCreateTx { switch (transaction.meta.type) { case TxMetaType.ERC20_APPROVE: - return CreateErc20ApproveTx.fromPlain(tokenRegistry, transaction); + return CreateErc20ApproveTx.fromPlain(transaction, tokenRegistry); case TxMetaType.ERC20_CANCEL: - return CreateErc20CancelTx.fromPlain(tokenRegistry, transaction); + return CreateErc20CancelTx.fromPlain(transaction, tokenRegistry); + case TxMetaType.ERC20_CONVERT: + return CreateErc20ConvertTx.fromPlain(transaction, tokenRegistry); case TxMetaType.ERC20_SPEEDUP: - return CreateErc20SpeedUpTx.fromPlain(tokenRegistry, transaction); + return CreateErc20SpeedUpTx.fromPlain(transaction, tokenRegistry); case TxMetaType.ERC20_TRANSFER: - return CreateErc20Tx.fromPlain(tokenRegistry, transaction); + return CreateErc20Tx.fromPlain(transaction, tokenRegistry); } throw new Error(`Unsupported transaction meta type ${transaction.meta.type}`); } export function fromEthereumPlainTx(transaction: EthereumPlainTx, tokenRegistry: TokenRegistry): AnyEthereumCreateTx { - if (tokenRegistry.hasAddress(transaction.blockchain, transaction.asset)) { + if ( + isErc20ApprovePlainTx(transaction) || + isErc20ConvertPlainTx(transaction) || + tokenRegistry.hasAddress(transaction.blockchain, transaction.asset) + ) { return fromErc20PlainTx(transaction, tokenRegistry); } diff --git a/packages/core/src/transaction/workflow/index.ts b/packages/core/src/transaction/workflow/index.ts index 38a6028db..549e8cc42 100644 --- a/packages/core/src/transaction/workflow/index.ts +++ b/packages/core/src/transaction/workflow/index.ts @@ -16,15 +16,15 @@ export { CreateBitcoinTx, CreateErc20ApproveTx, CreateErc20CancelTx, + CreateErc20ConvertTx, CreateErc20SpeedUpTx, CreateErc20Tx, - CreateErc20WrappedTx, CreateEtherCancelTx, CreateEtherSpeedUpTx, CreateEtherTx, Erc20ApproveTxDetails, + Erc20ConvertTxDetails, Erc20TxDetails, - Erc20WrappedTxDetails, EthereumTx, TxDetails, fromBitcoinPlainTx, @@ -41,6 +41,7 @@ export { isBitcoinSpeedUpCreateTx, isErc20ApproveCreateTx, isErc20CancelCreateTx, + isErc20ConvertCreateTx, isErc20CreateTx, isErc20SpeedUpCreateTx, isEtherCancelCreateTx, @@ -53,6 +54,7 @@ export { BitcoinFeeRange, BitcoinPlainTx, Erc20ApprovePlainTx, + Erc20ConvertPlainTx, EthereumBasicPlainTx, EthereumFeeRange, EthereumPlainTx, @@ -64,6 +66,7 @@ export { isBitcoinFeeRange, isBitcoinPlainTx, isErc20ApprovePlainTx, + isErc20ConvertPlainTx, isEthereumBasicPlainTx, isEthereumFeeRange, isEthereumPlainTx, diff --git a/packages/core/src/transaction/workflow/types.ts b/packages/core/src/transaction/workflow/types.ts index f2567445f..0a1ae7868 100644 --- a/packages/core/src/transaction/workflow/types.ts +++ b/packages/core/src/transaction/workflow/types.ts @@ -16,9 +16,9 @@ export enum TxMetaType { ETHER_TRANSFER, ERC20_APPROVE, ERC20_CANCEL, + ERC20_CONVERT, ERC20_SPEEDUP, ERC20_TRANSFER, - ERC20_WRAP, } export enum ValidationResult { @@ -77,6 +77,10 @@ export interface Erc20ApprovePlainTx extends EthereumPlainTx { approveBy?: string; } +export interface Erc20ConvertPlainTx extends EthereumPlainTx { + address?: string; +} + export type AnyPlainTx = BitcoinPlainTx | EthereumPlainTx; const ethereumBasicTxMetaTypes: Readonly = [ @@ -104,6 +108,10 @@ export function isErc20ApprovePlainTx(transaction: AnyPlainTx): transaction is E return transaction.meta.type === TxMetaType.ERC20_APPROVE; } +export function isErc20ConvertPlainTx(transaction: AnyPlainTx): transaction is Erc20ApprovePlainTx { + return transaction.meta.type === TxMetaType.ERC20_CONVERT; +} + export interface BitcoinFeeRange { std: number; min: number; diff --git a/packages/react-app/src/app/screen/Screen/Screen.tsx b/packages/react-app/src/app/screen/Screen/Screen.tsx index a358a2fda..565765d09 100644 --- a/packages/react-app/src/app/screen/Screen/Screen.tsx +++ b/packages/react-app/src/app/screen/Screen/Screen.tsx @@ -14,7 +14,6 @@ import SignMessage from '../../../message/SignMessage'; import ReceiveScreen from '../../../receive/ReceiveScreen'; import Settings from '../../../settings/Settings'; import { BroadcastEthTx } from '../../../transaction/BroadcastEthTx'; -import CreateConvertTransaction from '../../../transaction/CreateConvertTransaction'; import CreateRecoverTransaction from '../../../transaction/CreateRecoverTransaction'; import { CreateTransaction } from '../../../transaction/CreateTransaction'; import TxDetails from '../../../transactions/TxDetails'; @@ -62,8 +61,6 @@ const Screen: React.FC = ({ restoreData, screenItem, term return ; case screen.Pages.CREATE_TX: return ; - case screen.Pages.CREATE_TX_CONVERT: - return ; case screen.Pages.CREATE_TX_RECOVER: return ; case screen.Pages.CREATE_WALLET: diff --git a/packages/react-app/src/transaction/CreateConvertTransaction/CreateConvertTransaction.tsx b/packages/react-app/src/transaction/CreateConvertTransaction/CreateConvertTransaction.tsx deleted file mode 100644 index b1731c4a4..000000000 --- a/packages/react-app/src/transaction/CreateConvertTransaction/CreateConvertTransaction.tsx +++ /dev/null @@ -1,572 +0,0 @@ -import { BigAmount, CreateAmount } from '@emeraldpay/bigamount'; -import { WeiAny } from '@emeraldpay/bigamount-crypto'; -import { WalletEntry, isEthereumEntry } from '@emeraldpay/emerald-vault-core'; -import { - BlockchainCode, - Blockchains, - CoinTicker, - EthereumTransaction, - EthereumTransactionType, - Token, - TokenAmount, - TokenRegistry, - amountFactory, - blockchainIdToCode, - formatAmount, - workflow, -} from '@emeraldwallet/core'; -import { FEE_KEYS, GasPrices, IState, SignData, accounts, screen, tokens, transaction } from '@emeraldwallet/store'; -import { AccountSelect, Back, Button, ButtonGroup, FormLabel, FormRow, Page, PasswordInput } from '@emeraldwallet/ui'; -import { CircularProgress, Typography, createStyles, makeStyles } from '@material-ui/core'; -import { ToggleButton, ToggleButtonGroup } from '@material-ui/lab'; -import * as React from 'react'; -import { connect } from 'react-redux'; -import { AmountField } from '../../common/AmountField'; -import { EthTxSettings } from '../../common/EthTxSettings'; -import WaitLedger from '../../ledger/WaitLedger'; - -const useStyles = makeStyles( - createStyles({ - buttons: { - display: 'flex', - justifyContent: 'end', - width: '100%', - }, - }), -); - -enum Stages { - SETUP = 'setup', - SIGN = 'sign', -} - -interface OwnProps { - contractAddress: string; - entry: WalletEntry; -} - -interface StateProps { - addresses: Record; - blockchain: BlockchainCode; - coinTicker: CoinTicker; - isHardware: boolean; - supportEip1559: boolean; - token: Token; - getBalance(address?: string): BigAmount; - getBalancesByAddress(address: string): string[]; - getEntryByAddress(address: string): WalletEntry | undefined; - getTokenBalanceByAddress(address?: string): TokenAmount; -} - -interface DispatchProps { - estimateGas(tx: EthereumTransaction): Promise; - getFees(blockchain: BlockchainCode): Promise>; - goBack(): void; - signTransaction(entryId: string, tx: workflow.CreateErc20WrappedTx, token: Token, password?: string): Promise; - verifyGlobalKey(password: string): Promise; -} - -const CreateConvertTransaction: React.FC = ({ - addresses, - blockchain, - coinTicker, - entry: { address }, - isHardware, - supportEip1559, - token, - estimateGas, - getBalance, - getBalancesByAddress, - getEntryByAddress, - getFees, - getTokenBalanceByAddress, - goBack, - signTransaction, - verifyGlobalKey, -}) => { - const styles = useStyles(); - - const mounted = React.useRef(true); - - const [initializing, setInitializing] = React.useState(true); - const [preparing, setPreparing] = React.useState(false); - const [verifying, setVerifying] = React.useState(false); - - const [convertable, setConvertable] = React.useState(coinTicker); - - const [convertTx, setConvertTx] = React.useState(() => { - const tx = new workflow.CreateErc20WrappedTx({ - blockchain, - token, - address: address?.value, - meta: { type: workflow.TxMetaType.ERC20_WRAP }, - totalBalance: getBalance(address?.value), - totalTokenBalance: getTokenBalanceByAddress(address?.value), - type: supportEip1559 ? EthereumTransactionType.EIP1559 : EthereumTransactionType.LEGACY, - }); - - return tx.dump(); - }); - - const [useEip1559, setUseEip1559] = React.useState(supportEip1559); - - const factory = amountFactory(blockchain) as CreateAmount; - const zeroAmount = factory(0); - - const [maxGasPrice, setMaxGasPrice] = React.useState(zeroAmount); - const [priorityGasPrice, setPriorityGasPrice] = React.useState(zeroAmount); - - const [stdMaxGasPrice, setStdMaxGasPrice] = React.useState(zeroAmount); - const [highMaxGasPrice, setHighMaxGasPrice] = React.useState(zeroAmount); - const [lowMaxGasPrice, setLowMaxGasPrice] = React.useState(zeroAmount); - - const [stdPriorityGasPrice, setStdPriorityGasPrice] = React.useState(zeroAmount); - const [highPriorityGasPrice, setHighPriorityGasPrice] = React.useState(zeroAmount); - const [lowPriorityGasPrice, setLowPriorityGasPrice] = React.useState(zeroAmount); - - const [stage, setStage] = React.useState(Stages.SETUP); - - const [password, setPassword] = React.useState(); - const [passwordError, setPasswordError] = React.useState(); - - const onChangeConvertable = (event: React.MouseEvent, value: string): void => { - const converting = value ?? convertable; - - const tx = workflow.CreateErc20WrappedTx.fromPlain(convertTx); - - const { number: amount } = tx.amount; - - tx.amount = converting === coinTicker ? factory(amount) : token.getAmount(amount); - tx.target = workflow.TxTarget.MANUAL; - tx.rebalance(); - - setConvertable(converting); - setConvertTx(tx.dump()); - }; - - const onChangeAddress = (address: string): void => { - const tx = workflow.CreateErc20WrappedTx.fromPlain(convertTx); - - tx.address = address; - tx.totalBalance = getBalance(address); - tx.totalTokenBalance = getTokenBalanceByAddress(address); - tx.rebalance(); - - setConvertTx(tx.dump()); - }; - - const onChangeAmount = (amount: BigAmount): void => { - const tx = workflow.CreateErc20WrappedTx.fromPlain(convertTx); - - tx.amount = amount; - tx.target = workflow.TxTarget.MANUAL; - - setConvertTx(tx.dump()); - }; - - const onClickMaxAmount = (callback: (value: BigAmount) => void): void => { - const tx = workflow.CreateErc20WrappedTx.fromPlain(convertTx); - - tx.target = workflow.TxTarget.SEND_ALL; - tx.rebalance(); - - callback(tx.amount); - - setConvertTx(tx.dump()); - }; - - const onUseEip1559Change = (checked: boolean): void => { - setUseEip1559(checked); - - const tx = workflow.CreateErc20WrappedTx.fromPlain(convertTx); - - if (checked) { - tx.gasPrice = undefined; - tx.maxGasPrice = maxGasPrice; - tx.priorityGasPrice = maxGasPrice; - } else { - tx.gasPrice = maxGasPrice; - tx.maxGasPrice = undefined; - tx.priorityGasPrice = undefined; - } - - tx.type = checked ? EthereumTransactionType.EIP1559 : EthereumTransactionType.LEGACY; - - setConvertTx(tx.dump()); - }; - - const onMaxGasPriceChange = (price: WeiAny): void => { - setMaxGasPrice(price); - - const tx = workflow.CreateErc20WrappedTx.fromPlain(convertTx); - - if (useEip1559) { - tx.gasPrice = undefined; - tx.maxGasPrice = price; - } else { - tx.gasPrice = price; - tx.maxGasPrice = undefined; - } - - tx.rebalance(); - - setConvertTx(tx.dump()); - }; - - const onPriorityGasPriceChange = (price: WeiAny): void => { - setPriorityGasPrice(price); - - const tx = workflow.CreateErc20WrappedTx.fromPlain(convertTx); - - tx.priorityGasPrice = price; - tx.rebalance(); - - setConvertTx(tx.dump()); - }; - - const onCreateTransaction = async (): Promise => { - setPreparing(true); - - const tx = workflow.CreateErc20WrappedTx.fromPlain(convertTx); - - tx.gas = await estimateGas(tx.build()); - tx.rebalance(); - - setConvertTx(tx.dump()); - setStage(Stages.SIGN); - - setPreparing(false); - }; - - const onSignTransaction = async (): Promise => { - setPasswordError(undefined); - - const tx = workflow.CreateErc20WrappedTx.fromPlain(convertTx); - - if (tx.address == null) { - return; - } - - const entry = getEntryByAddress(tx.address); - - if (entry == null) { - return; - } - - if (isHardware) { - await signTransaction(entry.id, tx, token); - } else { - if (password == null) { - return; - } - - setVerifying(true); - - const correctPassword = await verifyGlobalKey(password); - - if (correctPassword) { - await signTransaction(entry.id, tx, token, password); - } else { - setPasswordError('Incorrect password'); - } - - if (mounted.current) { - setVerifying(false); - } - } - }; - - const onPasswordEnter = async (): Promise => { - if (!verifying && (password?.length ?? 0) > 0) { - await onSignTransaction(); - } - }; - - React.useEffect( - () => { - getFees(blockchain).then(({ avgLast, avgMiddle, avgTail5 }) => { - if (mounted.current) { - const newStdMaxGasPrice = factory(avgTail5.max) as WeiAny; - const newStdPriorityGasPrice = factory(avgTail5.priority) as WeiAny; - - setStdMaxGasPrice(newStdMaxGasPrice); - setHighMaxGasPrice(factory(avgMiddle.max) as WeiAny); - setLowMaxGasPrice(factory(avgLast.max) as WeiAny); - - setStdPriorityGasPrice(newStdPriorityGasPrice); - setHighPriorityGasPrice(factory(avgMiddle.priority) as WeiAny); - setLowPriorityGasPrice(factory(avgLast.priority) as WeiAny); - - setMaxGasPrice(newStdMaxGasPrice); - setPriorityGasPrice(newStdPriorityGasPrice); - - const tx = workflow.CreateErc20WrappedTx.fromPlain(convertTx); - - if (supportEip1559) { - tx.gasPrice = undefined; - tx.maxGasPrice = newStdMaxGasPrice; - tx.priorityGasPrice = newStdPriorityGasPrice; - } else { - tx.gasPrice = newStdMaxGasPrice; - tx.maxGasPrice = undefined; - tx.priorityGasPrice = undefined; - } - - tx.rebalance(); - - setConvertTx(tx.dump()); - setInitializing(false); - } - }); - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [], - ); - - React.useEffect(() => { - return () => { - mounted.current = false; - }; - }, []); - - const currentTx = workflow.CreateErc20WrappedTx.fromPlain(convertTx); - - return ( - }> - {stage === Stages.SETUP && ( - <> - - - - - Ether to {token.symbol} - - - {token.symbol} to Ether - - - - - From - getBalancesByAddress(address)} - onChange={onChangeAddress} - /> - - - Amount - - - - - - - {initializing && ( -