Repository navigation
Expand file tree
/
Copy pathTokenTransferService.ts
More file actions
232 lines (202 loc) · 9.44 KB
/
Copy pathTokenTransferService.ts
File metadata and controls
232 lines (202 loc) · 9.44 KB
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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
import di from 'a-di';
import { EoAccount, IAccount, SafeAccount, TAccount } from "dequanto/models/TAccount";
import { Web3Client } from 'dequanto/clients/Web3Client';
import { IToken } from 'dequanto/models/IToken';
import { TAddress } from 'dequanto/models/TAddress';
import { TxDataBuilder } from 'dequanto/txs/TxDataBuilder';
import { ITxWriterOptions, TxWriter } from 'dequanto/txs/TxWriter';
import { $bigint } from 'dequanto/utils/$bigint';
import { TokensService } from './TokensService';
import { $address } from 'dequanto/utils/$address';
import { ITxBuilderOptions } from 'dequanto/txs/ITxBuilderOptions';
import { $promise } from 'dequanto/utils/$promise';
import { LoggerService } from 'dequanto/loggers/LoggerService';
import { $logger } from 'dequanto/utils/$logger';
import { $account } from 'dequanto/utils/$account';
import { TEth } from 'dequanto/models/TEth';
import { $require } from 'dequanto/utils/$require';
export class TokenTransferService {
private gasPriorityFee?: number
private builderConfig: ITxBuilderOptions
private writerConfig: ITxWriterOptions
private tokenService: TokensService
constructor (public client: Web3Client, private logger = di.resolve(LoggerService)) {
this.tokenService = di.resolve(TokensService, this.client.platform);
}
$config (builderConfig: ITxBuilderOptions): this {
this.builderConfig = builderConfig;
return this;
}
$configWriter (writerConfig: ITxWriterOptions): this {
this.writerConfig = writerConfig;
return this;
}
async getBalance(address: TAddress, token: string | IToken): Promise<bigint> {
token = await this.getToken(token);
let isNative = this.tokenService.isNative(token.address);
if (isNative) {
return this.client.getBalance(address);
}
let erc20 = await TokensService.erc20(token, this.client.platform);
let balance = await erc20.balanceOf(address);
return balance;
}
async getReceived(receipt: TEth.TxReceipt): Promise<bigint> {
let receiver = receipt.from;
let ANYTOKEN = 'USDC';
let erc20 = await TokensService.erc20(ANYTOKEN, this.client.platform);
let transfers = erc20.extractLogsTransfer(receipt)
let transfer = transfers.find(x => $address.eq(x.params.to, receiver));
return transfer?.params.value ?? 0n;
}
/** Returns NULL for transaction, if no balance to transfer */
async transferAll (from: EoAccount, to: TAddress, token: string | IToken) : Promise<TxWriter> {
token = await this.getToken(token);
let isNative = this.tokenService.isNative(token.address);
if (isNative) {
return this.transferNativeAll(from, to);
}
return this.transferErc20All(from, to, token);
}
async transferAllWithRemainder (from: EoAccount, to: TAddress, token: string | IToken, remainder: number | bigint) : Promise<TxWriter> {
token = await this.getToken(token);
let isNative = this.tokenService.isNative(token.address);
if (isNative) {
return this.transferNativeAll(from, to, { remainder });
}
return this.transferErc20All(from, to, token, { remainder });
}
async transfer (from: IAccount, to: TAddress, token: string | IToken, amount: number | bigint) : Promise<TxWriter> {
token = await this.getToken(token);
amount = this.getAmount(amount, token);
let isNative = this.tokenService.isNative(token);
if (isNative) {
return this.transferNative(from, to, amount);
}
return this.transferErc20(from, to, token, amount);
}
async getToken (token: string | IToken): Promise<IToken> {
if (typeof token === 'string') {
token = await this.tokenService.getKnownToken(token);
}
$require.notNull(token, 'Token is undefined to transfer');
return token;
}
isNativeToken (token: string | IToken): boolean {
return this.tokenService.isNative(typeof token === 'string' ? token : (token.address ?? token.symbol));
}
private async transferNativeAll (from: EoAccount, to: TAddress, opts?: { remainder: number | bigint }): Promise<TxWriter> {
let buildTxRetries = 1;
const buildTx = async () => {
const GAS = 21000;
const GAS_RATIO = 1.05;
let [ balance, gasPrice] = await Promise.all([
this.client.getBalance(from.address),
this.client.getGasPrice()
]);
if (opts?.remainder != null) {
let remainder = this.getAmount(opts.remainder, 18);
balance -= remainder;
}
let $gasPrice = $bigint.multWithFloat(gasPrice.price, GAS_RATIO);
$logger.log(`GasPrice ${ $bigint.toGweiFromWei($gasPrice) }gwei`);
let gasConsumed = BigInt(GAS) * $gasPrice;
let transferValue = balance - gasConsumed;
if (transferValue <= 0) {
if (--buildTxRetries > -1) {
$logger.log(`No balance to transfer. Retry in 5s`);
await $promise.wait(5000);
return buildTx();
}
// No balance to transfer
$logger.log(`No balance to transfer in ${from.address}. ${ opts?.remainder ? ("Remainder: " + opts?.remainder) : ""}`);
return null;
}
let txBuilder = new TxDataBuilder(this.client, from, {
to: to,
value: $bigint.toHex(transferValue)
}, this.builderConfig);
txBuilder.data.gasPrice = $bigint.toHex($gasPrice);
// txBuilder.data.maxFeePerGas = $bigint.toHex($gasPrice - 20n**9n);
// txBuilder.data.maxPriorityFeePerGas = $bigint.toHex(20n**9n);
txBuilder.data.gas = GAS;
txBuilder.data.type = 1;
txBuilder.setConfig(this.builderConfig);
await Promise.all([
txBuilder.setNonce({ overriding: true }),
]);
$logger.log(`TransferNative ALL. Balance ${balance}; GasConsumed ${gasConsumed}; TransferValue ${transferValue}; Nonce: ${txBuilder.data.nonce}`);
return txBuilder;
}
let txBuilder = await buildTx();
return TxWriter.write(this.client, txBuilder, from, {
...(this.writerConfig ?? {}),
retries: 3,
async onErrorRebuild (tx, error, errCount) {
// If we got the `balance` value, but it was outdated, then all our calculations were wrong
// Retry the calculation and transfer once again
if (/insufficient funds/.test(error.message)) {
const ms = 6000 * errCount;
$logger.log(`TokenTransfer Failed: insufficient funds. Waiting ${ms}ms to retry`);
await $promise.wait(ms);
return buildTx();
}
return null;
}
});
}
private async transferNative (from: IAccount, to: TAddress, amount: bigint): Promise<TxWriter> {
let sender = $account.getSender(from);
let txBuilder = new TxDataBuilder(this.client, sender, {
to: to,
value: $bigint.toHex(amount)
}, this.builderConfig);
if (sender.address) {
await Promise.all([
txBuilder.setGas({ priceRatio: this.gasPriorityFee, gasEstimation: true, gasLimitRatio: 1, }),
txBuilder.setNonce(),
]);
}
txBuilder.setConfig(this.builderConfig);
return TxWriter.write(this.client, txBuilder, from, this.writerConfig);
}
private async transferErc20All (from: EoAccount, to: TAddress, token: IToken, opts?: { remainder?: number | bigint, retryCount?: number }): Promise<TxWriter> {
let erc20 = await TokensService.erc20(token, this.client.platform);
let balance = await erc20.balanceOf(from.address);
if (opts?.remainder != null) {
let remainder = this.getAmount(opts.remainder, 18);
balance -= remainder;
}
if (balance <= 0n) {
if (opts?.retryCount == null || opts?.retryCount !== 0) {
// re-read
this.logger.warn(`TransferErc20All has no balance to transfer: ${token.address}. Balance re-fetch scheduled.`);
await $promise.wait(4000);
return this.transferErc20All(from, to, token, {
...(opts ?? {}),
retryCount: 0
});
}
if (this.builderConfig.gasFunding == null) {
return null;
}
}
return erc20
.$config(this.builderConfig, this.writerConfig)
.transfer(from, to, balance);
}
private async transferErc20 (from: TAccount, to: TAddress, token: IToken, amount: bigint): Promise<TxWriter> {
let erc20 = await TokensService.erc20(token, this.client.platform);
return erc20
.$config(this.builderConfig, this.writerConfig)
.transfer(from, to, amount);
}
private getAmount (amount: number | bigint, token: IToken): bigint
private getAmount (amount: number | bigint, decimals: number): bigint
private getAmount (amount: number | bigint, mix: number | IToken): bigint {
if (typeof amount === 'number') {
amount = $bigint.toWei(amount, typeof mix === 'number' ? mix : mix.decimals);
}
return amount;
}
}