@dripfi/drip-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DripApi.ts +133 -0
- package/DripConfig.ts +38 -0
- package/DripSdk.ts +516 -0
- package/index.ts +5 -0
- package/package.json +16 -0
- package/types/QLFastRedeem.ts +15 -0
- package/types/SwapInfo.ts +5 -0
- package/types/UserBalance.ts +7 -0
- package/types/Vault.ts +68 -0
- package/utils.ts +12 -0
package/DripApi.ts
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
import { BigNumber } from "ethers"
|
2
|
+
import { Vault } from "./types/Vault"
|
3
|
+
import { SwapInfo } from "./types/SwapInfo"
|
4
|
+
import { QLFastRedeem } from "./types/QLFastRedeem"
|
5
|
+
|
6
|
+
const WETH_TOKEN_ADDRESS = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'
|
7
|
+
|
8
|
+
export default class DripApi {
|
9
|
+
|
10
|
+
route: string
|
11
|
+
|
12
|
+
constructor(route: string) {
|
13
|
+
this.route = route
|
14
|
+
}
|
15
|
+
|
16
|
+
async fetchAllVaults(): Promise<Vault[]> {
|
17
|
+
const res = await fetch(`${this.route}/api-be/api/vault`)
|
18
|
+
const data: Vault[] = await res.json() as Vault[]
|
19
|
+
return data
|
20
|
+
}
|
21
|
+
|
22
|
+
async fetchVaultDetails(vaultAddress: string): Promise<Vault> {
|
23
|
+
const res = await fetch(`${this.route}/api-be/api/vault/${vaultAddress}`)
|
24
|
+
const data: Vault = await res.json() as Vault
|
25
|
+
return data
|
26
|
+
}
|
27
|
+
|
28
|
+
|
29
|
+
async getSwapInfo(fromTokenAddress: string, toTokenAddress: string, amount: BigNumber, fromAddress: string,): Promise<SwapInfo[]> {
|
30
|
+
if (fromTokenAddress === toTokenAddress && fromTokenAddress === WETH_TOKEN_ADDRESS) {
|
31
|
+
return []
|
32
|
+
}
|
33
|
+
|
34
|
+
const url = `${this.route}/oneinch?getRequest=/swap/v5.2/1/swap?fromTokenAddress=${fromTokenAddress}%26toTokenAddress=${toTokenAddress}%26amount=${amount.toString()}%26fromAddress=${fromAddress}%26slippage=0.1%26disableEstimate=true%26allowPartialFill=false%26includeTokensInfo=true`
|
35
|
+
const res = await fetch(url)
|
36
|
+
const data = await res.json() as any
|
37
|
+
return [
|
38
|
+
{
|
39
|
+
swapTarget: data.tx.to,
|
40
|
+
token: data.fromToken.address,
|
41
|
+
swapCallData: data.tx.data,
|
42
|
+
}
|
43
|
+
]
|
44
|
+
}
|
45
|
+
|
46
|
+
async fetchUserSVTBalance(vaultAddress: string, walletAddress: string, token: string): Promise<BigNumber> {
|
47
|
+
const headers = new Headers()
|
48
|
+
headers.append('Authorization', token)
|
49
|
+
const res = await fetch(`${this.route}/api-be/api/spool/user/svtBalance/${walletAddress}/${vaultAddress}`, {
|
50
|
+
headers,
|
51
|
+
})
|
52
|
+
|
53
|
+
const data = await res.json() as BigNumber
|
54
|
+
return data
|
55
|
+
}
|
56
|
+
|
57
|
+
async fetchUserBalance(vaultAddress: string, walletAddress: string, token: string): Promise<Record<string, string>> {
|
58
|
+
const headers = new Headers()
|
59
|
+
headers.append('Authorization', token)
|
60
|
+
const res = await fetch(`${this.route}/api-be/api/spool/userBalance/${walletAddress}/${vaultAddress}`, {
|
61
|
+
headers,
|
62
|
+
})
|
63
|
+
const data = await res.json() as Record<string, string>
|
64
|
+
return data
|
65
|
+
}
|
66
|
+
|
67
|
+
async fetchEnrichedUserDNFTForVault(vaultAddress: string, walletAddress: string, token: string): Promise<any[]> {
|
68
|
+
const headers = new Headers()
|
69
|
+
headers.append('Authorization', token)
|
70
|
+
const res = await fetch(`${this.route}/api-be/api/spool/user/dNft/${walletAddress}/${vaultAddress}`, {
|
71
|
+
headers,
|
72
|
+
})
|
73
|
+
const data = await res.json()
|
74
|
+
return data as any[]
|
75
|
+
}
|
76
|
+
|
77
|
+
async fetchUserSVTFromNfts(vaultAddress: string, userAddress: string, dnfts: number[], token: string): Promise<BigNumber[]> {
|
78
|
+
const headers = new Headers()
|
79
|
+
headers.append('Authorization', token)
|
80
|
+
const res = await fetch(`${this.route}/api-be/api/spool/user/svtFromNft/${userAddress}/${vaultAddress}?dnfts=${dnfts.join(',')}`, {
|
81
|
+
headers: {
|
82
|
+
Authorization: token,
|
83
|
+
},
|
84
|
+
})
|
85
|
+
|
86
|
+
const data = await res.json() as BigNumber[]
|
87
|
+
return data
|
88
|
+
}
|
89
|
+
|
90
|
+
async fetchAllUserWNFTForVault(vaultAddress: string, walletAddress: string, token: string): Promise<any[]> {
|
91
|
+
const headers = new Headers()
|
92
|
+
headers.append('Authorization', token)
|
93
|
+
const res = await fetch(`${this.route}/api-be/api/spool/user/allWnft/${walletAddress}/${vaultAddress}`, {
|
94
|
+
headers
|
95
|
+
})
|
96
|
+
const data = await res.json() as any[]
|
97
|
+
return data
|
98
|
+
}
|
99
|
+
|
100
|
+
async fetchAllUserDNFTForVault(vaultAddress: string, walletAddress: string, token: string): Promise<any[]> {
|
101
|
+
const headers = new Headers()
|
102
|
+
headers.append('Authorization', token)
|
103
|
+
const res = await fetch(`${this.route}/api-be/api/spool/user/allDnft/${walletAddress}/${vaultAddress}`,{
|
104
|
+
headers
|
105
|
+
})
|
106
|
+
const data = await res.json() as any[]
|
107
|
+
return data
|
108
|
+
}
|
109
|
+
|
110
|
+
async fetchAssetPerSvtAtBlock(vaultAddress: string, blocknumber: number, token: string): Promise<BigNumber> {
|
111
|
+
const headers = new Headers()
|
112
|
+
headers.append('Authorization', token)
|
113
|
+
const res = await fetch(`${this.route}/api-be/api/spool/user/assetBalance/${blocknumber}/${vaultAddress}`, {
|
114
|
+
headers,
|
115
|
+
})
|
116
|
+
const data = await res.json() as BigNumber
|
117
|
+
return data
|
118
|
+
}
|
119
|
+
|
120
|
+
async fetchFastWithdrawNFTs(vaultAddress: string, walletAddress: string, token: string): Promise<QLFastRedeem[]> {
|
121
|
+
const headers = new Headers()
|
122
|
+
headers.append('Authorization', token)
|
123
|
+
const res = await fetch(`${this.route}/api-be/api/spool/user/fastWithdrawNft/${walletAddress}/${vaultAddress}`, {
|
124
|
+
headers,
|
125
|
+
})
|
126
|
+
const data = await res.json() as QLFastRedeem[]
|
127
|
+
|
128
|
+
return data
|
129
|
+
}
|
130
|
+
|
131
|
+
}
|
132
|
+
|
133
|
+
|
package/DripConfig.ts
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
import { ContractAddresses, SDKConfig } from "@spool.fi/spool-v2-sdk";
|
2
|
+
import { Signer } from "ethers";
|
3
|
+
|
4
|
+
export class DripConfig {
|
5
|
+
|
6
|
+
internalConfig: SDKConfig
|
7
|
+
dripRoute: string
|
8
|
+
swapAndDepositContractAddress: string | undefined
|
9
|
+
smartVaultManagerAddress: string | undefined
|
10
|
+
|
11
|
+
constructor(subgraphUrl: string, priceFeedApiUrl: string, rewardsUrl: string, fastRedeemApi: string, contracts: Record<number, ContractAddresses>, dripRoute: string) {
|
12
|
+
this.internalConfig = new SDKConfig(subgraphUrl, priceFeedApiUrl, rewardsUrl, fastRedeemApi, contracts)
|
13
|
+
this.dripRoute = dripRoute
|
14
|
+
}
|
15
|
+
|
16
|
+
async getSwapAndDepositContractAddress(signer: Signer): Promise<string> {
|
17
|
+
if (!signer) throw Error('No signer provided')
|
18
|
+
|
19
|
+
if (!this.swapAndDepositContractAddress) {
|
20
|
+
const contract = await this.internalConfig.getChainAddresses(signer)
|
21
|
+
this.swapAndDepositContractAddress = contract.IDepositSwap
|
22
|
+
}
|
23
|
+
|
24
|
+
return this.swapAndDepositContractAddress
|
25
|
+
}
|
26
|
+
|
27
|
+
async getSmartVaultManagerAddress(signer: Signer): Promise<string> {
|
28
|
+
if (!signer) throw Error('No signer provided')
|
29
|
+
|
30
|
+
if (!this.smartVaultManagerAddress) {
|
31
|
+
const contract = await this.internalConfig.getChainAddresses(signer)
|
32
|
+
this.smartVaultManagerAddress = contract.ISmartVaultManager
|
33
|
+
}
|
34
|
+
|
35
|
+
return this.smartVaultManagerAddress
|
36
|
+
}
|
37
|
+
|
38
|
+
}
|
package/DripSdk.ts
ADDED
@@ -0,0 +1,516 @@
|
|
1
|
+
import { Vault } from "./types/Vault";
|
2
|
+
import Web3Token from "web3-token";
|
3
|
+
import { DepositBagStruct, ERC20, ERC20__factory, EnrichedWNFT, RedeemBagStruct, SDKConfig, SpoolSdk } from '@spool.fi/spool-v2-sdk'
|
4
|
+
import { BigNumber, Bytes, Signer, ethers } from "ethers";
|
5
|
+
import { DripConfig } from "./DripConfig";
|
6
|
+
import { insertDot } from "./utils";
|
7
|
+
import { UserBalance } from "./types/UserBalance";
|
8
|
+
import DripApi from "./DripApi";
|
9
|
+
|
10
|
+
const KASU_USDC_VAULT_ADDRESS = "0xd8aa8099a53eddebe6a49c98ca12746ff19aa502"
|
11
|
+
|
12
|
+
export default class DripSdk {
|
13
|
+
|
14
|
+
private dripApi: DripApi
|
15
|
+
private spoolSdk?: SpoolSdk
|
16
|
+
private signer?: Signer
|
17
|
+
private dripConfig: DripConfig
|
18
|
+
|
19
|
+
constructor(dripConfig: DripConfig, signer?: Signer) {
|
20
|
+
this.signer = signer
|
21
|
+
this.dripConfig = dripConfig
|
22
|
+
if (signer) {
|
23
|
+
this.spoolSdk = new SpoolSdk(dripConfig.internalConfig, signer)
|
24
|
+
}
|
25
|
+
this.dripApi = new DripApi(dripConfig.dripRoute)
|
26
|
+
|
27
|
+
}
|
28
|
+
|
29
|
+
async getAllVaults(): Promise<Vault[]> {
|
30
|
+
return this.dripApi.fetchAllVaults()
|
31
|
+
}
|
32
|
+
|
33
|
+
async getVaultDetails(vaultAddress: string): Promise<Vault> {
|
34
|
+
return this.dripApi.fetchVaultDetails(vaultAddress)
|
35
|
+
}
|
36
|
+
|
37
|
+
async getUserBalance(vault: Vault): Promise<UserBalance> {
|
38
|
+
if (!this.signer) throw Error('No signer provided')
|
39
|
+
const userAddress = await this.signer.getAddress()
|
40
|
+
|
41
|
+
const token = await this.generateToken()
|
42
|
+
const dnfts = await this.dripApi.fetchAllUserDNFTForVault(vault.vaultAddress, userAddress, token)
|
43
|
+
const wnfts = await this.dripApi.fetchAllUserWNFTForVault(vault.vaultAddress, userAddress, token)
|
44
|
+
const decimals = await this.getERC20Precission(vault.depositToken.tokenAddress)
|
45
|
+
|
46
|
+
if (this.shouldUseNewWithdrawLogic(userAddress, vault)) {
|
47
|
+
|
48
|
+
const [estimatedPendingWithdrawalBalance, estimatedWithdrawableBalance] = await this.calculateAllWithdrawalBalances(
|
49
|
+
wnfts,
|
50
|
+
vault.vaultAddress,
|
51
|
+
decimals,
|
52
|
+
)
|
53
|
+
const hasWithdrawsToClaim = this.checkIfUserHasWithdrawsToClaim(wnfts)
|
54
|
+
|
55
|
+
const pendingDeposits: BigNumber = this.calculatePendingDeposits(dnfts, decimals)
|
56
|
+
|
57
|
+
const userBalance: BigNumber = this.calculateAllDeposits(dnfts, decimals).sub(pendingDeposits)
|
58
|
+
return {
|
59
|
+
hasWithdrawsToClaim,
|
60
|
+
userBalance: ethers.utils.formatUnits(userBalance, decimals),
|
61
|
+
pendingUserBalance: ethers.utils.formatUnits(pendingDeposits, decimals),
|
62
|
+
pendingWithdrawalBalance: ethers.utils.formatUnits(estimatedPendingWithdrawalBalance, decimals),
|
63
|
+
withdrawableBalance: ethers.utils.formatUnits(estimatedWithdrawableBalance, decimals),
|
64
|
+
}
|
65
|
+
} else {
|
66
|
+
const allDeposits = this.calculateDepositsOld(dnfts, decimals)
|
67
|
+
const pendingDeposits = this.calculatePendingDepositsOld(dnfts, decimals)
|
68
|
+
const [estimatedPendingWithdrawalBalance, estimatedWithdrawableBalance, estimatedWithdrawals] =
|
69
|
+
await this.calculateAllWithdrawalBalances(wnfts, vault.vaultAddress, decimals)
|
70
|
+
const fastWithdrawBalance = await this.calculateFastWithdrawBalancesOld(vault.vaultAddress, userAddress, decimals)
|
71
|
+
|
72
|
+
const hasWithdrawsToClaim = this.checkIfUserHasWithdrawsToClaim(wnfts)
|
73
|
+
let balance = allDeposits
|
74
|
+
.sub(estimatedWithdrawals)
|
75
|
+
.sub(estimatedWithdrawableBalance)
|
76
|
+
.sub(estimatedPendingWithdrawalBalance)
|
77
|
+
.sub(fastWithdrawBalance)
|
78
|
+
return {
|
79
|
+
hasWithdrawsToClaim,
|
80
|
+
userBalance: ethers.utils.formatUnits(balance, decimals),
|
81
|
+
pendingUserBalance: ethers.utils.formatUnits(pendingDeposits, decimals),
|
82
|
+
pendingWithdrawalBalance: ethers.utils.formatUnits(estimatedPendingWithdrawalBalance, decimals),
|
83
|
+
withdrawableBalance: ethers.utils.formatUnits(estimatedWithdrawableBalance, decimals),
|
84
|
+
}
|
85
|
+
}
|
86
|
+
|
87
|
+
}
|
88
|
+
|
89
|
+
private async fastWithdraw(vault: Vault, amountToWithdraw?: string) {
|
90
|
+
if (!this.signer) throw Error('No signer provided')
|
91
|
+
try {
|
92
|
+
|
93
|
+
const signerAddress = await this.signer.getAddress()
|
94
|
+
if (!signerAddress) throw Error('Error fetching address')
|
95
|
+
|
96
|
+
const redeemBagStruct: RedeemBagStruct = this.shouldUseNewWithdrawLogic(signerAddress, vault)
|
97
|
+
? await this.generateNewRedeemBagStruct(vault, signerAddress, amountToWithdraw)
|
98
|
+
: await this.generateOldRedeemBagStruct(vault, signerAddress, amountToWithdraw)
|
99
|
+
|
100
|
+
|
101
|
+
const currentBlockNumber = await this.signer.provider?.getBlockNumber()
|
102
|
+
|
103
|
+
if (!currentBlockNumber) throw Error('Error fetching block number')
|
104
|
+
|
105
|
+
const redeemTx = await this.spoolSdk?.redeemFast(
|
106
|
+
redeemBagStruct,
|
107
|
+
signerAddress.toLowerCase(),
|
108
|
+
currentBlockNumber,
|
109
|
+
)
|
110
|
+
|
111
|
+
const redeemTxReceipt = await redeemTx!.wait()
|
112
|
+
} catch (error) {
|
113
|
+
console.log(error)
|
114
|
+
}
|
115
|
+
}
|
116
|
+
|
117
|
+
private async deposit(tokenAddress: string, vaultAddress: string, amount: string) {
|
118
|
+
if (!this.signer) throw Error('No signer provided')
|
119
|
+
|
120
|
+
const currentTokenAllowance = await this.getTokenAllowanceForDeposit(tokenAddress)
|
121
|
+
const decimals = await this.getERC20Precission(tokenAddress)
|
122
|
+
let amountToWithdrawFixedDecimals = parseFloat(amount).toFixed(decimals)
|
123
|
+
const amountToDeposit = ethers.utils.parseUnits(amountToWithdrawFixedDecimals, decimals)
|
124
|
+
|
125
|
+
if (amountToDeposit.gt(currentTokenAllowance)) {
|
126
|
+
const requiredTokenAllowance = amountToDeposit.sub(currentTokenAllowance)
|
127
|
+
await this.approveTokenForDeposit(tokenAddress, requiredTokenAllowance)
|
128
|
+
}
|
129
|
+
const signerAddress = await this.signer.getAddress()
|
130
|
+
|
131
|
+
if (!signerAddress) throw Error('Error fetching address')
|
132
|
+
|
133
|
+
const depositBagStruct: DepositBagStruct = {
|
134
|
+
smartVault: vaultAddress,
|
135
|
+
assets: [amountToDeposit],
|
136
|
+
receiver: signerAddress,
|
137
|
+
referral: ethers.constants.AddressZero,
|
138
|
+
doFlush: false,
|
139
|
+
}
|
140
|
+
|
141
|
+
const depositTx = await this.spoolSdk!.deposit(depositBagStruct)
|
142
|
+
|
143
|
+
await depositTx.wait()
|
144
|
+
}
|
145
|
+
|
146
|
+
|
147
|
+
private async swapAndDeposit(fromTokenAddress: string, toTokenAddress: string, fromTokenAmount: string, vaultAddress: string, ethAmount?: string) {
|
148
|
+
if (!this.signer) throw Error('No signer provided')
|
149
|
+
|
150
|
+
|
151
|
+
const decimals = await this.getERC20Precission(fromTokenAddress)
|
152
|
+
const amountToWithdrawFixedDecimals = parseFloat(fromTokenAmount).toFixed(decimals)
|
153
|
+
const fromToken = ethers.utils.parseUnits(amountToWithdrawFixedDecimals, decimals)
|
154
|
+
|
155
|
+
const signerAddress = await this.signer.getAddress()
|
156
|
+
|
157
|
+
if (fromToken.gt(BigNumber.from(0))) {
|
158
|
+
const currentTokenAllowance = await this.getTokenAllowanceForSwapAndDepositContractAddress(fromTokenAddress)
|
159
|
+
|
160
|
+
if (fromToken.gt(currentTokenAllowance)) {
|
161
|
+
await this.approveTokenForSwapAndDepositContract(fromTokenAddress, fromToken.sub(currentTokenAllowance))
|
162
|
+
}
|
163
|
+
}
|
164
|
+
|
165
|
+
const swapInfo = await this.dripApi.getSwapInfo(fromTokenAddress, toTokenAddress, fromToken, signerAddress)
|
166
|
+
|
167
|
+
const swapDepositBagStruct = {
|
168
|
+
inTokens: [fromTokenAddress],
|
169
|
+
inAmounts: [fromToken],
|
170
|
+
smartVault: vaultAddress,
|
171
|
+
swapInfo,
|
172
|
+
receiver: signerAddress,
|
173
|
+
referral: ethers.constants.AddressZero,
|
174
|
+
doFlush: false,
|
175
|
+
}
|
176
|
+
|
177
|
+
let swapAndDepositRequest: ethers.ContractTransaction | undefined
|
178
|
+
|
179
|
+
if (ethAmount) {
|
180
|
+
const eth = ethers.utils.parseEther(ethAmount)
|
181
|
+
swapAndDepositRequest = await this.spoolSdk?.swapAndDeposit(swapDepositBagStruct, { value: eth })
|
182
|
+
} else {
|
183
|
+
swapAndDepositRequest = await this.spoolSdk?.swapAndDeposit(swapDepositBagStruct)
|
184
|
+
}
|
185
|
+
|
186
|
+
await swapAndDepositRequest?.wait()
|
187
|
+
}
|
188
|
+
|
189
|
+
private async withdraw(vault: Vault, amountToWithdraw?: string) {
|
190
|
+
if (!this.signer) throw Error('No signer provided')
|
191
|
+
|
192
|
+
try {
|
193
|
+
const signerAddress = await this.signer.getAddress()
|
194
|
+
|
195
|
+
if (!signerAddress) throw Error('Error fetching address')
|
196
|
+
|
197
|
+
const redeemBagStruct: RedeemBagStruct = this.shouldUseNewWithdrawLogic(signerAddress, vault)
|
198
|
+
? await this.generateNewRedeemBagStruct(vault, signerAddress, amountToWithdraw)
|
199
|
+
: await this.generateOldRedeemBagStruct(vault, signerAddress, amountToWithdraw)
|
200
|
+
|
201
|
+
const redeemTx = await this.spoolSdk!.redeem(redeemBagStruct, signerAddress.toLowerCase(), false)
|
202
|
+
|
203
|
+
const redeemTxReceipt = await redeemTx.wait()
|
204
|
+
} catch (error) {
|
205
|
+
console.log(error)
|
206
|
+
}
|
207
|
+
}
|
208
|
+
|
209
|
+
|
210
|
+
private async generateOldRedeemBagStruct(vault: Vault, signerAddress: string, amountToWithdraw?: string): Promise<RedeemBagStruct> {
|
211
|
+
const token = await this.generateToken()
|
212
|
+
|
213
|
+
const dnfts = await this.dripApi.fetchEnrichedUserDNFTForVault(vault.vaultAddress, signerAddress, token)
|
214
|
+
|
215
|
+
const userBalance = await this.dripApi.fetchUserBalance(vault.vaultAddress, signerAddress, token)
|
216
|
+
const totalTokenBalance = BigNumber.from(userBalance[vault.depositToken.tokenAddress])
|
217
|
+
const totalSharesFetched = await this.dripApi.fetchUserSVTBalance(vault.vaultAddress, signerAddress, token)
|
218
|
+
const totalShares = BigNumber.from(totalSharesFetched)
|
219
|
+
|
220
|
+
const decimals = await this.getERC20Precission(vault.depositToken.tokenAddress)
|
221
|
+
|
222
|
+
let sharesToWithdraw = BigNumber.from(0)
|
223
|
+
|
224
|
+
if (!amountToWithdraw) {
|
225
|
+
sharesToWithdraw = totalShares
|
226
|
+
} else {
|
227
|
+
//! Not a MAX Withdraw (calculate the shares to withdraw)
|
228
|
+
let amountToWithdrawFixedDecimals = parseFloat(amountToWithdraw).toFixed(decimals)
|
229
|
+
|
230
|
+
sharesToWithdraw = ethers.utils
|
231
|
+
.parseUnits(amountToWithdrawFixedDecimals, decimals)
|
232
|
+
.mul(totalShares)
|
233
|
+
.div(totalTokenBalance)
|
234
|
+
|
235
|
+
if (sharesToWithdraw.gt(totalShares)) {
|
236
|
+
sharesToWithdraw = totalShares
|
237
|
+
}
|
238
|
+
}
|
239
|
+
|
240
|
+
const nftIds = dnfts
|
241
|
+
.filter((item) => !item.isBurned && BigNumber.from(item.shares).gt(BigNumber.from('0')) && item.isDHWFinished)
|
242
|
+
.map((item) => item.nftId.toString())
|
243
|
+
const nftAmounts = dnfts
|
244
|
+
.filter((item) => !item.isBurned && BigNumber.from(item.shares).gt(BigNumber.from('0')) && item.isDHWFinished)
|
245
|
+
.map((item) => item.shares.toString())
|
246
|
+
|
247
|
+
return {
|
248
|
+
smartVault: vault.vaultAddress,
|
249
|
+
shares: sharesToWithdraw,
|
250
|
+
nftIds,
|
251
|
+
nftAmounts,
|
252
|
+
}
|
253
|
+
}
|
254
|
+
|
255
|
+
|
256
|
+
private async generateNewRedeemBagStruct(vault: Vault, signerAddress: string, amountToWithdraw?: string): Promise<RedeemBagStruct> {
|
257
|
+
const token = await this.generateToken()
|
258
|
+
const decimals = await this.getERC20Precission(vault.depositToken.tokenAddress.toLowerCase())
|
259
|
+
const withdrawAll: boolean = (!amountToWithdraw)
|
260
|
+
const initialAmountToWithdraw: BigNumber = ethers.utils.parseUnits(amountToWithdraw || '0', decimals)
|
261
|
+
let totalAmountToWithdraw: BigNumber = initialAmountToWithdraw
|
262
|
+
let dnfts: any[] = await this.dripApi.fetchEnrichedUserDNFTForVault(vault.vaultAddress.toLowerCase(), signerAddress, token)
|
263
|
+
dnfts = await this.getAllSvts(vault.vaultAddress.toLowerCase(), signerAddress, dnfts)
|
264
|
+
let shares: BigNumber = BigNumber.from(0)
|
265
|
+
let nftIds: BigNumber[] = []
|
266
|
+
let nftAmounts: BigNumber[] = []
|
267
|
+
let index = 0
|
268
|
+
|
269
|
+
while (dnfts[index] && (totalAmountToWithdraw.gt(0) || withdrawAll)) {
|
270
|
+
|
271
|
+
const dnft: { nftId: BigNumber, shares: BigNumber, svts: BigNumber, assets: number[] } = dnfts[index]
|
272
|
+
|
273
|
+
const userBalance = this.calculateBalanceForDNFT(dnft, decimals)
|
274
|
+
|
275
|
+
if (userBalance.gt(0)) {
|
276
|
+
|
277
|
+
let amountToWithdraw: BigNumber = totalAmountToWithdraw
|
278
|
+
const userSvts = dnft.svts
|
279
|
+
|
280
|
+
let svtsToWithdraw = BigNumber.from(0)
|
281
|
+
let nftAmount = BigNumber.from(0)
|
282
|
+
if (amountToWithdraw.gte(userBalance) || withdrawAll) {
|
283
|
+
nftAmount = dnft.shares
|
284
|
+
svtsToWithdraw = userSvts
|
285
|
+
if (!withdrawAll) totalAmountToWithdraw = totalAmountToWithdraw.sub(userBalance)
|
286
|
+
} else {
|
287
|
+
nftAmount = amountToWithdraw.mul(dnft.shares).div(userBalance)
|
288
|
+
svtsToWithdraw = nftAmount.mul(userSvts).div(dnft.shares)
|
289
|
+
totalAmountToWithdraw = totalAmountToWithdraw.sub(amountToWithdraw)
|
290
|
+
}
|
291
|
+
shares = shares.add(svtsToWithdraw)
|
292
|
+
nftIds.push(dnft.nftId)
|
293
|
+
nftAmounts.push(nftAmount)
|
294
|
+
}
|
295
|
+
index++
|
296
|
+
}
|
297
|
+
|
298
|
+
return {
|
299
|
+
smartVault: vault.vaultAddress.toLowerCase(),
|
300
|
+
shares: shares,
|
301
|
+
nftIds,
|
302
|
+
nftAmounts
|
303
|
+
}
|
304
|
+
}
|
305
|
+
|
306
|
+
private async getAllSvts(vaultAddress: string, userAddress: string, dnfts: any[]): Promise<{ nftId: BigNumber, shares: BigNumber, assets: number[], svts: BigNumber }[]> {
|
307
|
+
const token = await this.generateToken()
|
308
|
+
const svts: BigNumber[] = await this.dripApi.fetchUserSVTFromNfts(vaultAddress, userAddress, dnfts.map((element) => parseInt(element.nftId)), token)
|
309
|
+
const result = dnfts.map((element, index) => { return { nftId: element.nftId, assets: element.assets, shares: element.shares, svts: svts[index] } })
|
310
|
+
return result
|
311
|
+
}
|
312
|
+
|
313
|
+
private async getERC20Precission(tokenAddress: string): Promise<number> {
|
314
|
+
if (!this.signer) throw Error('No signer provided')
|
315
|
+
|
316
|
+
const signerAddress = await this.signer.getAddress()
|
317
|
+
|
318
|
+
if (!signerAddress) throw Error('Error fetching address')
|
319
|
+
|
320
|
+
const erc20Instance: ERC20 = ERC20__factory.connect(tokenAddress, this.signer)
|
321
|
+
|
322
|
+
const decimals = await erc20Instance.decimals()
|
323
|
+
|
324
|
+
return decimals
|
325
|
+
}
|
326
|
+
|
327
|
+
private async calculateFastWithdrawBalancesOld(vaultAddress: string, userAddress: string, decimals: number) {
|
328
|
+
const token = await this.generateToken()
|
329
|
+
const fastWithdrawNFTs = await this.dripApi.fetchFastWithdrawNFTs(vaultAddress, userAddress, token)
|
330
|
+
|
331
|
+
let fastWithdrawBalance = 0
|
332
|
+
for (const wnft of fastWithdrawNFTs) {
|
333
|
+
if (wnft.assetsWithdrawn[0].asset && wnft.assetsWithdrawn[0].claimed) {
|
334
|
+
const value = +insertDot(wnft.assetsWithdrawn[0].claimed, decimals)
|
335
|
+
fastWithdrawBalance += value
|
336
|
+
}
|
337
|
+
}
|
338
|
+
return ethers.utils.parseUnits(fastWithdrawBalance.toFixed(decimals), decimals)
|
339
|
+
}
|
340
|
+
|
341
|
+
private calculatePendingDepositsOld(dnfts: any[], decimals: number) {
|
342
|
+
let pendingDeposits = 0
|
343
|
+
for (const nft of dnfts) {
|
344
|
+
if (nft['assets'] && nft['assets'][0] && !nft.isDHWFinished) {
|
345
|
+
pendingDeposits += nft['assets'][0]
|
346
|
+
}
|
347
|
+
}
|
348
|
+
pendingDeposits = +pendingDeposits.toFixed(decimals)
|
349
|
+
return ethers.utils.parseUnits(pendingDeposits.toString(), decimals)
|
350
|
+
}
|
351
|
+
|
352
|
+
private calculateDepositsOld(dnfts: any[], decimals: number): BigNumber {
|
353
|
+
let deposits = 0
|
354
|
+
for (const nft of dnfts) {
|
355
|
+
if (nft['assets'] && nft['assets'][0] && nft.isDHWFinished) {
|
356
|
+
deposits += nft['assets'][0]
|
357
|
+
}
|
358
|
+
}
|
359
|
+
deposits = +deposits.toFixed(decimals)
|
360
|
+
|
361
|
+
return ethers.utils.parseUnits(deposits.toString(), decimals)
|
362
|
+
}
|
363
|
+
|
364
|
+
private calculatePendingDeposits(dnfts: any[], decimals: number): BigNumber {
|
365
|
+
let pendingDeposits: BigNumber = BigNumber.from(0)
|
366
|
+
for (const nft of dnfts) {
|
367
|
+
if (nft['assets'] && nft['assets'][0] && !nft.isDHWFinished) {
|
368
|
+
pendingDeposits = pendingDeposits.add(ethers.utils.parseUnits(nft['assets'][0].toString(), decimals))
|
369
|
+
}
|
370
|
+
}
|
371
|
+
|
372
|
+
return pendingDeposits
|
373
|
+
}
|
374
|
+
|
375
|
+
|
376
|
+
private calculateAllDeposits(dnfts: any[], decimals: number): BigNumber {
|
377
|
+
let deposits: BigNumber = BigNumber.from(0)
|
378
|
+
for (const nft of dnfts) {
|
379
|
+
deposits = deposits.add(this.calculateBalanceForDNFT(nft, decimals))
|
380
|
+
}
|
381
|
+
return deposits
|
382
|
+
}
|
383
|
+
|
384
|
+
private calculateBalanceForDNFT(dnft: { assets: number[], shares: BigNumber }, decimals: number): BigNumber {
|
385
|
+
|
386
|
+
const assets = BigNumber.from(ethers.utils.parseUnits(dnft.assets[0]!.toString(), decimals))
|
387
|
+
const shares = BigNumber.from(dnft.shares)
|
388
|
+
if (shares.eq(0)) {
|
389
|
+
return BigNumber.from(0)
|
390
|
+
}
|
391
|
+
return assets.mul(shares).div(1000000)
|
392
|
+
}
|
393
|
+
|
394
|
+
private checkIfUserHasWithdrawsToClaim(withdrawNFTS: any[]): boolean {
|
395
|
+
const nftIds = withdrawNFTS
|
396
|
+
//! Shares come as Strings instead of BigNumber from our Backend
|
397
|
+
.filter((item) => !item.isBurned && BigNumber.from(item.shares).gt(BigNumber.from('0')) && item.isDHWFinished)
|
398
|
+
.map((item) => item.nftId.toString())
|
399
|
+
|
400
|
+
const nftAmounts = withdrawNFTS
|
401
|
+
.filter((item) => !item.isBurned && BigNumber.from(item.shares).gt(BigNumber.from('0')) && item.isDHWFinished)
|
402
|
+
.map((item) => item.shares.toString())
|
403
|
+
|
404
|
+
return nftIds.length !== 0 || nftAmounts.length !== 0
|
405
|
+
}
|
406
|
+
private shouldUseNewWithdrawLogic(userAddress: string, vault: Vault) {
|
407
|
+
|
408
|
+
// Users that already withdrew using the old approach should keep using the same
|
409
|
+
const usersWhoAlreadyWithdraw: { [walletAddress: string]: string } = {
|
410
|
+
'0x5ae62d2bc40e9119aee9cd4ed50e3d9378c9a191': KASU_USDC_VAULT_ADDRESS
|
411
|
+
}
|
412
|
+
if (usersWhoAlreadyWithdraw[userAddress.toLowerCase()] && usersWhoAlreadyWithdraw[userAddress.toLowerCase()] === vault.vaultAddress.toLowerCase()) {
|
413
|
+
return false
|
414
|
+
}
|
415
|
+
return vault.newWithdraw
|
416
|
+
}
|
417
|
+
|
418
|
+
private async generateToken(): Promise<string> {
|
419
|
+
if (!this.signer) throw Error('No signer provided')
|
420
|
+
const token: string = await Web3Token.sign(async (msg: string | Bytes) => await this.signer!.signMessage(msg), '1d');
|
421
|
+
return token
|
422
|
+
}
|
423
|
+
|
424
|
+
private async calculateAllWithdrawalBalances(wnfts: EnrichedWNFT[], vaultAddress: string, decimals: number) {
|
425
|
+
if (!this.signer) throw Error('No signer provided')
|
426
|
+
const userAddress = await this.signer.getAddress()
|
427
|
+
|
428
|
+
let estimatedPendingWithdrawalBalance = BigNumber.from(0)
|
429
|
+
let estimatedWithdrawableBalance = BigNumber.from(0)
|
430
|
+
let withdrawals = 0
|
431
|
+
|
432
|
+
for (const wnft of wnfts) {
|
433
|
+
// Handle burned NFTs (already withdrawn by the user)
|
434
|
+
if (wnft.assets && wnft.assets[0] && wnft.isBurned) {
|
435
|
+
withdrawals += wnft.assets[0].amount
|
436
|
+
continue
|
437
|
+
}
|
438
|
+
const currentBlockNumber = wnft.blockNumber - 1
|
439
|
+
const token = await this.generateToken()
|
440
|
+
const assetsPerSvtAtBlock = await this.dripApi.fetchAssetPerSvtAtBlock(vaultAddress, currentBlockNumber, token)
|
441
|
+
const estimatedValueOfNFT = BigNumber.from(
|
442
|
+
parseInt(ethers.utils.formatUnits(BigNumber.from(wnft.svtWithdrawn).mul(assetsPerSvtAtBlock), 36)).toString()
|
443
|
+
)
|
444
|
+
|
445
|
+
if (wnft.isDHWFinished) {
|
446
|
+
// Processed and Withdrawable
|
447
|
+
estimatedWithdrawableBalance = estimatedWithdrawableBalance.add(estimatedValueOfNFT)
|
448
|
+
} else {
|
449
|
+
// Not processed, no DHW was ran => pending
|
450
|
+
estimatedPendingWithdrawalBalance = estimatedPendingWithdrawalBalance.add(estimatedValueOfNFT)
|
451
|
+
}
|
452
|
+
}
|
453
|
+
// return all values as BigNumber
|
454
|
+
withdrawals = +withdrawals.toFixed(decimals)
|
455
|
+
const estimatedWithdrawals = ethers.utils.parseUnits(withdrawals.toString(), decimals)
|
456
|
+
return [estimatedPendingWithdrawalBalance, estimatedWithdrawableBalance, estimatedWithdrawals]
|
457
|
+
}
|
458
|
+
|
459
|
+
|
460
|
+
|
461
|
+
private async getTokenAllowanceForSwapAndDepositContractAddress(tokenAddress: string): Promise<BigNumber> {
|
462
|
+
if (!this.signer) throw Error('No signer provided')
|
463
|
+
|
464
|
+
const signerAddress = await this.signer.getAddress()
|
465
|
+
|
466
|
+
if (!signerAddress) throw Error('Error fetching address')
|
467
|
+
|
468
|
+
const erc20Instance: ERC20 = ERC20__factory.connect(tokenAddress, this.signer)
|
469
|
+
|
470
|
+
const swapAndDepositAddress = await this.dripConfig.getSwapAndDepositContractAddress(this.signer)
|
471
|
+
|
472
|
+
const allowance = await erc20Instance.allowance(signerAddress, swapAndDepositAddress)
|
473
|
+
|
474
|
+
return allowance
|
475
|
+
}
|
476
|
+
|
477
|
+
private async approveTokenForSwapAndDepositContract(tokenAddress: string, amount: BigNumber) {
|
478
|
+
if (!this.signer) throw Error('No signer provided')
|
479
|
+
|
480
|
+
const swapAndDepositContractAddress = await this.dripConfig.getSwapAndDepositContractAddress(this.signer)
|
481
|
+
|
482
|
+
const erc20Instance: ERC20 = ERC20__factory.connect(tokenAddress, this.signer)
|
483
|
+
const approveTx = await erc20Instance.approve(swapAndDepositContractAddress, amount)
|
484
|
+
|
485
|
+
await approveTx.wait()
|
486
|
+
}
|
487
|
+
|
488
|
+
private async getTokenAllowanceForDeposit(tokenAddress: string): Promise<BigNumber> {
|
489
|
+
if (!this.signer) throw Error('No signer provided')
|
490
|
+
|
491
|
+
const signerAddress = await this.signer.getAddress()
|
492
|
+
|
493
|
+
if (!signerAddress) throw Error('Error fetching address')
|
494
|
+
|
495
|
+
const erc20Instance: ERC20 = ERC20__factory.connect(tokenAddress, this.signer)
|
496
|
+
|
497
|
+
const smartVaultManagerAddress = await this.dripConfig.getSmartVaultManagerAddress(this.signer)
|
498
|
+
|
499
|
+
const allowance = await erc20Instance.allowance(signerAddress, smartVaultManagerAddress)
|
500
|
+
|
501
|
+
return allowance
|
502
|
+
}
|
503
|
+
|
504
|
+
private async approveTokenForDeposit(tokenAddress: string, amount: BigNumber) {
|
505
|
+
if (!this.signer) throw Error('No signer provided')
|
506
|
+
|
507
|
+
const smartVaultManagerAddress = await this.dripConfig.getSmartVaultManagerAddress(this.signer)
|
508
|
+
|
509
|
+
const erc20Instance: ERC20 = ERC20__factory.connect(tokenAddress, this.signer)
|
510
|
+
const approveTx = await erc20Instance.approve(smartVaultManagerAddress, amount)
|
511
|
+
|
512
|
+
await approveTx.wait()
|
513
|
+
}
|
514
|
+
|
515
|
+
}
|
516
|
+
|
package/index.ts
ADDED
package/package.json
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
{
|
2
|
+
"name": "@dripfi/drip-sdk",
|
3
|
+
"version": "1.0.0",
|
4
|
+
"description": "Drip SDK",
|
5
|
+
"main": "index.js",
|
6
|
+
"scripts": {
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
8
|
+
},
|
9
|
+
"dependencies": {
|
10
|
+
"@spool.fi/spool-v2-sdk": "1.0.14",
|
11
|
+
"web3-token": "^1.0.6",
|
12
|
+
"ethers": "^5.7.2"
|
13
|
+
},
|
14
|
+
"author": "",
|
15
|
+
"license": "ISC"
|
16
|
+
}
|
package/types/Vault.ts
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
export type Vault = {
|
2
|
+
vaultName: string
|
3
|
+
vaultAddress: string
|
4
|
+
apy: number
|
5
|
+
tvr: number
|
6
|
+
protocols: string[]
|
7
|
+
projectName: string
|
8
|
+
depositToken: VaultDepositToken
|
9
|
+
type: VaultType
|
10
|
+
rewards: VaultReward[]
|
11
|
+
liveUntil: string
|
12
|
+
liveUntilFormatted: string
|
13
|
+
hasPoolEnded: boolean
|
14
|
+
boosters: NFTBoost[]
|
15
|
+
stretchGoals: StretchGoal[]
|
16
|
+
strategies: Strategy[]
|
17
|
+
newWithdraw: boolean
|
18
|
+
tgePrice?: number
|
19
|
+
maxAmountOfTokens?: number,
|
20
|
+
rewardsInformation: RewardsInformation
|
21
|
+
}
|
22
|
+
|
23
|
+
export type VaultType = 'launch' | 'earn' | 'airdrop'
|
24
|
+
|
25
|
+
export type VaultDepositToken = {
|
26
|
+
name: string
|
27
|
+
symbol: string
|
28
|
+
roundingDecimals: number
|
29
|
+
precisionDecimals: number
|
30
|
+
tokenAddress: string
|
31
|
+
}
|
32
|
+
|
33
|
+
export type VaultReward = {
|
34
|
+
type: 'token' | 'points'
|
35
|
+
name: string
|
36
|
+
symbol: string
|
37
|
+
decimals: number
|
38
|
+
tokenAddress: string
|
39
|
+
monthlyEmissionRate?: number
|
40
|
+
}
|
41
|
+
|
42
|
+
export type NFTBoost = {
|
43
|
+
url: string
|
44
|
+
tokenAddress: string
|
45
|
+
multiplier: number
|
46
|
+
nftAddress: string
|
47
|
+
network: string
|
48
|
+
initialBlock: number // needs to be the block when rewards was deployed
|
49
|
+
imagePath: string
|
50
|
+
}
|
51
|
+
|
52
|
+
export interface StretchGoal {
|
53
|
+
threshhold: number
|
54
|
+
threshholdDescription: string
|
55
|
+
rewardTooltip: string
|
56
|
+
rewardDescription: string
|
57
|
+
amountOfTokens: number
|
58
|
+
}
|
59
|
+
|
60
|
+
export type RewardsInformation = {
|
61
|
+
[tokenAddress: string]: { blockNumber: string, rewardrate: string, timestamp: string, endTime: string }
|
62
|
+
}
|
63
|
+
|
64
|
+
export type Strategy = {
|
65
|
+
address: string
|
66
|
+
lastDoHardWorkTime: number | null
|
67
|
+
lastDoHardWorkBlock: number | null
|
68
|
+
}
|
package/utils.ts
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
|
2
|
+
export function insertDot(numberString: string, decimals: number) {
|
3
|
+
if (numberString.length < decimals + 1) {
|
4
|
+
numberString = '0'.repeat(decimals + 1 - numberString.length) + numberString
|
5
|
+
}
|
6
|
+
const indexToInsertDot = numberString.length - decimals
|
7
|
+
// Ensure there is a part before the dot, even if it's 0
|
8
|
+
const beforeDecimal = indexToInsertDot > 0 ? numberString.slice(0, indexToInsertDot) : '0'
|
9
|
+
const afterDecimal = numberString.slice(indexToInsertDot)
|
10
|
+
return `${beforeDecimal}.${afterDecimal}`
|
11
|
+
}
|
12
|
+
|