/
approve-erc20-spender-step.ts
126 lines (114 loc) · 3.67 KB
/
approve-erc20-spender-step.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
import {
RecipeERC20Info,
StepConfig,
StepInput,
StepOutputERC20Amount,
UnvalidatedStepOutput,
} from '../../../models/export-models';
import { Step } from '../../step';
import { ERC20Contract } from '../../../contract/token/erc20-contract';
import { compareERC20Info } from '../../../utils/token';
import { createNoActionStepOutput } from '../../../utils/no-action-output';
import {
maxBigNumberForTransaction,
minBigNumber,
} from '../../../utils/big-number';
import { NetworkName, isDefined } from '@railgun-community/shared-models';
import { ContractTransaction } from 'ethers';
export class ApproveERC20SpenderStep extends Step {
readonly config: StepConfig = {
name: 'Approve ERC20 Spender',
description: 'Approves ERC20 for spender contract.',
};
private readonly spender: Optional<string>;
private readonly tokenInfo: RecipeERC20Info;
private readonly amount: Optional<bigint>;
constructor(
spender: Optional<string>,
tokenInfo: RecipeERC20Info,
amount?: bigint,
) {
super();
this.spender = spender;
this.tokenInfo = tokenInfo;
this.amount = amount;
}
protected async getStepOutput(
input: StepInput,
): Promise<UnvalidatedStepOutput> {
if (!isDefined(this.spender) || (this.tokenInfo.isBaseToken ?? false)) {
return createNoActionStepOutput(input);
}
const { erc20Amounts, networkName } = input;
const { erc20AmountForStep, unusedERC20Amounts } =
this.getValidInputERC20Amount(
erc20Amounts,
erc20Amount =>
compareERC20Info(erc20Amount, this.tokenInfo) &&
erc20Amount.approvedSpender !== this.spender,
this.amount,
);
const contract = new ERC20Contract(erc20AmountForStep.tokenAddress);
const approveAmount = this.amount ?? maxBigNumberForTransaction();
const crossContractCalls: ContractTransaction[] = [];
if (
this.requiresClearApprovalTransaction(
networkName,
erc20AmountForStep.tokenAddress,
)
) {
crossContractCalls.push(
await contract.createSpenderApproval(this.spender, 0n),
);
}
crossContractCalls.push(
await contract.createSpenderApproval(this.spender, approveAmount),
);
const approvedERC20Amount: StepOutputERC20Amount = {
tokenAddress: erc20AmountForStep.tokenAddress,
decimals: erc20AmountForStep.decimals,
isBaseToken: erc20AmountForStep.isBaseToken,
expectedBalance: minBigNumber(
approveAmount,
erc20AmountForStep.expectedBalance,
),
minBalance: minBigNumber(approveAmount, erc20AmountForStep.minBalance),
approvedSpender: this.spender,
};
return {
crossContractCalls,
outputERC20Amounts: [approvedERC20Amount, ...unusedERC20Amounts],
outputNFTs: input.nfts,
};
}
/**
* Certain tokens require a clear approval transaction before approving a new spender.
*/
private requiresClearApprovalTransaction(
networkName: NetworkName,
tokenAddress: string,
): boolean {
switch (networkName) {
case NetworkName.Ethereum:
if (
[
'0xdac17f958d2ee523a2206206994597c13d831ec7', // USDT
].includes(tokenAddress.toLowerCase())
) {
return true;
}
return false;
case NetworkName.BNBChain:
case NetworkName.Polygon:
case NetworkName.Arbitrum:
case NetworkName.EthereumSepolia:
case NetworkName.Hardhat:
case NetworkName.PolygonAmoy:
case NetworkName.EthereumRopsten_DEPRECATED:
case NetworkName.EthereumGoerli_DEPRECATED:
case NetworkName.PolygonMumbai_DEPRECATED:
case NetworkName.ArbitrumGoerli_DEPRECATED:
return false;
}
}
}