@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/README.md +38 -0
- package/package.json +18 -4
- package/src/main.d.ts +2 -0
- package/src/main.js +2 -0
- package/src/network/network-spec.d.ts +88 -0
- package/src/network/network-spec.d.ts.map +1 -0
- package/src/network/network-spec.js +115 -0
- package/src/network/prod-network.d.ts +4 -0
- package/src/network/prod-network.d.ts.map +1 -0
- package/src/network/prod-network.js +399 -0
- package/src/places.d.ts +289 -0
- package/src/places.d.ts.map +1 -0
- package/src/places.js +190 -0
- package/src/target-balances.d.ts +42 -0
- package/src/target-balances.d.ts.map +1 -0
- package/src/target-balances.js +394 -0
- package/src/type-guards.d.ts +2 -7
- package/src/type-guards.d.ts.map +1 -1
- package/src/type-guards.js +3 -13
- package/src/types-index.d.ts +2 -0
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);
|
package/src/type-guards.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { BeefyInstrumentId, ERC4626InstrumentId } from '
|
|
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 `-`)?
|
package/src/type-guards.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"type-guards.d.ts","sourceRoot":"","sources":["type-guards.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
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"}
|
package/src/type-guards.js
CHANGED
|
@@ -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 `-`)?
|