@agoric/portfolio-api 0.1.1-dev-159ee8a.0.159ee8a → 0.1.1-dev-6deecfc.0.6deecfc

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/src/places.js ADDED
@@ -0,0 +1,190 @@
1
+ import { Fail } from '@endo/errors';
2
+
3
+ ;
4
+
5
+
6
+
7
+
8
+ ;
9
+ ;
10
+
11
+ const isPrimitive = (value ) => Object(value) !== value;
12
+
13
+ const deepFreeze = (value ) => {
14
+ if (isPrimitive(value)) return value;
15
+ const obj = value ;
16
+ Object.freeze(obj);
17
+ for (const key of Reflect.ownKeys(obj)) {
18
+ deepFreeze(obj[key]);
19
+ }
20
+ return value;
21
+ };
22
+
23
+ ;
24
+
25
+
26
+
27
+ // XXX special handling. What's the functional difference from other places?
28
+ export const BeefyPoolPlaces = {
29
+ Beefy_re7_Avalanche: {
30
+ protocol: 'Beefy',
31
+ chainName: 'Avalanche',
32
+ },
33
+ Beefy_morphoGauntletUsdc_Ethereum: {
34
+ protocol: 'Beefy',
35
+ chainName: 'Ethereum',
36
+ },
37
+ Beefy_morphoSmokehouseUsdc_Ethereum: {
38
+ protocol: 'Beefy',
39
+ chainName: 'Ethereum',
40
+ },
41
+ Beefy_morphoSeamlessUsdc_Base: {
42
+ protocol: 'Beefy',
43
+ chainName: 'Base',
44
+ },
45
+ Beefy_compoundUsdc_Optimism: {
46
+ protocol: 'Beefy',
47
+ chainName: 'Optimism',
48
+ },
49
+ Beefy_compoundUsdc_Arbitrum: {
50
+ protocol: 'Beefy',
51
+ chainName: 'Arbitrum',
52
+ },
53
+ } ;
54
+ deepFreeze(BeefyPoolPlaces);
55
+
56
+ ;
57
+
58
+ export const ERC4626PoolPlaces = {
59
+ ERC4626_vaultU2_Ethereum: {
60
+ protocol: 'ERC4626',
61
+ chainName: 'Ethereum',
62
+ },
63
+ ERC4626_morphoClearstarHighYieldUsdc_Ethereum: {
64
+ protocol: 'ERC4626',
65
+ chainName: 'Ethereum',
66
+ },
67
+ ERC4626_morphoClearstarUsdcCore_Ethereum: {
68
+ protocol: 'ERC4626',
69
+ chainName: 'Ethereum',
70
+ },
71
+ ERC4626_morphoGauntletUsdcRwa_Ethereum: {
72
+ protocol: 'ERC4626',
73
+ chainName: 'Ethereum',
74
+ },
75
+ ERC4626_morphoSteakhouseHighYieldInstant_Ethereum: {
76
+ protocol: 'ERC4626',
77
+ chainName: 'Ethereum',
78
+ },
79
+ ERC4626_morphoClearstarInstitutionalUsdc_Ethereum: {
80
+ protocol: 'ERC4626',
81
+ chainName: 'Ethereum',
82
+ },
83
+ ERC4626_morphoClearstarUsdcReactor_Ethereum: {
84
+ protocol: 'ERC4626',
85
+ chainName: 'Ethereum',
86
+ },
87
+ ERC4626_morphoAlphaUsdcCore_Ethereum: {
88
+ protocol: 'ERC4626',
89
+ chainName: 'Ethereum',
90
+ },
91
+ ERC4626_morphoResolvUsdc_Ethereum: {
92
+ protocol: 'ERC4626',
93
+ chainName: 'Ethereum',
94
+ },
95
+ ERC4626_morphoGauntletUsdcFrontier_Ethereum: {
96
+ protocol: 'ERC4626',
97
+ chainName: 'Ethereum',
98
+ },
99
+ ERC4626_morphoHyperithmUsdcMidcurve_Ethereum: {
100
+ protocol: 'ERC4626',
101
+ chainName: 'Ethereum',
102
+ },
103
+ ERC4626_morphoHyperithmUsdcDegen_Ethereum: {
104
+ protocol: 'ERC4626',
105
+ chainName: 'Ethereum',
106
+ },
107
+ ERC4626_morphoGauntletUsdcCore_Ethereum: {
108
+ protocol: 'ERC4626',
109
+ chainName: 'Ethereum',
110
+ },
111
+ ERC4626_morphoSteakhousePrimeUsdc_Base: {
112
+ protocol: 'ERC4626',
113
+ chainName: 'Base',
114
+ },
115
+ ERC4626_morphoSteakhouseUsdc_Base: {
116
+ protocol: 'ERC4626',
117
+ chainName: 'Base',
118
+ },
119
+ ERC4626_morphoGauntletUsdcPrime_Base: {
120
+ protocol: 'ERC4626',
121
+ chainName: 'Base',
122
+ },
123
+ ERC4626_morphoSeamlessUsdcVault_Base: {
124
+ protocol: 'ERC4626',
125
+ chainName: 'Base',
126
+ },
127
+ ERC4626_morphoSteakhouseHighYieldUsdc_Arbitrum: {
128
+ protocol: 'ERC4626',
129
+ chainName: 'Arbitrum',
130
+ },
131
+ ERC4626_morphoGauntletUsdcCore_Arbitrum: {
132
+ protocol: 'ERC4626',
133
+ chainName: 'Arbitrum',
134
+ },
135
+ ERC4626_morphoHyperithmUsdc_Arbitrum: {
136
+ protocol: 'ERC4626',
137
+ chainName: 'Arbitrum',
138
+ },
139
+ ERC4626_morphoGauntletUsdcPrime_Optimism: {
140
+ protocol: 'ERC4626',
141
+ chainName: 'Optimism',
142
+ },
143
+ } ;
144
+ deepFreeze(ERC4626PoolPlaces);
145
+
146
+ ;
147
+
148
+ export const PoolPlaces = {
149
+ USDN: { protocol: 'USDN', vault: null, chainName: 'noble' }, // MsgSwap only
150
+ USDNVault: { protocol: 'USDN', vault: 1, chainName: 'noble' }, // MsgSwap, MsgLock
151
+ Aave_Avalanche: { protocol: 'Aave', chainName: 'Avalanche' },
152
+ Aave_Ethereum: { protocol: 'Aave', chainName: 'Ethereum' },
153
+ Aave_Optimism: { protocol: 'Aave', chainName: 'Optimism' },
154
+ Aave_Arbitrum: { protocol: 'Aave', chainName: 'Arbitrum' },
155
+ Aave_Base: { protocol: 'Aave', chainName: 'Base' },
156
+ Compound_Ethereum: { protocol: 'Compound', chainName: 'Ethereum' },
157
+ Compound_Optimism: { protocol: 'Compound', chainName: 'Optimism' },
158
+ Compound_Arbitrum: { protocol: 'Compound', chainName: 'Arbitrum' },
159
+ Compound_Base: { protocol: 'Compound', chainName: 'Base' },
160
+ ...BeefyPoolPlaces,
161
+ ...ERC4626PoolPlaces,
162
+ } ;
163
+ deepFreeze(PoolPlaces);
164
+
165
+ ;
166
+
167
+ /**
168
+ * Without regard to supported chains, is the input plausibly an InstrumentId
169
+ * (i.e., does it start with an ASCII letter)?
170
+ */
171
+ export const isInstrumentId = (ref ) =>
172
+ !!ref.match(/^[a-z]/i);
173
+ deepFreeze(isInstrumentId);
174
+
175
+ // XXX Possible to consolidate with {@link getChainNameOfPlaceRef}?
176
+ export const chainOf = (id ) => {
177
+ if (id.startsWith('<')) return 'agoric';
178
+ if (!isInstrumentId(id)) return id.slice(1) ;
179
+ if (Object.hasOwn(PoolPlaces, id)) {
180
+ return PoolPlaces[id ].chainName;
181
+ }
182
+
183
+ // Fallback: syntactic pool id like `${Protocol}_${Chain}` => `${Chain}`.
184
+ // This enables base graph edges for pools even if not listed in PoolPlaces.
185
+ const m = /^([A-Za-z0-9]+)_([A-Za-z0-9-]+)$/.exec(id);
186
+ if (m) return m[2] ;
187
+
188
+ throw Fail`Cannot determine chain for ${id}`;
189
+ };
190
+ deepFreeze(chainOf);
@@ -0,0 +1,42 @@
1
+ import type { Brand, NatAmount, NatValue } from '@agoric/ertp/src/types.js';
2
+ import { type SupportedChain } from './constants.js';
3
+ import type { AssetPlaceRef, TargetAllocation } from './types.js';
4
+ import type { NetworkSpec } from './network/network-spec.js';
5
+ export declare const DEFAULT_DELTA_SOFT_MIN = 1000000n;
6
+ export declare const TargetBalanceError: ErrorConstructor;
7
+ export type ComputeTargetBalancesOptions<C extends AssetPlaceRef, T extends string & keyof TargetAllocation> = {
8
+ /**
9
+ * Brand for the returned nat amounts. It must match the brand used by
10
+ * `currentBalances`.
11
+ */
12
+ brand: Brand<'nat'>;
13
+ /** Current portfolio balances keyed by pool, hub, or local place. */
14
+ currentBalances: Partial<Record<C, NatAmount>>;
15
+ /**
16
+ * Signed balance change to apply before computing targets. Deposits are
17
+ * positive, withdrawals are negative, and rebalances are zero.
18
+ */
19
+ balanceDelta?: NatValue;
20
+ /** Relative target weights. Empty means preserve the non-hub status quo. */
21
+ targetAllocation?: Partial<Pick<TargetAllocation, T>>;
22
+ /** Static network constraints and pool availability. */
23
+ network: NetworkSpec;
24
+ /**
25
+ * Chain providing a deposit. Used to keep funds on a reachable hub when a
26
+ * target sink is unavailable or suppressed.
27
+ */
28
+ depositFromChain?: SupportedChain;
29
+ };
30
+ /**
31
+ * Derive target balances for allocation keys, suppressing small changes and
32
+ * movements blocked by e.g. lack of instrument liquidity or available capacity.
33
+ * When target allocations cannot be satisfied, strive for proportionality and
34
+ * bend the rules for a withdrawal, but do not increase any position beyond its
35
+ * target allocation (opting instead to leave the excess at a non-instrument
36
+ * hub).
37
+ *
38
+ * Returns only entries whose values change by at least ACCOUNT_DUST_EPSILON
39
+ * compared to current.
40
+ */
41
+ export declare const computeTargetBalances: <C extends AssetPlaceRef, T extends string & keyof TargetAllocation>({ brand, currentBalances, balanceDelta, targetAllocation, network, depositFromChain, }: ComputeTargetBalancesOptions<C, T>) => Partial<Record<C | T, NatAmount>>;
42
+ //# sourceMappingURL=target-balances.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"target-balances.d.ts","sourceRoot":"","sources":["target-balances.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAO5E,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAE3E,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAElE,OAAO,KAAK,EAEV,WAAW,EAEZ,MAAM,2BAA2B,CAAC;AAInC,eAAO,MAAM,sBAAsB,WAAa,CAAC;AAEjD,eAAO,MAAM,kBAAkB,EAA6B,gBAAgB,CAAC;AAG7E,MAAM,MAAM,4BAA4B,CACtC,CAAC,SAAS,aAAa,EACvB,CAAC,SAAS,MAAM,GAAG,MAAM,gBAAgB,IACvC;IACF;;;OAGG;IACH,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;IACpB,qEAAqE;IACrE,eAAe,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC;IAC/C;;;OAGG;IACH,YAAY,CAAC,EAAE,QAAQ,CAAC;IACxB,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAC,CAAC;IACtD,wDAAwD;IACxD,OAAO,EAAE,WAAW,CAAC;IACrB;;;OAGG;IACH,gBAAgB,CAAC,EAAE,cAAc,CAAC;CACnC,CAAC;AAwFF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB,GAChC,CAAC,SAAS,aAAa,EACvB,CAAC,SAAS,MAAM,GAAG,MAAM,gBAAgB,EACzC,wFAOC,4BAA4B,CAAC,CAAC,EAAE,CAAC,CAAC,KAAG,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,EAAE,SAAS,CAAC,CA0OvE,CAAC"}
@@ -0,0 +1,394 @@
1
+
2
+ import {
3
+ fromTypedEntries,
4
+ objectMap,
5
+ provideLazyMap,
6
+ typedEntries,
7
+ } from '@agoric/internal';
8
+ import { ACCOUNT_DUST_EPSILON, } from './constants.js';
9
+ import { chainOf, isInstrumentId } from './places.js';
10
+ ;
11
+
12
+ ;
13
+
14
+
15
+
16
+
17
+
18
+ const hardenOrFreeze = globalThis.harden ?? Object.freeze;
19
+
20
+ export const DEFAULT_DELTA_SOFT_MIN = 1_000_000n; // 1 USDC
21
+
22
+ export const TargetBalanceError = class extends Error {} ;
23
+ hardenOrFreeze(TargetBalanceError);
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
+ const placeRecordsByNetwork = new WeakMap
58
+
59
+
60
+ ();
61
+
62
+ const failTargetBalance = (message ) => {
63
+ throw new TargetBalanceError(message);
64
+ };
65
+
66
+ const failInternal = (message ) => {
67
+ throw Error(message);
68
+ };
69
+
70
+ const getOwn = (
71
+ obj ,
72
+ key ,
73
+ ) => (Object.hasOwn(obj, key) ? obj[key] : undefined);
74
+
75
+ const isDust = (value ) =>
76
+ -ACCOUNT_DUST_EPSILON < value && value < ACCOUNT_DUST_EPSILON;
77
+
78
+ // #region XXX These probably belong in @agoric/internal.
79
+ const bigintAbs = (x ) => (x < 0n ? -x : x);
80
+
81
+ const bigintMin = (first , ...rest ) => {
82
+ let min = first;
83
+ for (const arg of rest) {
84
+ if (arg < min) min = arg;
85
+ }
86
+ return min;
87
+ };
88
+ // #endregion
89
+
90
+ const sortEntriesDesc = (
91
+ entries ,
92
+ ) => entries.sort(([_k1, a], [_k2, b]) => (a < b ? 1 : a > b ? -1 : 0));
93
+
94
+ const makeNatAmount = (brand , value ) => {
95
+ value >= 0n ||
96
+ failInternal(`internal: computed negative target balance ${value}`);
97
+ return hardenOrFreeze({ brand, value }) ;
98
+ };
99
+
100
+ const getPlaceData = (
101
+ place ,
102
+ network ,
103
+ ) => {
104
+ // The `chains` and `pools` arrays of `network` are immutable, so we only need
105
+ // to build its corresponding Map<AssetPlaceRef, PlaceRecord> once.
106
+ const placeRecords = provideLazyMap(placeRecordsByNetwork, network, () => {
107
+ const records = new Map ();
108
+ for (const chain of network.chains) {
109
+ records.set(`@${chain.name}` , { chain });
110
+ }
111
+ for (const pool of network.pools) {
112
+ const chainRecord =
113
+ records.get(`@${pool.chain}` ) ??
114
+ failInternal(`No chain found for pool ${pool.pool}`);
115
+ records.set(pool.pool , {
116
+ chain: chainRecord.chain,
117
+ pool,
118
+ });
119
+ }
120
+ return records;
121
+ });
122
+
123
+ // `network.pools` is not necessarily complete.
124
+ return provideLazyMap(placeRecords, place, () => {
125
+ const chainName = chainOf(place);
126
+ const chainRecord =
127
+ placeRecords.get(`@${chainName}` ) ??
128
+ failInternal(`No chain found for asset place ${place}`);
129
+ return { chain: chainRecord.chain };
130
+ });
131
+ };
132
+
133
+ const isNonemptyPositionEntry = (entry ) => {
134
+ const [place, value] = entry;
135
+ return isInstrumentId(place) && value > 0n;
136
+ };
137
+
138
+ /**
139
+ * Derive target balances for allocation keys, suppressing small changes and
140
+ * movements blocked by e.g. lack of instrument liquidity or available capacity.
141
+ * When target allocations cannot be satisfied, strive for proportionality and
142
+ * bend the rules for a withdrawal, but do not increase any position beyond its
143
+ * target allocation (opting instead to leave the excess at a non-instrument
144
+ * hub).
145
+ *
146
+ * Returns only entries whose values change by at least ACCOUNT_DUST_EPSILON
147
+ * compared to current.
148
+ */
149
+ export const computeTargetBalances = (
150
+
151
+
152
+ {
153
+ brand,
154
+ currentBalances,
155
+ balanceDelta = 0n,
156
+ targetAllocation = {},
157
+ network,
158
+ depositFromChain,
159
+ } ) => {
160
+ const currentValues = objectMap(
161
+ currentBalances ,
162
+ amount => amount.value,
163
+ ) ;
164
+ const currentTotal = Object.values (
165
+ currentValues ,
166
+ ).reduce((acc, value) => acc + value, 0n);
167
+ const total = currentTotal + balanceDelta;
168
+ total >= 0n || failTargetBalance('Insufficient funds for withdrawal.');
169
+ let liquidTotal = total;
170
+
171
+ ;
172
+ const allWeights = Object.keys(targetAllocation).length
173
+ ? (typedEntries({
174
+ // Any current balance with no target has an effective weight of 0.
175
+ ...objectMap(currentValues, () => 0n),
176
+ ...(targetAllocation ),
177
+ } ) )
178
+ : // In the absence of target weights, maintain the relative status quo but
179
+ // zero out hubs (chains) if there is anywhere else to deploy their funds.
180
+ (valueEntries => {
181
+ return valueEntries.some(isNonemptyPositionEntry)
182
+ ? valueEntries.map(([p, v]) => [p, isInstrumentId(p) ? v : 0n] )
183
+ : (valueEntries );
184
+ })(typedEntries(currentValues) );
185
+ allWeights.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
186
+ let sumW = allWeights.reduce((acc, entry) => acc + entry[1], 0n);
187
+ sumW > 0n ||
188
+ failTargetBalance('Total target allocation weights must be positive.');
189
+
190
+ ;
191
+
192
+
193
+
194
+
195
+
196
+
197
+
198
+
199
+
200
+
201
+
202
+ const makeDraftRecord = (
203
+ place ,
204
+ weight = 0n,
205
+ ) => {
206
+ const placeData = getPlaceData(place, network);
207
+ return {
208
+ place,
209
+ chain: placeData.chain,
210
+ weight,
211
+ current: getOwn(currentValues, place) ?? 0n,
212
+ blockDeposit: !!placeData.pool?.blockDepositReason,
213
+ blockWithdraw: !!placeData.pool?.blockWithdrawReason,
214
+ deltaSoftMin: placeData.chain.deltaSoftMin ?? DEFAULT_DELTA_SOFT_MIN,
215
+
216
+ target: 0n,
217
+ delta: 0n,
218
+ resolvedDelta: 0n,
219
+ };
220
+ };
221
+ const draft = Object.assign(
222
+ Object.create(null) ,
223
+ fromTypedEntries(
224
+ allWeights.map(([place, weight]) => {
225
+ return [place, makeDraftRecord(place, weight)] ;
226
+ }),
227
+ ),
228
+ );
229
+
230
+ // Blocked/suppressed sources proportionally reduce the other targets,
231
+ // potentially even cascading into new blocked sources (e.g., A/B/C/D target
232
+ // balances 40/20/20/20 can become 52/16/16/16 from A being withdraw-blocked
233
+ // at current 52, and then 52/15/15/18 from the originally-a-sink D being
234
+ // withdraw-blocked at current 18.
235
+ const suppressions = new Map ();
236
+ for (;;) {
237
+ const badSources = [];
238
+ const needsSuppress = [];
239
+ for (const [place, draftRecord] of typedEntries(draft)) {
240
+ if (suppressions.has(place)) continue;
241
+ const { weight, current, blockDeposit, blockWithdraw } = draftRecord;
242
+ const target = (liquidTotal * weight) / sumW; // rounds down
243
+ /** positive delta is a sink, negative delta is a source */
244
+ const delta = target - current;
245
+ Object.assign(draftRecord, { target, delta });
246
+ const absDelta = bigintAbs(delta);
247
+ const isBlocked =
248
+ (delta > 0n && blockDeposit) || (delta < 0n && blockWithdraw);
249
+ const isSuppressed = delta !== 0n && absDelta < draftRecord.deltaSoftMin;
250
+ if (isBlocked && delta < 0n) {
251
+ badSources.push([draftRecord, 0n]);
252
+ } else if (isSuppressed && delta < 0n) {
253
+ needsSuppress.push([draftRecord, draftRecord.deltaSoftMin - absDelta]);
254
+ }
255
+ draftRecord.resolvedDelta = isBlocked || isSuppressed ? 0n : delta;
256
+ }
257
+
258
+ if (badSources.length === 0) {
259
+ // No sources to block, but there still might be suppressions.
260
+ // Cement those with the largest gap.
261
+ sortEntriesDesc(needsSuppress);
262
+ for (let i = 0; i < needsSuppress.length; i += 1) {
263
+ const gap = needsSuppress[i][1];
264
+ if (i > 0 && gap !== needsSuppress[i - 1][1]) break;
265
+ badSources.push(needsSuppress[i]);
266
+ }
267
+ }
268
+ for (const [source, gap] of badSources) {
269
+ const { place, current, weight, blockWithdraw } = source;
270
+ // A blocked source is not usable even as a withdrawal fallback.
271
+ if (blockWithdraw) delete draft[place];
272
+ if (suppressions.has(place)) continue;
273
+ suppressions.set(place, gap);
274
+ liquidTotal -= current;
275
+ sumW -= weight;
276
+ }
277
+ if (badSources.length === 0 || liquidTotal < 0n) break;
278
+ }
279
+
280
+ // If all sources are suppressed for a deposit or rebalance, we're done.
281
+ if (liquidTotal <= 0n && balanceDelta >= 0n) return {};
282
+
283
+ // If all sources are suppressed for a withdrawal, try to succeed anyway but
284
+ // minimize the count of affected places rather than the divergence from
285
+ // allocation.
286
+ if (liquidTotal < 0n && balanceDelta < 0n) {
287
+ const fallback = Object.create(null) ;
288
+ let unsatisfied = -balanceDelta;
289
+ for (const [place, value] of sortEntriesDesc(
290
+ typedEntries(currentValues) ,
291
+ )) {
292
+ if (isDust(value)) break;
293
+ if (draft[place]?.blockWithdraw !== false) continue;
294
+ const take = bigintMin(unsatisfied, value);
295
+ fallback[place] = makeNatAmount(brand, value - take);
296
+ unsatisfied -= take;
297
+ if (unsatisfied === 0n) return { ...fallback };
298
+ }
299
+ // TODO(AGO-535): Effect a partial withdrawal here if necessary (e.g., when
300
+ // some requested funds are in a low-liquidity position).
301
+ return {};
302
+ }
303
+
304
+ // Blocked/suppressed *sinks* just leave funds in a source chain account.
305
+ // We track chain-level outflow to know which one.
306
+ const outByChain = {};
307
+ for (const [place, { chain, delta }] of typedEntries(draft)) {
308
+ if (suppressions.has(place)) continue;
309
+ const chainName = chain.name;
310
+ outByChain[chainName] = (getOwn(outByChain, chainName) ?? 0n) - delta;
311
+ }
312
+ if (depositFromChain) {
313
+ outByChain[depositFromChain] =
314
+ (getOwn(outByChain, depositFromChain) ?? 0n) + balanceDelta;
315
+ }
316
+ const donorChainsDesc = sortEntriesDesc(
317
+ typedEntries(outByChain) ,
318
+ );
319
+ let remainder = liquidTotal;
320
+ const pending = new Set (Object.values(draft));
321
+ for (const draftRecord of pending) {
322
+ pending.delete(draftRecord);
323
+ const { place, current, delta, resolvedDelta } = draftRecord;
324
+ if (suppressions.has(place)) continue;
325
+ // No adjustment is necessary for a source and/or satisfied delta.
326
+ if (delta <= 0n || resolvedDelta !== 0n) {
327
+ const newBalance = current + resolvedDelta;
328
+ remainder -= newBalance;
329
+ continue;
330
+ }
331
+
332
+ // This sink cannot receive its inbound funds. If its chain is a net source
333
+ // or neutral, we leave them at the local hub. Otherwise, we reduce the net
334
+ // outflow from one or more donor-chain hubs.
335
+ remainder -= current;
336
+ const local = getPlaceData(place, network).chain;
337
+ const localNetOut = donorChainsDesc.find(([n]) => n === local.name) [1];
338
+ if (localNetOut >= 0n) {
339
+ const chainPlace = `@${local.name}` ;
340
+ draft[chainPlace] ??= makeDraftRecord(chainPlace);
341
+ draft[chainPlace].resolvedDelta += delta;
342
+ if (!pending.has(draft[chainPlace])) remainder -= delta;
343
+ } else {
344
+ let excess = delta;
345
+ for (const donorEntry of donorChainsDesc) {
346
+ const [chainName, netOut] = donorEntry;
347
+ if (excess === 0n || netOut <= 0n) break;
348
+ const chainPlace = `@${chainName}` ;
349
+ draft[chainPlace] ??= makeDraftRecord(chainPlace);
350
+ const d = bigintMin(excess, netOut);
351
+ if (!pending.has(draft[chainPlace])) remainder -= d;
352
+ draft[chainPlace].resolvedDelta += d;
353
+ donorEntry[1] -= d;
354
+ excess -= d;
355
+ }
356
+ if (excess !== 0n) {
357
+ failInternal(`internal: Unable to suppress ${place}`);
358
+ }
359
+ sortEntriesDesc(donorChainsDesc);
360
+ }
361
+ }
362
+
363
+ // We have our targets. Distribute any rounding loss to the highest-weight
364
+ // place that can accept it.
365
+ // XXX We should instead redistribute to minimize error.
366
+ if (remainder !== 0n) {
367
+ const weightsDesc = sortEntriesDesc(allWeights);
368
+ for (const [place, _w] of weightsDesc) {
369
+ if (draft[place]?.blockDeposit) continue;
370
+ draft[place] ??= makeDraftRecord(place);
371
+ const newDelta = draft[place].resolvedDelta + remainder;
372
+ if (newDelta === 0n || !isDust(newDelta)) {
373
+ draft[place].resolvedDelta = newDelta;
374
+ remainder = 0n;
375
+ break;
376
+ }
377
+ }
378
+ remainder === 0n ||
379
+ failTargetBalance(
380
+ `Nowhere to place ${remainder} in target balance update`,
381
+ );
382
+ }
383
+
384
+ // Return a mutable Record that omits no-change entries.
385
+ const result = Object.create(null) ;
386
+ for (const [place, draftRecord] of typedEntries(draft)) {
387
+ const { current, resolvedDelta } = draftRecord;
388
+ if (resolvedDelta === 0n) continue;
389
+ const targetValue = current + resolvedDelta;
390
+ result[place] = makeNatAmount(brand, targetValue);
391
+ }
392
+ return { ...result };
393
+ };
394
+ hardenOrFreeze(computeTargetBalances);
@@ -1,6 +1,6 @@
1
- import type { BeefyInstrumentId, ERC4626InstrumentId } from '@aglocal/portfolio-contract/src/type-guards.js';
2
- import type { InstrumentId } from './instruments.js';
1
+ import type { BeefyInstrumentId, ERC4626InstrumentId } from './places.js';
3
2
  import type { DepositFromChainRef, LocalChainAccountRef, InterChainAccountRef, WithdrawToChainRef } from './types.js';
3
+ export { isInstrumentId } from './places.js';
4
4
  /**
5
5
  * Without regard to supported chains, is the input plausibly a
6
6
  * DepositFromChainRef (i.e., does it start with `+`)?
@@ -16,11 +16,6 @@ export declare const isLocalChainAccountRef: (ref: string) => ref is LocalChainA
16
16
  * InterChainAccountRef (i.e., does it start with `@`)?
17
17
  */
18
18
  export declare const isInterChainAccountRef: (ref: string) => ref is InterChainAccountRef;
19
- /**
20
- * Without regard to supported chains, is the input plausibly an InstrumentId
21
- * (i.e., does it start with an ASCII letter)?
22
- */
23
- export declare const isInstrumentId: (ref: string) => ref is InstrumentId;
24
19
  /**
25
20
  * Without regard to supported chains, is the input plausibly a
26
21
  * WithdrawToChainRef (i.e., does it start with `-`)?
@@ -1 +1 @@
1
- {"version":3,"file":"type-guards.d.ts","sourceRoot":"","sources":["type-guards.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EACjB,mBAAmB,EACpB,MAAM,gDAAgD,CAAC;AACxD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAK,EACV,mBAAmB,EACnB,oBAAoB,EACpB,oBAAoB,EACpB,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAEpB;;;GAGG;AACH,eAAO,MAAM,qBAAqB,GAChC,KAAK,MAAM,KACV,GAAG,IAAI,mBAA0C,CAAC;AAGrD;;;GAGG;AACH,eAAO,MAAM,sBAAsB,GACjC,KAAK,MAAM,KACV,GAAG,IAAI,oBAA2C,CAAC;AAGtD;;;GAGG;AACH,eAAO,MAAM,sBAAsB,GACjC,KAAK,MAAM,KACV,GAAG,IAAI,oBAA2C,CAAC;AAGtD;;;GAGG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,KAAG,GAAG,IAAI,YAC5B,CAAC;AAGzB;;;GAGG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,MAAM,KAAG,GAAG,IAAI,kBACrC,CAAC;AAGtB;;;GAGG;AACH,eAAO,MAAM,qBAAqB,GAChC,KAAK,MAAM,KACV,GAAG,IAAI,mBAAiD,CAAC;AAG5D;;;GAGG;AACH,eAAO,MAAM,mBAAmB,GAAI,KAAK,MAAM,KAAG,GAAG,IAAI,iBAC/B,CAAC"}
1
+ {"version":3,"file":"type-guards.d.ts","sourceRoot":"","sources":["type-guards.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAC1E,OAAO,KAAK,EACV,mBAAmB,EACnB,oBAAoB,EACpB,oBAAoB,EACpB,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C;;;GAGG;AACH,eAAO,MAAM,qBAAqB,GAChC,KAAK,MAAM,KACV,GAAG,IAAI,mBAA0C,CAAC;AAGrD;;;GAGG;AACH,eAAO,MAAM,sBAAsB,GACjC,KAAK,MAAM,KACV,GAAG,IAAI,oBAA2C,CAAC;AAGtD;;;GAGG;AACH,eAAO,MAAM,sBAAsB,GACjC,KAAK,MAAM,KACV,GAAG,IAAI,oBAA2C,CAAC;AAGtD;;;GAGG;AACH,eAAO,MAAM,oBAAoB,GAAI,KAAK,MAAM,KAAG,GAAG,IAAI,kBACrC,CAAC;AAGtB;;;GAGG;AACH,eAAO,MAAM,qBAAqB,GAChC,KAAK,MAAM,KACV,GAAG,IAAI,mBAAiD,CAAC;AAG5D;;;GAGG;AACH,eAAO,MAAM,mBAAmB,GAAI,KAAK,MAAM,KAAG,GAAG,IAAI,iBAC/B,CAAC"}
@@ -1,8 +1,4 @@
1
-
2
-
3
-
4
-
5
-
1
+
6
2
 
7
3
 
8
4
 
@@ -10,6 +6,8 @@
10
6
 
11
7
 
12
8
 
9
+ export { isInstrumentId } from './places.js';
10
+
13
11
  /**
14
12
  * Without regard to supported chains, is the input plausibly a
15
13
  * DepositFromChainRef (i.e., does it start with `+`)?
@@ -37,14 +35,6 @@ export const isInterChainAccountRef = (
37
35
  ) => ref.startsWith('@');
38
36
  harden(isInterChainAccountRef);
39
37
 
40
- /**
41
- * Without regard to supported chains, is the input plausibly an InstrumentId
42
- * (i.e., does it start with an ASCII letter)?
43
- */
44
- export const isInstrumentId = (ref ) =>
45
- !!ref.match(/^[a-z]/i);
46
- harden(isInstrumentId);
47
-
48
38
  /**
49
39
  * Without regard to supported chains, is the input plausibly a
50
40
  * WithdrawToChainRef (i.e., does it start with `-`)?