Repository navigation
Expand file tree
/
Copy pathdefindex.ts
More file actions
187 lines (175 loc) · 6.17 KB
/
Copy pathdefindex.ts
File metadata and controls
187 lines (175 loc) · 6.17 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
import { DEFINDEX_SLIPPAGE_BPS } from "@meridian/shared";
import {
Address,
Contract,
nativeToScVal,
rpc,
xdr,
} from "@stellar/stellar-sdk";
import { simulateView, prepareSorobanTx } from "./tx";
import type { StellarNetwork } from "./types";
import type { PositionInfo } from "./positions";
import { toBigInt, STROOPS_PER_UNIT, getRpcServer } from "./internal";
// Converts a stroop-denominated bigint to a floating-point unit value without
// precision loss: the whole-unit part stays in bigint space until it fits
// safely in a Number, then the sub-unit remainder is added as a fraction.
export function stroopsToUnits(stroops: bigint): number {
const s = BigInt(STROOPS_PER_UNIT);
return Number(stroops / s) + Number(stroops % s) / STROOPS_PER_UNIT;
}
export interface DefindexVaultConfig {
// DeFindex vault contract (C...) the request targets.
vaultId: string;
network: StellarNetwork;
}
function i128(value: bigint): xdr.ScVal {
return nativeToScVal(value, { type: "i128" });
}
/**
* Quotes the underlying-asset value of `shares` DeFindex shares via
* get_asset_amounts_per_shares, returning the single-asset vault's one
* amount, or null when the simulation returned no usable value. Shared by
* buildDefindexWithdrawTx, fetchDefindexPosition, and rate-sources.ts's
* DeFindex share-price probe so the response parsing (the array check and
* toBigInt(amounts[0])) only needs to be right in one place.
*/
export async function getDefindexAssetAmountPerShares(
server: rpc.Server,
vaultId: string,
passphrase: string,
shares: bigint
): Promise<bigint | null> {
const amounts = (await simulateView(
server,
vaultId,
passphrase,
"get_asset_amounts_per_shares",
i128(shares)
)) as Array<bigint | number> | null;
return Array.isArray(amounts) && amounts.length > 0
? toBigInt(amounts[0])
: null;
}
/**
* Build an unsigned transaction that deposits `amount` (in stroops) of the
* vault's single underlying asset into a DeFindex vault on behalf of `depositor`,
* auto-investing into the vault's strategies. The user signs and submits the
* returned XDR and holds the resulting dfToken shares directly — non-custodial.
*
* Contract ABI: deposit(amounts_desired: Vec<i128>, amounts_min: Vec<i128>,
* from: Address, invest: bool). Single-asset vault, so each Vec has one element.
*
* `slippageBps` (default 10 = 0.1%) controls the gap between amounts_desired
* and amounts_min so minor share-price rounding between simulation and submission
* does not revert the transaction. Pass 0 only in tests.
*/
export async function buildDefindexDepositTx(
config: DefindexVaultConfig,
depositor: string,
amount: bigint,
slippageBps = BigInt(DEFINDEX_SLIPPAGE_BPS)
): Promise<{ xdr: string; fee: string }> {
if (amount <= 0n) throw new Error("amount must be positive");
const minAmount = amount - (amount * slippageBps) / 10_000n;
const contract = new Contract(config.vaultId);
return prepareSorobanTx(
config.network,
depositor,
contract.call(
"deposit",
xdr.ScVal.scvVec([i128(amount)]),
xdr.ScVal.scvVec([i128(minAmount)]),
Address.fromString(depositor).toScVal(),
xdr.ScVal.scvBool(true)
)
);
}
/**
* Build an unsigned transaction that burns `shares` (dfTokens, in stroops) to
* withdraw the proportional underlying back to `withdrawer`.
*
* Contract ABI: withdraw(withdraw_shares: i128, min_amounts_out: Vec<i128>,
* from: Address).
*
* `slippageBps` (default 10 = 0.1%) controls the gap between the expected
* payout (quoted via `get_asset_amounts_per_shares`) and min_amounts_out so
* that a withdrawal executing at a much worse rate than expected fails cleanly.
* Pass 0 only in tests.
*
* Throws before building anything when the quote is missing or zero, since the
* floor cannot be computed.
*/
export async function buildDefindexWithdrawTx(
config: DefindexVaultConfig,
withdrawer: string,
shares: bigint,
slippageBps = BigInt(DEFINDEX_SLIPPAGE_BPS)
): Promise<{ xdr: string; fee: string }> {
if (shares <= 0n) throw new Error("shares must be positive");
// Quote the expected payout so we can compute a real floor.
const server = getRpcServer(config.network.rpcUrl, 12_000);
const expectedAmount = await getDefindexAssetAmountPerShares(
server,
config.vaultId,
config.network.passphrase,
shares
);
// A missing or zero quote would collapse the floor to 0, so refuse to build
// a withdrawal that cannot revert on a bad rate.
if (expectedAmount === null || expectedAmount <= 0n) {
throw new Error(
"DeFindex withdrawal quote unavailable: cannot compute a slippage floor"
);
}
const minAmount = expectedAmount - (expectedAmount * slippageBps) / 10_000n;
const contract = new Contract(config.vaultId);
return prepareSorobanTx(
config.network,
withdrawer,
contract.call(
"withdraw",
i128(shares),
xdr.ScVal.scvVec([i128(minAmount)]),
Address.fromString(withdrawer).toScVal()
)
);
}
/**
* Read a user's live position in a DeFindex vault via read-only simulation.
* `balance` gives the dfToken share count; `get_asset_amounts_per_shares` values
* those shares in the underlying asset. Returns `[]` when the user holds nothing.
*
* `shares` carries the dfToken count (not the USD value) because a DeFindex
* withdrawal burns shares — so the UI's "withdraw max" maps straight to it.
* `earned` is 0: a direct vault position has no on-chain cost basis (same as the
* Blend path).
*/
export async function fetchDefindexPosition(
network: StellarNetwork,
vaultId: string,
reportVaultId: string,
publicKey: string
): Promise<PositionInfo[]> {
const server = getRpcServer(network.rpcUrl, 12_000);
const caller = Address.fromString(publicKey).toScVal();
const shares = toBigInt(
await simulateView(server, vaultId, network.passphrase, "balance", caller)
);
if (shares <= 0n) return [];
const underlying =
(await getDefindexAssetAmountPerShares(
server,
vaultId,
network.passphrase,
shares
)) ?? 0n;
return [
{
vaultId: reportVaultId,
shares: stroopsToUnits(shares),
deposited: stroopsToUnits(underlying),
earned: 0,
entryTime: 0,
},
];
}