@meridian-capital/core 0.0.0-stage → 0.1.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/README.md +92 -2
- package/dist/addresses.d.ts +36 -0
- package/dist/addresses.js +31 -0
- package/dist/api/holders.d.ts +94 -0
- package/dist/api/holders.js +24 -0
- package/dist/api/multisig.d.ts +216 -0
- package/dist/api/multisig.js +78 -0
- package/dist/api/treasury.d.ts +99 -0
- package/dist/api/treasury.js +18 -0
- package/dist/api/yield.d.ts +384 -0
- package/dist/api/yield.js +1 -0
- package/dist/chain.d.ts +8 -0
- package/dist/chain.js +15 -0
- package/dist/format.d.ts +2 -0
- package/dist/format.js +8 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/math/concentratedLiquidity.d.ts +10 -0
- package/dist/math/concentratedLiquidity.js +60 -0
- package/dist/math/launch.d.ts +33 -0
- package/dist/math/launch.js +21 -0
- package/dist/math/pairPrice.d.ts +55 -0
- package/dist/math/pairPrice.js +84 -0
- package/dist/tokens.d.ts +40 -0
- package/dist/tokens.js +196 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.js +1 -0
- package/package.json +23 -4
package/README.md
CHANGED
|
@@ -1,3 +1,93 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @meridian-capital/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Meridian's chain facts, pure maths and API types, written once and shared by meridian-frontend (the
|
|
4
|
+
site and its API functions), the internal dashboard and the contracts repo's `ts-scripts`. It
|
|
5
|
+
exists so a token address, an RPC list, a liquidity formula or the shape of an API answer has one
|
|
6
|
+
copy rather than three that drift.
|
|
7
|
+
|
|
8
|
+
The source lives here, in meridian-frontend, which uses it straight from this folder. Everyone else
|
|
9
|
+
installs it from npm once it is published (until then the dashboard keeps copies in its `src/data/*Wire.ts`): `pnpm add @meridian-capital/core` (or `bun add`), with `viem` beside it. It is public:
|
|
10
|
+
nothing in it is a secret, and a public package needs no token to install.
|
|
11
|
+
|
|
12
|
+
## Rules
|
|
13
|
+
|
|
14
|
+
- **Runs anywhere.** No Bun or Node APIs (`fs`, `process`, `Buffer`) and no DOM. The same file runs
|
|
15
|
+
in the browser, in a Vercel edge function and under Bun.
|
|
16
|
+
- **Nothing happens at import.** No env reads, no clients, no network. A caller that needs an RPC
|
|
17
|
+
passes the URL (`rpcUrls(process.env.ROBINHOOD_RPC_URL)`). A future read client takes `fetch` and
|
|
18
|
+
its key as arguments.
|
|
19
|
+
- **No assets.** Token logos stay in `src/constants/tokens.ts`, which extends `TokenRecord` with
|
|
20
|
+
them. Edge functions cannot import a `.png`.
|
|
21
|
+
- **viem is a peer dependency.** Only types and pure helpers come from it. The package never owns a
|
|
22
|
+
copy.
|
|
23
|
+
- **Relative imports only inside the package, with a `.js` extension** (`./types.js`), so the built
|
|
24
|
+
`dist/` loads in Node and Vitest as well as in a bundler. It has no `@/` alias of its own.
|
|
25
|
+
|
|
26
|
+
## What is in it
|
|
27
|
+
|
|
28
|
+
| Module | What it holds |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| `types` | `Address`, `Amount` (base units), `Bps`, `Unix` |
|
|
31
|
+
| `chain` | `CHAIN_ID` 4663, `MULTICALL3`, `EXPLORER_URL`, `MAINNET_RPCS`, `rpcUrls(keyed?)` |
|
|
32
|
+
| `addresses` | `ContractAddresses` and `MAINNET_ADDRESSES`, the deployed Meridian contracts |
|
|
33
|
+
| `tokens` | `TOKEN_RECORDS`: every token Meridian knows, with address, decimals, treasury category and whether it is a genesis deposit token, without logos; lookups by symbol and by address; `MCD_TOKEN` and `GMCD_TOKEN`, their fixed facts |
|
|
34
|
+
| `format` | `shortAddress` |
|
|
35
|
+
| `math/concentratedLiquidity` | `sqrtPriceAtTick`, `amountsForLiquidity`: Uniswap V3/V4 amounts in bigint, with no SDK; `sqrtPriceOf` as a float |
|
|
36
|
+
| `math/pairPrice` | `priceFromReserves`, `reservesBySide`, `pairAddress` (V2 CREATE2), `mcdPriceUsd1e18` |
|
|
37
|
+
| `math/launch` | where MCD's launch stands (`launchStage`) and the chain's clock off one read (`chainNow`) |
|
|
38
|
+
| `api/yield` | The yield board's types, and what `api/yield-board`, `yield-pool-state`, `yield-price-history` and the feeds answer (`YieldBoardWire`, `PoolStateWire`, `PriceHistoryWire`, ...) |
|
|
39
|
+
| `api/treasury` | The treasury rows as `valueTreasury` gives them, `TreasuryWire` as `api/treasury` answers, and `fromTreasuryWire` |
|
|
40
|
+
| `api/holders` | MCD's holders, `McdHoldersWire` as `api/mcd-holders` answers, and `parseMcdHolders` |
|
|
41
|
+
| `api/multisig` | The Safes and their transactions, `DescribedMultisigsWire` as `api/multisig` answers, and `parseMultisigs` / `parseDescribedMultisigs` |
|
|
42
|
+
|
|
43
|
+
Tests sit next to the code (`*.test.ts`). The root `pnpm test` runs them.
|
|
44
|
+
|
|
45
|
+
## How it is imported
|
|
46
|
+
|
|
47
|
+
| From | Import |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| `src/` (the app, built by Vite) | `import { CHAIN_ID } from "@meridian-capital/core"`, through the alias in `vite.config.ts`, `vitest.config.ts` and `tsconfig.app.json` |
|
|
50
|
+
| `api/` and every `src/` file an API function reaches | a relative path: `../packages/core/src/chain` |
|
|
51
|
+
| The dashboard, `ts-scripts`, anything else | `import { CHAIN_ID } from "@meridian-capital/core"`, installed from npm: the built `dist/` (ESM and type declarations, for a bundler or Bun) |
|
|
52
|
+
|
|
53
|
+
Vercel bundles `api/` without the Vite alias, so any file an API function reaches must use relative
|
|
54
|
+
paths all the way down, including `src/data/onchain/yield.ts` and `src/utils/yield/merge.ts`.
|
|
55
|
+
`tsconfig.node.json` typechecks `api/` with no `paths`, so an aliased import there fails
|
|
56
|
+
`pnpm typecheck` before it can fail a deploy.
|
|
57
|
+
|
|
58
|
+
## Releasing a version
|
|
59
|
+
|
|
60
|
+
1. Change the code here, in a PR; meridian-frontend uses it at once.
|
|
61
|
+
2. Bump `version` in `packages/core/package.json` (semver: a removed or renamed export is a major).
|
|
62
|
+
3. Tag the PR's commit with that version and push the tag: `git tag core-v0.2.0 && git push origin
|
|
63
|
+
core-v0.2.0`. The tag, not a branch, starts `.github/workflows/publish-core.yml`, so a release
|
|
64
|
+
needs no merge first. The workflow checks the tag against the version, runs the package's
|
|
65
|
+
tests, builds it (`pnpm build:core`) and publishes it with the repository secret `NPM_TOKEN`.
|
|
66
|
+
4. Each consumer takes it when it needs it: `pnpm up @meridian-capital/core`, or let Renovate open the PR.
|
|
67
|
+
|
|
68
|
+
`pnpm build:core && cd packages/core && npm pack --dry-run` shows what a release would contain.
|
|
69
|
+
|
|
70
|
+
## Taking it into the other repos
|
|
71
|
+
|
|
72
|
+
- **The internal dashboard**: its `src/data/*Wire.ts` copies and the chain facts in
|
|
73
|
+
`src/constants/chains.ts` become imports from `@meridian-capital/core` (its `SafeTx`, `SafeAccount` and
|
|
74
|
+
`Multisigs` are core's `DescribedSafeTx`, `DescribedSafeAccount` and `DescribedMultisigs`).
|
|
75
|
+
- **`ts-scripts`** (contracts repo), replacing these local copies:
|
|
76
|
+
|
|
77
|
+
| ts-scripts today | core |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| `lib/chain.ts` `CHAIN_ID`, `EXPLORER_URL`, `RPC_URLS` | `chain` (`rpcUrls(process.env.RPC_URL)`) |
|
|
80
|
+
| `lib/chain.ts` `WETH`, `USDG`, `USDG_DECIMALS`, factory and pool-manager addresses | `tokens`, `addresses` |
|
|
81
|
+
| `config/deployment.ts` contract addresses | `addresses` |
|
|
82
|
+
| `config/tokens.ts` `RobinhoodTokens` | `tokens` |
|
|
83
|
+
| `lib/uniswap-math.ts` `sqrtRatioAtTick`, `principal` (on `consolidate/research`, built on `@uniswap/v3-sdk`) | `math/concentratedLiquidity` `sqrtPriceAtTick`, `amountsForLiquidity` |
|
|
84
|
+
|
|
85
|
+
Keep `txUrl`/`addressUrl`, `defineChain` and the deploy blocks in ts-scripts until a second
|
|
86
|
+
caller needs them.
|
|
87
|
+
|
|
88
|
+
Next to move in, in this order:
|
|
89
|
+
- read clients that take `fetch` and keys as arguments: Blockscout logs, DefiLlama, Uniswap price
|
|
90
|
+
history, the Uniswap positions backend, Fables, Merkl, Safe reads;
|
|
91
|
+
- the readers built on them: TWAP buys, Furnace events, the MCD holder scan.
|
|
92
|
+
|
|
93
|
+
Signing, Safe proposals, keepers, Tenderly, quoters and disk caches stay in ts-scripts.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Address } from "./types.js";
|
|
2
|
+
export interface ContractAddresses {
|
|
3
|
+
mcd: Address;
|
|
4
|
+
gmcd: Address;
|
|
5
|
+
genesisFarm: Address;
|
|
6
|
+
convertible: Address;
|
|
7
|
+
treasuryWallets: readonly Address[];
|
|
8
|
+
/** Uniswap V2 factory, for looking up MCD's pair against each Genesis asset. */
|
|
9
|
+
uniswapV2Factory: Address;
|
|
10
|
+
/** The position NFTs' contracts: where a listed position's owner and each wallet's count are checked. */
|
|
11
|
+
uniswapV3PositionManager: Address;
|
|
12
|
+
uniswapV4PositionManager: Address;
|
|
13
|
+
/** FablesFi's pool registry, which names the hook a range's ERC-6909 shares sit on, and the lens that reads them. */
|
|
14
|
+
fablesPoolRegistry: Address;
|
|
15
|
+
fablesLens: Address;
|
|
16
|
+
/** ArrowFarm's MasterChef, where the treasury stakes its ArrowFarm vault shares. */
|
|
17
|
+
arrowChef: Address;
|
|
18
|
+
/** StonkPress's genesis farm, where the treasury stakes single tokens and LP. */
|
|
19
|
+
stonkGenesisPool: Address;
|
|
20
|
+
furnace: Address;
|
|
21
|
+
/** Morpho Blue, where the treasury lends, and the markets it lends into, by id. */
|
|
22
|
+
morpho: Address;
|
|
23
|
+
morphoMarkets: readonly `0x${string}`[];
|
|
24
|
+
/** Uniswap V4's PoolManager, whose storage the yield board reads a pool's price and liquidity out of. */
|
|
25
|
+
uniswapV4PoolManager: Address;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Meridian's contracts on Robinhood Chain mainnet, and the other protocols' it reads. The treasury
|
|
29
|
+
* wallets are the ones the dashboard reads balances and positions for, not Meridian contracts.
|
|
30
|
+
* The Uniswap V3 / V4 entries are Uniswap's own deployment on the chain; `uniswapV4PoolManager` is
|
|
31
|
+
* the one the yield board reads pool state out of. `fablesPoolRegistry` maps a pool id to the hook
|
|
32
|
+
* holding its liquidity, and `fablesLens` is the lens Fables' own portfolio page reads them through.
|
|
33
|
+
* `stonkGenesisPool` is StonkPress's genesis farm. `morphoMarkets` are the Morpho markets the
|
|
34
|
+
* treasury lends into.
|
|
35
|
+
*/
|
|
36
|
+
export declare const MAINNET_ADDRESSES: ContractAddresses;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Meridian's contracts on Robinhood Chain mainnet, and the other protocols' it reads. The treasury
|
|
3
|
+
* wallets are the ones the dashboard reads balances and positions for, not Meridian contracts.
|
|
4
|
+
* The Uniswap V3 / V4 entries are Uniswap's own deployment on the chain; `uniswapV4PoolManager` is
|
|
5
|
+
* the one the yield board reads pool state out of. `fablesPoolRegistry` maps a pool id to the hook
|
|
6
|
+
* holding its liquidity, and `fablesLens` is the lens Fables' own portfolio page reads them through.
|
|
7
|
+
* `stonkGenesisPool` is StonkPress's genesis farm. `morphoMarkets` are the Morpho markets the
|
|
8
|
+
* treasury lends into.
|
|
9
|
+
*/
|
|
10
|
+
export const MAINNET_ADDRESSES = {
|
|
11
|
+
mcd: "0x378A50eaC56f45Fb74e4cE7F1962dbCA33515c14",
|
|
12
|
+
gmcd: "0x296182D54dc5cBe6d960b9cFe3882dD5c87d51fd",
|
|
13
|
+
genesisFarm: "0xE5BffCF30D18c27Dac29b2EDF8a608b2E2BB52F7",
|
|
14
|
+
convertible: "0xf948Cd016956A942AAd10C3181e6B6fb49e30ef4",
|
|
15
|
+
treasuryWallets: [
|
|
16
|
+
"0xD57056275A348E7C71dD80C8ddf6d82B1C808749",
|
|
17
|
+
"0xF186674191b6CC037c699Ec066CDB14971cfE797",
|
|
18
|
+
"0xcF2d75B69e723246722B7D4cCdA7DDC762B2fC55",
|
|
19
|
+
],
|
|
20
|
+
uniswapV2Factory: "0x8bcEaA40B9AcdfAedF85AdF4FF01F5Ad6517937f",
|
|
21
|
+
uniswapV3PositionManager: "0x73991a25C818Bf1f1128dEAaB1492D45638DE0D3",
|
|
22
|
+
uniswapV4PositionManager: "0x58daec3116aae6D93017bAAea7749052E8a04fA7",
|
|
23
|
+
uniswapV4PoolManager: "0x8366a39CC670B4001A1121B8F6A443A643e40951",
|
|
24
|
+
fablesPoolRegistry: "0x159A113E012593D9B3cC63ad45E30F0467e13Ef3",
|
|
25
|
+
fablesLens: "0xE44c0BAb43BdD47e7Ab40236bC183dCc77A9ED6c",
|
|
26
|
+
arrowChef: "0x0591F89386eBfF9AA52903939774E218eA25eC55",
|
|
27
|
+
stonkGenesisPool: "0x935661973d9379b792bcb110d1eebd05034b838f",
|
|
28
|
+
furnace: "0x25E06c975219C91cf05Ba3cC4030A542317b0Fc6",
|
|
29
|
+
morpho: "0x9D53d5E3bd5E8d4Cbfa6DB1ca238AEA02E651010",
|
|
30
|
+
morphoMarkets: ["0xaa586d26a6fe62d9c0f0948fede6e2130500ac7a655587447e2d4a37e6330589"],
|
|
31
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { Address, Amount, Unix } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* MCD's holders as meridian-frontend's `api/mcd-holders` serves them: every address holding one MCD
|
|
4
|
+
* or more, named, and where its MCD sits. MCD travels in base units (18 decimals) as decimal strings;
|
|
5
|
+
* `parseMcdHolders` revives them.
|
|
6
|
+
*/
|
|
7
|
+
/** Where a holder's MCD sits: loose in the wallet, behind Uniswap V2 LP it holds, or staked in a farm. */
|
|
8
|
+
export type McdHoldingKind = "wallet" | "lp" | "staked";
|
|
9
|
+
export interface McdHolding {
|
|
10
|
+
kind: McdHoldingKind;
|
|
11
|
+
/** "Uniswap V2 MCD/USDG", "StonkPress MCD", "StonkPress MCD/WETH LP"; null for the wallet. */
|
|
12
|
+
venue: string | null;
|
|
13
|
+
/** The pair or the farm; null for the wallet. */
|
|
14
|
+
contract: Address | null;
|
|
15
|
+
/** The MCD behind it, base units. */
|
|
16
|
+
mcd: Amount;
|
|
17
|
+
}
|
|
18
|
+
export interface McdHolder {
|
|
19
|
+
address: Address;
|
|
20
|
+
/** The treasury's or a Meridian contract's name, else its ENS or DeBank name; null where none is known. */
|
|
21
|
+
name: string | null;
|
|
22
|
+
/** Set on the treasury's wallets and Meridian's contracts, so they are not read as the community. */
|
|
23
|
+
role: HolderRole | null;
|
|
24
|
+
/** The X handle, without the `@`. */
|
|
25
|
+
x: string | null;
|
|
26
|
+
/** Every holding added up. */
|
|
27
|
+
total: Amount;
|
|
28
|
+
/** Largest first; a kind with nothing in it is left out. */
|
|
29
|
+
holdings: McdHolding[];
|
|
30
|
+
}
|
|
31
|
+
/** Every address holding at least one MCD, read off the token's transfers and balances. */
|
|
32
|
+
export interface McdHolders {
|
|
33
|
+
supply: Amount;
|
|
34
|
+
/** How many addresses MCD has ever been sent to; `holders` keeps those with one MCD or more. */
|
|
35
|
+
addresses: number;
|
|
36
|
+
/** Largest first. */
|
|
37
|
+
holders: McdHolder[];
|
|
38
|
+
block: number;
|
|
39
|
+
asOf: Unix;
|
|
40
|
+
}
|
|
41
|
+
/** A Uniswap V2 pair with MCD on one side. Its MCD is counted to the addresses holding its LP. */
|
|
42
|
+
export interface McdPairWire {
|
|
43
|
+
address: string;
|
|
44
|
+
/** The other side's token and symbol, as the token answers it. */
|
|
45
|
+
quote: string;
|
|
46
|
+
quoteSymbol: string;
|
|
47
|
+
reserveMcd: string;
|
|
48
|
+
lpSupply: string;
|
|
49
|
+
}
|
|
50
|
+
/** A StonkPress genesis-farm pool that takes MCD, or the LP of an MCD pair. */
|
|
51
|
+
export interface McdFarmPoolWire {
|
|
52
|
+
pid: number;
|
|
53
|
+
token: string;
|
|
54
|
+
/** The pair whose LP the pool takes; null when it takes MCD itself. */
|
|
55
|
+
pair: string | null;
|
|
56
|
+
}
|
|
57
|
+
/** A Meridian contract or the treasury, named so it is not read as a holder among the community. */
|
|
58
|
+
export type HolderRole = "treasury" | "protocol";
|
|
59
|
+
export interface McdHolderWire {
|
|
60
|
+
address: string;
|
|
61
|
+
/** Who the address is, where known: the treasury's or a Meridian contract's name, else its ENS or DeBank name. */
|
|
62
|
+
name?: string;
|
|
63
|
+
/** Set on the treasury's wallets and Meridian's contracts. */
|
|
64
|
+
role?: HolderRole;
|
|
65
|
+
/** The holder's X handle, without the `@`. */
|
|
66
|
+
x?: string;
|
|
67
|
+
/** MCD in the wallet. */
|
|
68
|
+
wallet: string;
|
|
69
|
+
/** The MCD behind the pair LP the wallet holds, per pair. */
|
|
70
|
+
lp: {
|
|
71
|
+
pair: string;
|
|
72
|
+
mcd: string;
|
|
73
|
+
}[];
|
|
74
|
+
/** The MCD behind what the wallet has staked in the farm, per pool. */
|
|
75
|
+
staked: {
|
|
76
|
+
pid: number;
|
|
77
|
+
mcd: string;
|
|
78
|
+
}[];
|
|
79
|
+
}
|
|
80
|
+
export interface McdHoldersWire {
|
|
81
|
+
/** The block every balance was read at, and when, unix seconds. */
|
|
82
|
+
block: number;
|
|
83
|
+
asOf: number;
|
|
84
|
+
/** `MCD.totalSupply()`. */
|
|
85
|
+
supply: string;
|
|
86
|
+
/** How many addresses MCD has ever been sent to; `holders` keeps those with at least one MCD. */
|
|
87
|
+
addresses: number;
|
|
88
|
+
farm: string;
|
|
89
|
+
pairs: McdPairWire[];
|
|
90
|
+
farmPools: McdFarmPoolWire[];
|
|
91
|
+
holders: McdHolderWire[];
|
|
92
|
+
}
|
|
93
|
+
/** The wire's decimal strings to amounts, each holding named after its venue, largest first. */
|
|
94
|
+
export declare function parseMcdHolders(wire: McdHoldersWire): McdHolders;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** The wire's decimal strings to amounts, each holding named after its venue, largest first. */
|
|
2
|
+
export function parseMcdHolders(wire) {
|
|
3
|
+
const pairs = new Map(wire.pairs.map((p) => [p.address.toLowerCase(), p]));
|
|
4
|
+
const pools = new Map(wire.farmPools.map((f) => [f.pid, f]));
|
|
5
|
+
const holders = wire.holders.map((h) => {
|
|
6
|
+
const holdings = [
|
|
7
|
+
{ kind: "wallet", venue: null, contract: null, mcd: BigInt(h.wallet) },
|
|
8
|
+
...h.lp.map((x) => {
|
|
9
|
+
const pair = pairs.get(x.pair.toLowerCase());
|
|
10
|
+
return { kind: "lp", venue: `Uniswap V2 MCD/${pair?.quoteSymbol ?? "?"}`, contract: x.pair, mcd: BigInt(x.mcd) };
|
|
11
|
+
}),
|
|
12
|
+
...h.staked.map((x) => {
|
|
13
|
+
const pool = pools.get(x.pid);
|
|
14
|
+
const lp = pool?.pair ? pairs.get(pool.pair.toLowerCase()) : undefined;
|
|
15
|
+
return { kind: "staked", venue: lp ? `StonkPress MCD/${lp.quoteSymbol} LP` : "StonkPress MCD", contract: wire.farm, mcd: BigInt(x.mcd) };
|
|
16
|
+
}),
|
|
17
|
+
]
|
|
18
|
+
.filter((x) => x.mcd > 0n)
|
|
19
|
+
.sort((a, b) => (b.mcd > a.mcd ? 1 : b.mcd < a.mcd ? -1 : 0));
|
|
20
|
+
return { address: h.address, name: h.name ?? null, role: h.role ?? null, x: h.x ?? null, total: holdings.reduce((s, x) => s + x.mcd, 0n), holdings };
|
|
21
|
+
});
|
|
22
|
+
holders.sort((a, b) => (b.total > a.total ? 1 : b.total < a.total ? -1 : 0));
|
|
23
|
+
return { supply: BigInt(wire.supply), addresses: wire.addresses, holders, block: wire.block, asOf: wire.asOf };
|
|
24
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import type { Address, Amount, Unix } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The project's Safes as meridian-frontend's `api/multisig` serves them: each Safe's owners,
|
|
4
|
+
* threshold and nonce, its queue and history as Safe's Transaction Service lists them (its own field
|
|
5
|
+
* names, numbers as decimal strings), each transaction described in words, and every name the
|
|
6
|
+
* describer knows. `parseMultisigs` revives the Safes; `parseDescribedMultisigs` revives a read with
|
|
7
|
+
* its descriptions.
|
|
8
|
+
*/
|
|
9
|
+
/** `0` CALL, `1` DELEGATECALL: the Safe runs the target's code as itself. */
|
|
10
|
+
export type SafeOperation = 0 | 1;
|
|
11
|
+
/** One owner's signature on a Safe transaction, as Safe's Transaction Service holds it. */
|
|
12
|
+
export interface SafeConfirmation {
|
|
13
|
+
owner: Address;
|
|
14
|
+
/** Hex; 65 bytes for an EOA, longer for a contract signature. Null when the service holds none (an on-chain approval). */
|
|
15
|
+
signature: Address | null;
|
|
16
|
+
/** The service's own label: `EOA`, `ETH_SIGN`, `APPROVED_HASH`, `CONTRACT_SIGNATURE`. */
|
|
17
|
+
signatureType: string | null;
|
|
18
|
+
submittedAt: Unix | null;
|
|
19
|
+
}
|
|
20
|
+
/** A call decoded by the Transaction Service off the target's verified ABI; the fallback for one the app has no ABI for. */
|
|
21
|
+
export interface SafeDecodedCall {
|
|
22
|
+
method: string;
|
|
23
|
+
parameters: SafeDecodedParam[];
|
|
24
|
+
}
|
|
25
|
+
export interface SafeDecodedParam {
|
|
26
|
+
name: string;
|
|
27
|
+
type: string;
|
|
28
|
+
/** As the service gives it: a string, or an array of them. */
|
|
29
|
+
value: unknown;
|
|
30
|
+
/** A `multiSend` batch's calls, each with its own decoding where the service has one. */
|
|
31
|
+
valueDecoded?: {
|
|
32
|
+
operation: number;
|
|
33
|
+
to: Address;
|
|
34
|
+
value: string;
|
|
35
|
+
data: Address | null;
|
|
36
|
+
dataDecoded: SafeDecodedCall | null;
|
|
37
|
+
}[] | null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A Safe multisig transaction: proposed (and maybe signed) off-chain through Safe's Transaction
|
|
41
|
+
* Service, or executed. Every field the Safe hashes is here, so the hash is rebuilt and checked
|
|
42
|
+
* before anything is signed or sent.
|
|
43
|
+
*/
|
|
44
|
+
export interface SafeTx {
|
|
45
|
+
safe: Address;
|
|
46
|
+
safeTxHash: Address;
|
|
47
|
+
nonce: number;
|
|
48
|
+
to: Address;
|
|
49
|
+
value: Amount;
|
|
50
|
+
data: Address | null;
|
|
51
|
+
operation: SafeOperation;
|
|
52
|
+
safeTxGas: Amount;
|
|
53
|
+
baseGas: Amount;
|
|
54
|
+
gasPrice: Amount;
|
|
55
|
+
gasToken: Address;
|
|
56
|
+
refundReceiver: Address;
|
|
57
|
+
proposer: Address | null;
|
|
58
|
+
submittedAt: Unix | null;
|
|
59
|
+
executed: boolean;
|
|
60
|
+
/** Null until executed. */
|
|
61
|
+
successful: boolean | null;
|
|
62
|
+
executedAt: Unix | null;
|
|
63
|
+
/** The execution's chain transaction. */
|
|
64
|
+
txHash: Address | null;
|
|
65
|
+
executor: Address | null;
|
|
66
|
+
confirmations: SafeConfirmation[];
|
|
67
|
+
decoded: SafeDecodedCall | null;
|
|
68
|
+
}
|
|
69
|
+
/** One watched Safe: its owners and threshold off the chain, its queue and history off the Transaction Service. */
|
|
70
|
+
export interface SafeAccount {
|
|
71
|
+
address: Address;
|
|
72
|
+
name: string;
|
|
73
|
+
/** `VERSION()`; null if the read failed. */
|
|
74
|
+
version: string | null;
|
|
75
|
+
owners: Address[];
|
|
76
|
+
threshold: number;
|
|
77
|
+
/** The next nonce the Safe will execute. */
|
|
78
|
+
nonce: number;
|
|
79
|
+
/** Not executed, nonce at or past the Safe's: what can still be signed or executed. Lowest nonce first. */
|
|
80
|
+
queued: SafeTx[];
|
|
81
|
+
/** Executed, newest first. */
|
|
82
|
+
history: SafeTx[];
|
|
83
|
+
/** Why the queue or history could not be read, when it could not; the chain figures may still be good. */
|
|
84
|
+
error: string | null;
|
|
85
|
+
}
|
|
86
|
+
/** The project's multisigs. */
|
|
87
|
+
export interface Multisigs {
|
|
88
|
+
chainId: number;
|
|
89
|
+
/** Safe{Wallet}'s prefix for the chain (`<shortName>:<address>`), for its links; null when unknown. */
|
|
90
|
+
shortName: string | null;
|
|
91
|
+
safes: SafeAccount[];
|
|
92
|
+
asOf: Unix;
|
|
93
|
+
}
|
|
94
|
+
export interface DescribedArg {
|
|
95
|
+
name: string;
|
|
96
|
+
value: string;
|
|
97
|
+
}
|
|
98
|
+
export interface DescribedCall {
|
|
99
|
+
/** One sentence. */
|
|
100
|
+
summary: string;
|
|
101
|
+
/** Who is called, named where known. */
|
|
102
|
+
target: string;
|
|
103
|
+
targetAddress: Address;
|
|
104
|
+
/** `transfer`, `multiSend`; null for a plain value transfer or an undecoded call. */
|
|
105
|
+
method: string | null;
|
|
106
|
+
args: DescribedArg[];
|
|
107
|
+
/** Every address the arguments name, in order, once each; the target is `targetAddress`. */
|
|
108
|
+
addresses: Address[];
|
|
109
|
+
operation: SafeOperation;
|
|
110
|
+
/** Native ETH sent with the call, base units. */
|
|
111
|
+
value: bigint;
|
|
112
|
+
/** Inner calls, for a batch or a multicall. */
|
|
113
|
+
calls: DescribedCall[];
|
|
114
|
+
/** Decoded off the app's ABIs or the service's; false means only the selector is known. */
|
|
115
|
+
decoded: boolean;
|
|
116
|
+
}
|
|
117
|
+
export interface TxDescription {
|
|
118
|
+
/** The headline: the one call's sentence, or a batch's count and first actions. */
|
|
119
|
+
title: string;
|
|
120
|
+
call: DescribedCall;
|
|
121
|
+
/** Things a signer should look at twice. */
|
|
122
|
+
warnings: string[];
|
|
123
|
+
}
|
|
124
|
+
/** A transaction as the Transaction Service lists it (`/api/v1/safes/{address}/multisig-transactions/`). */
|
|
125
|
+
export interface ServiceTxWire {
|
|
126
|
+
safe: string;
|
|
127
|
+
to: string;
|
|
128
|
+
value: string;
|
|
129
|
+
data: string | null;
|
|
130
|
+
operation: number;
|
|
131
|
+
safeTxGas: string | number;
|
|
132
|
+
baseGas: string | number;
|
|
133
|
+
gasPrice: string | number;
|
|
134
|
+
gasToken: string | null;
|
|
135
|
+
refundReceiver: string | null;
|
|
136
|
+
nonce: string | number;
|
|
137
|
+
safeTxHash: string;
|
|
138
|
+
proposer?: string | null;
|
|
139
|
+
submissionDate?: string | null;
|
|
140
|
+
executionDate?: string | null;
|
|
141
|
+
isExecuted: boolean;
|
|
142
|
+
isSuccessful?: boolean | null;
|
|
143
|
+
transactionHash?: string | null;
|
|
144
|
+
executor?: string | null;
|
|
145
|
+
confirmations?: {
|
|
146
|
+
owner: string;
|
|
147
|
+
signature?: string | null;
|
|
148
|
+
signatureType?: string | null;
|
|
149
|
+
submissionDate?: string | null;
|
|
150
|
+
}[] | null;
|
|
151
|
+
dataDecoded?: SafeDecodedCall | null;
|
|
152
|
+
}
|
|
153
|
+
export interface SafeWire {
|
|
154
|
+
address: string;
|
|
155
|
+
name: string;
|
|
156
|
+
version: string | null;
|
|
157
|
+
owners: string[];
|
|
158
|
+
threshold: number;
|
|
159
|
+
nonce: number;
|
|
160
|
+
queued: ServiceTxWire[];
|
|
161
|
+
history: ServiceTxWire[];
|
|
162
|
+
error: string | null;
|
|
163
|
+
}
|
|
164
|
+
export interface MultisigsWire {
|
|
165
|
+
chainId: number;
|
|
166
|
+
shortName: string | null;
|
|
167
|
+
safes: SafeWire[];
|
|
168
|
+
asOf: number;
|
|
169
|
+
}
|
|
170
|
+
/** A described call on the wire: its ETH as a decimal string. */
|
|
171
|
+
export type DescribedCallWire = Omit<DescribedCall, "value" | "calls"> & {
|
|
172
|
+
value: string;
|
|
173
|
+
calls: DescribedCallWire[];
|
|
174
|
+
};
|
|
175
|
+
export interface TxDescriptionWire extends Omit<TxDescription, "call"> {
|
|
176
|
+
call: DescribedCallWire;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* What a read answers: the Safes, each transaction in words (`describeSafeTx`, run in the function
|
|
180
|
+
* against the app's ABIs and names, so a client needs neither), and every name the describer knows.
|
|
181
|
+
*/
|
|
182
|
+
export interface DescribedMultisigsWire extends MultisigsWire {
|
|
183
|
+
/** Each transaction in words, by its lower-cased `safeTxHash`. */
|
|
184
|
+
descriptions: Record<string, TxDescriptionWire>;
|
|
185
|
+
/** Lower-cased address → name: the Safes, the signers, Meridian's contracts, the tokens, known counterparties. */
|
|
186
|
+
names: Record<string, string>;
|
|
187
|
+
/** The treasury's wallets outside the Safes. */
|
|
188
|
+
wallets: {
|
|
189
|
+
address: string;
|
|
190
|
+
name: string;
|
|
191
|
+
}[];
|
|
192
|
+
}
|
|
193
|
+
export declare function describedCallWire(call: DescribedCall): DescribedCallWire;
|
|
194
|
+
export declare function parseSafeTx(w: ServiceTxWire): SafeTx;
|
|
195
|
+
export declare function parseSafe(w: SafeWire): SafeAccount;
|
|
196
|
+
export declare function parseMultisigs(wire: MultisigsWire): Multisigs;
|
|
197
|
+
/** A transaction with its description. */
|
|
198
|
+
export interface DescribedSafeTx extends SafeTx {
|
|
199
|
+
description: TxDescription;
|
|
200
|
+
}
|
|
201
|
+
export interface DescribedSafeAccount extends Omit<SafeAccount, "queued" | "history"> {
|
|
202
|
+
queued: DescribedSafeTx[];
|
|
203
|
+
history: DescribedSafeTx[];
|
|
204
|
+
}
|
|
205
|
+
/** A read revived with every transaction's description, every name the describer knows and the treasury's other wallets. */
|
|
206
|
+
export interface DescribedMultisigs extends Omit<Multisigs, "safes"> {
|
|
207
|
+
safes: DescribedSafeAccount[];
|
|
208
|
+
/** Lower-cased address → a name. */
|
|
209
|
+
names: ReadonlyMap<string, string>;
|
|
210
|
+
wallets: {
|
|
211
|
+
address: Address;
|
|
212
|
+
name: string;
|
|
213
|
+
}[];
|
|
214
|
+
}
|
|
215
|
+
/** The Safes, each transaction with the description the function gave it. A transaction with none is an error, never drawn blind. */
|
|
216
|
+
export declare function parseDescribedMultisigs(wire: DescribedMultisigsWire): DescribedMultisigs;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export function describedCallWire(call) {
|
|
2
|
+
return { ...call, value: call.value.toString(), calls: call.calls.map(describedCallWire) };
|
|
3
|
+
}
|
|
4
|
+
const ZERO = "0x0000000000000000000000000000000000000000";
|
|
5
|
+
const unix = (date) => {
|
|
6
|
+
if (!date)
|
|
7
|
+
return null;
|
|
8
|
+
const ms = Date.parse(date);
|
|
9
|
+
return Number.isFinite(ms) ? Math.floor(ms / 1000) : null;
|
|
10
|
+
};
|
|
11
|
+
const big = (v) => (v == null || v === "" ? 0n : BigInt(v));
|
|
12
|
+
const hexOrNull = (v) => (v && v !== "0x" ? v : null);
|
|
13
|
+
export function parseSafeTx(w) {
|
|
14
|
+
const confirmations = (w.confirmations ?? []).map((c) => ({
|
|
15
|
+
owner: c.owner,
|
|
16
|
+
signature: hexOrNull(c.signature),
|
|
17
|
+
signatureType: c.signatureType ?? null,
|
|
18
|
+
submittedAt: unix(c.submissionDate),
|
|
19
|
+
}));
|
|
20
|
+
return {
|
|
21
|
+
safe: w.safe,
|
|
22
|
+
safeTxHash: w.safeTxHash,
|
|
23
|
+
nonce: Number(w.nonce),
|
|
24
|
+
to: w.to,
|
|
25
|
+
value: big(w.value),
|
|
26
|
+
data: hexOrNull(w.data),
|
|
27
|
+
operation: (w.operation === 1 ? 1 : 0),
|
|
28
|
+
safeTxGas: big(w.safeTxGas),
|
|
29
|
+
baseGas: big(w.baseGas),
|
|
30
|
+
gasPrice: big(w.gasPrice),
|
|
31
|
+
gasToken: (w.gasToken ?? ZERO),
|
|
32
|
+
refundReceiver: (w.refundReceiver ?? ZERO),
|
|
33
|
+
proposer: (w.proposer ?? null),
|
|
34
|
+
submittedAt: unix(w.submissionDate),
|
|
35
|
+
executed: w.isExecuted,
|
|
36
|
+
successful: w.isExecuted ? (w.isSuccessful ?? null) : null,
|
|
37
|
+
executedAt: unix(w.executionDate),
|
|
38
|
+
txHash: hexOrNull(w.transactionHash),
|
|
39
|
+
executor: (w.executor ?? null),
|
|
40
|
+
confirmations,
|
|
41
|
+
decoded: w.dataDecoded ?? null,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function parseSafe(w) {
|
|
45
|
+
return {
|
|
46
|
+
address: w.address,
|
|
47
|
+
name: w.name,
|
|
48
|
+
version: w.version,
|
|
49
|
+
owners: w.owners,
|
|
50
|
+
threshold: w.threshold,
|
|
51
|
+
nonce: w.nonce,
|
|
52
|
+
queued: w.queued.map(parseSafeTx).sort((a, b) => a.nonce - b.nonce || (a.submittedAt ?? 0) - (b.submittedAt ?? 0)),
|
|
53
|
+
history: w.history.map(parseSafeTx).sort((a, b) => b.nonce - a.nonce),
|
|
54
|
+
error: w.error,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function parseMultisigs(wire) {
|
|
58
|
+
return { chainId: wire.chainId, shortName: wire.shortName, safes: wire.safes.map(parseSafe), asOf: wire.asOf };
|
|
59
|
+
}
|
|
60
|
+
function describedCall(wire) {
|
|
61
|
+
return { ...wire, value: BigInt(wire.value), calls: wire.calls.map(describedCall) };
|
|
62
|
+
}
|
|
63
|
+
/** The Safes, each transaction with the description the function gave it. A transaction with none is an error, never drawn blind. */
|
|
64
|
+
export function parseDescribedMultisigs(wire) {
|
|
65
|
+
const described = (tx) => {
|
|
66
|
+
const d = wire.descriptions[tx.safeTxHash.toLowerCase()];
|
|
67
|
+
if (!d)
|
|
68
|
+
throw new Error(`The multisig read sent no description for ${tx.safeTxHash}`);
|
|
69
|
+
return { ...tx, description: { ...d, call: describedCall(d.call) } };
|
|
70
|
+
};
|
|
71
|
+
const { safes, ...rest } = parseMultisigs(wire);
|
|
72
|
+
return {
|
|
73
|
+
...rest,
|
|
74
|
+
safes: safes.map((s) => ({ ...s, queued: s.queued.map(described), history: s.history.map(described) })),
|
|
75
|
+
names: new Map(Object.entries(wire.names)),
|
|
76
|
+
wallets: wire.wallets.map((w) => ({ address: w.address, name: w.name })),
|
|
77
|
+
};
|
|
78
|
+
}
|