jalin-sdk 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/LICENSE +21 -0
- package/README.md +81 -0
- package/dist/anonymity.d.ts +99 -0
- package/dist/anonymity.d.ts.map +1 -0
- package/dist/anonymity.js +166 -0
- package/dist/anonymity.js.map +1 -0
- package/dist/crowd.d.ts +53 -0
- package/dist/crowd.d.ts.map +1 -0
- package/dist/crowd.js +50 -0
- package/dist/crowd.js.map +1 -0
- package/dist/disclosure.d.ts +22 -0
- package/dist/disclosure.d.ts.map +1 -0
- package/dist/disclosure.js +98 -0
- package/dist/disclosure.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/plan.d.ts +88 -0
- package/dist/plan.d.ts.map +1 -0
- package/dist/plan.js +161 -0
- package/dist/plan.js.map +1 -0
- package/dist/receipt.d.ts +77 -0
- package/dist/receipt.d.ts.map +1 -0
- package/dist/receipt.js +164 -0
- package/dist/receipt.js.map +1 -0
- package/dist/recipes.d.ts +60 -0
- package/dist/recipes.d.ts.map +1 -0
- package/dist/recipes.js +57 -0
- package/dist/recipes.js.map +1 -0
- package/dist/rpc-response.d.ts +22 -0
- package/dist/rpc-response.d.ts.map +1 -0
- package/dist/rpc-response.js +44 -0
- package/dist/rpc-response.js.map +1 -0
- package/dist/shadow.d.ts +72 -0
- package/dist/shadow.d.ts.map +1 -0
- package/dist/shadow.js +61 -0
- package/dist/shadow.js.map +1 -0
- package/dist/share.d.ts +46 -0
- package/dist/share.d.ts.map +1 -0
- package/dist/share.js +99 -0
- package/dist/share.js.map +1 -0
- package/dist/subaccounts.d.ts +63 -0
- package/dist/subaccounts.d.ts.map +1 -0
- package/dist/subaccounts.js +83 -0
- package/dist/subaccounts.js.map +1 -0
- package/dist/wallet.d.ts +103 -0
- package/dist/wallet.d.ts.map +1 -0
- package/dist/wallet.js +135 -0
- package/dist/wallet.js.map +1 -0
- package/package.json +36 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pugar Huda
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# jalin-sdk
|
|
2
|
+
|
|
3
|
+
Plan encoding and sub-account portfolio helpers for [Jalin](https://github.com/PugarHuda/jalin),
|
|
4
|
+
a programmable execution router for the STRK20 shielded pool on Starknet.
|
|
5
|
+
|
|
6
|
+
STRK20 allows one external call per pool transaction, and every token balance
|
|
7
|
+
must end at exactly zero. A private DeFi action is therefore only as expressive
|
|
8
|
+
as the single helper contract it calls — which is why almost everything built on
|
|
9
|
+
STRK20 today is a payment app. Jalin is one helper that takes a **plan** instead
|
|
10
|
+
of fixed parameters: a bounded list of steps, each naming a target, a selector,
|
|
11
|
+
calldata and the approvals it needs, all executed inside a single
|
|
12
|
+
`privacy_invoke`.
|
|
13
|
+
|
|
14
|
+
This package is the client half: it builds those plans, validates them against
|
|
15
|
+
the router's limits before you spend gas, and encodes them as calldata.
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
npm install jalin-sdk
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
> **Not on the registry yet.** Until this package is published, `jalin-sdk` is an
|
|
22
|
+
> unclaimed name, and installing it would fetch whatever anyone else has put
|
|
23
|
+
> there. Use it from the repository — `npm install github:PugarHuda/jalin` — or
|
|
24
|
+
> wait for the published version.
|
|
25
|
+
|
|
26
|
+
## Building a plan
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
import { PlanBuilder, callStep, openNote } from 'jalin-sdk'
|
|
30
|
+
|
|
31
|
+
const plan = PlanBuilder.create()
|
|
32
|
+
.call(
|
|
33
|
+
callStep({
|
|
34
|
+
target: VAULT,
|
|
35
|
+
selector: DEPOSIT_SELECTOR,
|
|
36
|
+
spend: { token: STRK, amount: 10n ** 18n },
|
|
37
|
+
calldata: [10n ** 18n, 0n, ROUTER],
|
|
38
|
+
}),
|
|
39
|
+
)
|
|
40
|
+
// Credit the shares back into the first open note, but only above a floor.
|
|
41
|
+
.creditTo(SHARES, openNote(0), 9n * 10n ** 17n)
|
|
42
|
+
.build()
|
|
43
|
+
|
|
44
|
+
const calldata = PlanBuilder.create().call(step).creditTo(SHARES, openNote(0)).encode()
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`build()` runs `validatePlan`, which rejects a plan the router would reject:
|
|
48
|
+
too many steps, oversized calldata, an approval the step never spends, or a
|
|
49
|
+
token moved by a step and credited to nobody. `openNote(n)` is the placeholder
|
|
50
|
+
the wallet resolves at submit time, so the note id never has to be known here.
|
|
51
|
+
|
|
52
|
+
## What else is in it
|
|
53
|
+
|
|
54
|
+
| | |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `plan.ts` | `PlanBuilder`, `validatePlan`, `encodePlan`, `unclaimedTokens`, limits |
|
|
57
|
+
| `recipes.ts` | `callStep` for any ABI, `depositStep` for ERC-4626, `oneWay` |
|
|
58
|
+
| `receipt.ts` | reads a transaction receipt and says whether it really touched the pool |
|
|
59
|
+
| `subaccounts.ts` | sub-account portfolios, and when one has no anonymity left |
|
|
60
|
+
| `shadow.ts` | the Wallet API action for `shadow_account_invoke` |
|
|
61
|
+
| `crowd.ts`, `anonymity.ts` | the anonymity set a plan actually lands in |
|
|
62
|
+
| `disclosure.ts`, `share.ts` | selective disclosure of a transaction you made |
|
|
63
|
+
|
|
64
|
+
Every export is typed, and the package ships its own declarations.
|
|
65
|
+
|
|
66
|
+
## Verifying a submission
|
|
67
|
+
|
|
68
|
+
The router's deployment also answers for anyone, not just for Jalin:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
https://jalin-five.vercel.app/api/manifest?owner=<owner>&repo=<repo>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
It reads that repository's `strk20.json`, checks each hash against Starknet
|
|
75
|
+
mainnet — exists, succeeded, touched the pool, ran through your own contract —
|
|
76
|
+
and reports how many of them count.
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT. The router contract, the Cairo tests and the full design notes are in the
|
|
81
|
+
[repository](https://github.com/PugarHuda/jalin).
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { PoolEvent } from './crowd.ts';
|
|
2
|
+
/**
|
|
3
|
+
* How big is the crowd you actually hide in?
|
|
4
|
+
*
|
|
5
|
+
* Not the pool's headcount. A shielded pool does not mix everything with
|
|
6
|
+
* everything: an observer watching the public deposit leg sees the asset, the
|
|
7
|
+
* order of magnitude, and roughly when. Two deposits only hide each other if
|
|
8
|
+
* they agree on all three. So the set that matters is the cell, not the pool,
|
|
9
|
+
* and on this pool the median cell holds one person.
|
|
10
|
+
*
|
|
11
|
+
* Headcount also flatters a cell where one address carries nearly all the
|
|
12
|
+
* volume. Four addresses, one holding 97% of the flow, is not a crowd of four.
|
|
13
|
+
* The effective set is the perplexity of the flow distribution, 2^H — four
|
|
14
|
+
* equal participants give exactly 4, and the lopsided four give about 1.2.
|
|
15
|
+
*/
|
|
16
|
+
/** ~6 hours at Starknet mainnet's measured 1.68s per block. */
|
|
17
|
+
export declare const CELL_BLOCKS = 12888;
|
|
18
|
+
export interface Cell {
|
|
19
|
+
asset: string;
|
|
20
|
+
/** Digits in the raw amount. Decimals-agnostic, and the asset is in the key. */
|
|
21
|
+
magnitude: number;
|
|
22
|
+
/** Which CELL_BLOCKS-wide slot of the chain. */
|
|
23
|
+
slot: number;
|
|
24
|
+
/** Distinct depositors in the cell. */
|
|
25
|
+
headcount: number;
|
|
26
|
+
/** Perplexity of the flow distribution: the crowd this cell really is. */
|
|
27
|
+
effectiveSet: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* 2^H over each address's share of the cell's flow. One participant gives 1,
|
|
31
|
+
* n equal participants give n, and an unequal split gives something between.
|
|
32
|
+
*/
|
|
33
|
+
export declare function effectiveSet(flows: Iterable<bigint>): number;
|
|
34
|
+
/** Every cell the given deposits fall into. */
|
|
35
|
+
export declare function measureCells(events: PoolEvent[], cellBlocks?: number): Cell[];
|
|
36
|
+
export interface CellSummary {
|
|
37
|
+
cells: number;
|
|
38
|
+
/** The middle cell's effective set. The number a typical deposit gets. */
|
|
39
|
+
medianEffectiveSet: number;
|
|
40
|
+
/** Share of cells holding exactly one depositor, 0 to 1. */
|
|
41
|
+
aloneShare: number;
|
|
42
|
+
largestHeadcount: number;
|
|
43
|
+
largestEffectiveSet: number;
|
|
44
|
+
}
|
|
45
|
+
export declare function summariseCells(cells: Cell[]): CellSummary;
|
|
46
|
+
export interface Prospect {
|
|
47
|
+
/** Depositors already in the cell this deposit would join. */
|
|
48
|
+
headcount: number;
|
|
49
|
+
/** The cell's effective set as it stands, before this deposit. */
|
|
50
|
+
effectiveSet: number;
|
|
51
|
+
/** The effective set once this deposit is added. */
|
|
52
|
+
effectiveSetAfter: number;
|
|
53
|
+
/**
|
|
54
|
+
* Blocks until this cell closes and a new, empty one opens.
|
|
55
|
+
*
|
|
56
|
+
* The answer above is about the cell open right now. Sign after this many
|
|
57
|
+
* blocks and the deposit lands in the next one instead, which starts empty -
|
|
58
|
+
* so a small number here means the crowd being quoted is about to expire.
|
|
59
|
+
*/
|
|
60
|
+
blocksLeftInCell: number;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* What a specific deposit would be joining.
|
|
64
|
+
*
|
|
65
|
+
* This is the number worth showing someone before they sign, because it is the
|
|
66
|
+
* one that describes their transaction rather than the pool's history. An
|
|
67
|
+
* `effectiveSetAfter` of 1 means the deposit stands alone in its cell and the
|
|
68
|
+
* public leg identifies it completely.
|
|
69
|
+
*/
|
|
70
|
+
export declare function prospectFor(events: PoolEvent[], intent: {
|
|
71
|
+
asset: string;
|
|
72
|
+
amount: bigint;
|
|
73
|
+
atBlock: number;
|
|
74
|
+
}, cellBlocks?: number): Prospect;
|
|
75
|
+
export interface Period {
|
|
76
|
+
/** The CELL_BLOCKS-wide slot these cells belong to. */
|
|
77
|
+
slot: number;
|
|
78
|
+
/** First block of the slot, so a reader can find it on a block explorer. */
|
|
79
|
+
fromBlock: number;
|
|
80
|
+
/** Cells that opened in this slot. */
|
|
81
|
+
cells: number;
|
|
82
|
+
/** The middle cell's effective set — what a typical deposit got that day. */
|
|
83
|
+
medianEffectiveSet: number;
|
|
84
|
+
/** The best any single deposit could have done. */
|
|
85
|
+
bestEffectiveSet: number;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The same measurement, over time.
|
|
89
|
+
*
|
|
90
|
+
* A single median says the pool is thin. It cannot say whether that is
|
|
91
|
+
* improving, and "1.00" printed forever reads as a broken gauge rather than a
|
|
92
|
+
* finding. Grouped by slot, the shape of the thing becomes visible: whether a
|
|
93
|
+
* crowd ever forms, and how long it lasts when it does.
|
|
94
|
+
*
|
|
95
|
+
* Slots with no deposits are absent rather than zero. A day nobody shielded is
|
|
96
|
+
* not a day the anonymity set was zero; it is a day the question was not asked.
|
|
97
|
+
*/
|
|
98
|
+
export declare function measurePeriods(cells: Cell[], cellBlocks?: number): Period[];
|
|
99
|
+
//# sourceMappingURL=anonymity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anonymity.d.ts","sourceRoot":"","sources":["../src/anonymity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAE3C;;;;;;;;;;;;;GAaG;AAEH,+DAA+D;AAC/D,eAAO,MAAM,WAAW,QAAS,CAAA;AAEjC,MAAM,WAAW,IAAI;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,gFAAgF;IAChF,SAAS,EAAE,MAAM,CAAA;IACjB,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAA;IACZ,uCAAuC;IACvC,SAAS,EAAE,MAAM,CAAA;IACjB,0EAA0E;IAC1E,YAAY,EAAE,MAAM,CAAA;CACrB;AAsCD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,MAAM,CAW5D;AAED,+CAA+C;AAC/C,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,UAAU,SAAc,GAAG,IAAI,EAAE,CAoBlF;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAA;IACb,0EAA0E;IAC1E,kBAAkB,EAAE,MAAM,CAAA;IAC1B,4DAA4D;IAC5D,UAAU,EAAE,MAAM,CAAA;IAClB,gBAAgB,EAAE,MAAM,CAAA;IACxB,mBAAmB,EAAE,MAAM,CAAA;CAC5B;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,WAAW,CAmBzD;AAED,MAAM,WAAW,QAAQ;IACvB,8DAA8D;IAC9D,SAAS,EAAE,MAAM,CAAA;IACjB,kEAAkE;IAClE,YAAY,EAAE,MAAM,CAAA;IACpB,oDAAoD;IACpD,iBAAiB,EAAE,MAAM,CAAA;IACzB;;;;;;OAMG;IACH,gBAAgB,EAAE,MAAM,CAAA;CACzB;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,SAAS,EAAE,EACnB,MAAM,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EAC1D,UAAU,SAAc,GACvB,QAAQ,CAgCV;AAED,MAAM,WAAW,MAAM;IACrB,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAA;IACZ,4EAA4E;IAC5E,SAAS,EAAE,MAAM,CAAA;IACjB,sCAAsC;IACtC,KAAK,EAAE,MAAM,CAAA;IACb,6EAA6E;IAC7E,kBAAkB,EAAE,MAAM,CAAA;IAC1B,mDAAmD;IACnD,gBAAgB,EAAE,MAAM,CAAA;CACzB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,UAAU,SAAc,GAAG,MAAM,EAAE,CAoBhF"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How big is the crowd you actually hide in?
|
|
3
|
+
*
|
|
4
|
+
* Not the pool's headcount. A shielded pool does not mix everything with
|
|
5
|
+
* everything: an observer watching the public deposit leg sees the asset, the
|
|
6
|
+
* order of magnitude, and roughly when. Two deposits only hide each other if
|
|
7
|
+
* they agree on all three. So the set that matters is the cell, not the pool,
|
|
8
|
+
* and on this pool the median cell holds one person.
|
|
9
|
+
*
|
|
10
|
+
* Headcount also flatters a cell where one address carries nearly all the
|
|
11
|
+
* volume. Four addresses, one holding 97% of the flow, is not a crowd of four.
|
|
12
|
+
* The effective set is the perplexity of the flow distribution, 2^H — four
|
|
13
|
+
* equal participants give exactly 4, and the lopsided four give about 1.2.
|
|
14
|
+
*/
|
|
15
|
+
/** ~6 hours at Starknet mainnet's measured 1.68s per block. */
|
|
16
|
+
export const CELL_BLOCKS = 12_888;
|
|
17
|
+
/** `keys[1]` is the depositor, `keys[2]` the asset, `data[0]` the amount. */
|
|
18
|
+
function read(events) {
|
|
19
|
+
const deposits = [];
|
|
20
|
+
for (const event of events) {
|
|
21
|
+
if (!event.keys || event.keys.length < 3)
|
|
22
|
+
continue;
|
|
23
|
+
const amount = BigInt(event.data?.[0] ?? '0x0');
|
|
24
|
+
if (amount === 0n)
|
|
25
|
+
continue;
|
|
26
|
+
deposits.push({
|
|
27
|
+
// By value, so a padded address is the same address.
|
|
28
|
+
who: BigInt(event.keys[1]).toString(),
|
|
29
|
+
asset: BigInt(event.keys[2]).toString(),
|
|
30
|
+
amount,
|
|
31
|
+
block: event.block_number ?? 0,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return deposits;
|
|
35
|
+
}
|
|
36
|
+
function magnitudeOf(amount) {
|
|
37
|
+
return amount.toString().length;
|
|
38
|
+
}
|
|
39
|
+
function key(asset, magnitude, slot) {
|
|
40
|
+
return `${asset}|${magnitude}|${slot}`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 2^H over each address's share of the cell's flow. One participant gives 1,
|
|
44
|
+
* n equal participants give n, and an unequal split gives something between.
|
|
45
|
+
*/
|
|
46
|
+
export function effectiveSet(flows) {
|
|
47
|
+
const shares = [...flows];
|
|
48
|
+
const total = shares.reduce((sum, value) => sum + value, 0n);
|
|
49
|
+
if (total === 0n)
|
|
50
|
+
return 0;
|
|
51
|
+
let entropy = 0;
|
|
52
|
+
for (const value of shares) {
|
|
53
|
+
const share = Number(value) / Number(total);
|
|
54
|
+
if (share > 0)
|
|
55
|
+
entropy -= share * Math.log2(share);
|
|
56
|
+
}
|
|
57
|
+
return 2 ** entropy;
|
|
58
|
+
}
|
|
59
|
+
/** Every cell the given deposits fall into. */
|
|
60
|
+
export function measureCells(events, cellBlocks = CELL_BLOCKS) {
|
|
61
|
+
const grouped = new Map();
|
|
62
|
+
const facts = new Map();
|
|
63
|
+
for (const deposit of read(events)) {
|
|
64
|
+
const magnitude = magnitudeOf(deposit.amount);
|
|
65
|
+
const slot = Math.floor(deposit.block / cellBlocks);
|
|
66
|
+
const id = key(deposit.asset, magnitude, slot);
|
|
67
|
+
const cell = grouped.get(id) ?? new Map();
|
|
68
|
+
cell.set(deposit.who, (cell.get(deposit.who) ?? 0n) + deposit.amount);
|
|
69
|
+
grouped.set(id, cell);
|
|
70
|
+
facts.set(id, { asset: deposit.asset, magnitude, slot });
|
|
71
|
+
}
|
|
72
|
+
return [...grouped].map(([id, cell]) => ({
|
|
73
|
+
...facts.get(id),
|
|
74
|
+
headcount: cell.size,
|
|
75
|
+
effectiveSet: effectiveSet(cell.values()),
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
export function summariseCells(cells) {
|
|
79
|
+
if (cells.length === 0) {
|
|
80
|
+
return {
|
|
81
|
+
cells: 0,
|
|
82
|
+
medianEffectiveSet: 0,
|
|
83
|
+
aloneShare: 0,
|
|
84
|
+
largestHeadcount: 0,
|
|
85
|
+
largestEffectiveSet: 0,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const sorted = [...cells].sort((a, b) => a.effectiveSet - b.effectiveSet);
|
|
89
|
+
return {
|
|
90
|
+
cells: cells.length,
|
|
91
|
+
medianEffectiveSet: sorted[Math.floor(sorted.length / 2)].effectiveSet,
|
|
92
|
+
aloneShare: cells.filter((cell) => cell.headcount === 1).length / cells.length,
|
|
93
|
+
largestHeadcount: Math.max(...cells.map((cell) => cell.headcount)),
|
|
94
|
+
largestEffectiveSet: Math.max(...cells.map((cell) => cell.effectiveSet)),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* What a specific deposit would be joining.
|
|
99
|
+
*
|
|
100
|
+
* This is the number worth showing someone before they sign, because it is the
|
|
101
|
+
* one that describes their transaction rather than the pool's history. An
|
|
102
|
+
* `effectiveSetAfter` of 1 means the deposit stands alone in its cell and the
|
|
103
|
+
* public leg identifies it completely.
|
|
104
|
+
*/
|
|
105
|
+
export function prospectFor(events, intent, cellBlocks = CELL_BLOCKS) {
|
|
106
|
+
const blocksLeftInCell = cellBlocks - (intent.atBlock % cellBlocks);
|
|
107
|
+
if (intent.amount <= 0n) {
|
|
108
|
+
return { headcount: 0, effectiveSet: 0, effectiveSetAfter: 0, blocksLeftInCell };
|
|
109
|
+
}
|
|
110
|
+
const asset = BigInt(intent.asset).toString();
|
|
111
|
+
const magnitude = magnitudeOf(intent.amount);
|
|
112
|
+
const slot = Math.floor(intent.atBlock / cellBlocks);
|
|
113
|
+
const flows = new Map();
|
|
114
|
+
for (const deposit of read(events)) {
|
|
115
|
+
if (deposit.asset !== asset)
|
|
116
|
+
continue;
|
|
117
|
+
if (magnitudeOf(deposit.amount) !== magnitude)
|
|
118
|
+
continue;
|
|
119
|
+
if (Math.floor(deposit.block / cellBlocks) !== slot)
|
|
120
|
+
continue;
|
|
121
|
+
flows.set(deposit.who, (flows.get(deposit.who) ?? 0n) + deposit.amount);
|
|
122
|
+
}
|
|
123
|
+
const before = effectiveSet(flows.values());
|
|
124
|
+
// The newcomer is somebody not already here — the honest assumption, and the
|
|
125
|
+
// one that does not let an address inflate its own crowd by depositing twice.
|
|
126
|
+
const after = new Map(flows);
|
|
127
|
+
after.set('newcomer', intent.amount);
|
|
128
|
+
return {
|
|
129
|
+
headcount: flows.size,
|
|
130
|
+
effectiveSet: before,
|
|
131
|
+
effectiveSetAfter: effectiveSet(after.values()),
|
|
132
|
+
blocksLeftInCell,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The same measurement, over time.
|
|
137
|
+
*
|
|
138
|
+
* A single median says the pool is thin. It cannot say whether that is
|
|
139
|
+
* improving, and "1.00" printed forever reads as a broken gauge rather than a
|
|
140
|
+
* finding. Grouped by slot, the shape of the thing becomes visible: whether a
|
|
141
|
+
* crowd ever forms, and how long it lasts when it does.
|
|
142
|
+
*
|
|
143
|
+
* Slots with no deposits are absent rather than zero. A day nobody shielded is
|
|
144
|
+
* not a day the anonymity set was zero; it is a day the question was not asked.
|
|
145
|
+
*/
|
|
146
|
+
export function measurePeriods(cells, cellBlocks = CELL_BLOCKS) {
|
|
147
|
+
const bySlot = new Map();
|
|
148
|
+
for (const cell of cells) {
|
|
149
|
+
const group = bySlot.get(cell.slot) ?? [];
|
|
150
|
+
group.push(cell);
|
|
151
|
+
bySlot.set(cell.slot, group);
|
|
152
|
+
}
|
|
153
|
+
return [...bySlot]
|
|
154
|
+
.sort(([a], [b]) => a - b)
|
|
155
|
+
.map(([slot, group]) => {
|
|
156
|
+
const sorted = [...group].sort((a, b) => a.effectiveSet - b.effectiveSet);
|
|
157
|
+
return {
|
|
158
|
+
slot,
|
|
159
|
+
fromBlock: slot * cellBlocks,
|
|
160
|
+
cells: group.length,
|
|
161
|
+
medianEffectiveSet: sorted[Math.floor(sorted.length / 2)].effectiveSet,
|
|
162
|
+
bestEffectiveSet: Math.max(...group.map((cell) => cell.effectiveSet)),
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=anonymity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anonymity.js","sourceRoot":"","sources":["../src/anonymity.ts"],"names":[],"mappings":"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"}
|
package/dist/crowd.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How large the anonymity set actually is.
|
|
3
|
+
*
|
|
4
|
+
* Every privacy tool says your anonymity depends on the size of the crowd and
|
|
5
|
+
* then leaves you to guess the number. Shielding is public, so it is countable.
|
|
6
|
+
*
|
|
7
|
+
* Count arrivals, not exits. Two kinds of withdrawal have nothing to do with
|
|
8
|
+
* anyone leaving the pool:
|
|
9
|
+
*
|
|
10
|
+
* - **the fee leg.** Every pool transaction pays the fee collector, and that
|
|
11
|
+
* payment emits a `Withdrawal` naming it. Reported by Shoal on
|
|
12
|
+
* starkience/strk20-hackathon#121, whose first pass counted 334 "atomic
|
|
13
|
+
* shield and unshield" transactions that were every one a fee payment.
|
|
14
|
+
* - **the gas leg.** On STRK20 mainnet most withdrawals name the paymaster that
|
|
15
|
+
* relays gasless transactions. Filtering the fee collector alone removes
|
|
16
|
+
* nothing, which is how this one turns up.
|
|
17
|
+
*
|
|
18
|
+
* A withdrawal means a person left only if its destination is a person. A
|
|
19
|
+
* deposit's `keys[1]` is unambiguously the depositor, so that is what this
|
|
20
|
+
* counts.
|
|
21
|
+
*/
|
|
22
|
+
/** The minimum shape of a Starknet event this needs. */
|
|
23
|
+
export interface PoolEvent {
|
|
24
|
+
keys: string[];
|
|
25
|
+
/** `data[0]` of a Deposit is the amount. Absent on events read keys-only. */
|
|
26
|
+
data?: string[];
|
|
27
|
+
block_number?: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Addresses that are infrastructure rather than people. Anything here is
|
|
31
|
+
* excluded from the count.
|
|
32
|
+
*/
|
|
33
|
+
export interface Infrastructure {
|
|
34
|
+
/** Relays gasless pool transactions. Its deposits are not a person arriving. */
|
|
35
|
+
paymaster?: string;
|
|
36
|
+
/** Paid on every pool transaction. */
|
|
37
|
+
feeCollector?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface Crowd {
|
|
40
|
+
/** Distinct depositors, excluding known infrastructure. */
|
|
41
|
+
depositors: number;
|
|
42
|
+
/** Deposit events seen, including any excluded ones. */
|
|
43
|
+
deposits: number;
|
|
44
|
+
/** Addresses dropped as infrastructure, so the exclusion is visible. */
|
|
45
|
+
excluded: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* `keys[0]` is the event selector and `keys[1]` is the depositor, so an event
|
|
49
|
+
* with fewer than two keys is not a deposit this can read and is skipped rather
|
|
50
|
+
* than counted as an anonymous one.
|
|
51
|
+
*/
|
|
52
|
+
export declare function countDepositors(events: PoolEvent[], infra?: Infrastructure): Crowd;
|
|
53
|
+
//# sourceMappingURL=crowd.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crowd.d.ts","sourceRoot":"","sources":["../src/crowd.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,wDAAwD;AACxD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,EAAE,CAAA;IACd,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;IACf,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,gFAAgF;IAChF,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,sCAAsC;IACtC,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,KAAK;IACpB,2DAA2D;IAC3D,UAAU,EAAE,MAAM,CAAA;IAClB,wDAAwD;IACxD,QAAQ,EAAE,MAAM,CAAA;IAChB,wEAAwE;IACxE,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,KAAK,GAAE,cAAmB,GAAG,KAAK,CAyBtF"}
|
package/dist/crowd.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How large the anonymity set actually is.
|
|
3
|
+
*
|
|
4
|
+
* Every privacy tool says your anonymity depends on the size of the crowd and
|
|
5
|
+
* then leaves you to guess the number. Shielding is public, so it is countable.
|
|
6
|
+
*
|
|
7
|
+
* Count arrivals, not exits. Two kinds of withdrawal have nothing to do with
|
|
8
|
+
* anyone leaving the pool:
|
|
9
|
+
*
|
|
10
|
+
* - **the fee leg.** Every pool transaction pays the fee collector, and that
|
|
11
|
+
* payment emits a `Withdrawal` naming it. Reported by Shoal on
|
|
12
|
+
* starkience/strk20-hackathon#121, whose first pass counted 334 "atomic
|
|
13
|
+
* shield and unshield" transactions that were every one a fee payment.
|
|
14
|
+
* - **the gas leg.** On STRK20 mainnet most withdrawals name the paymaster that
|
|
15
|
+
* relays gasless transactions. Filtering the fee collector alone removes
|
|
16
|
+
* nothing, which is how this one turns up.
|
|
17
|
+
*
|
|
18
|
+
* A withdrawal means a person left only if its destination is a person. A
|
|
19
|
+
* deposit's `keys[1]` is unambiguously the depositor, so that is what this
|
|
20
|
+
* counts.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* `keys[0]` is the event selector and `keys[1]` is the depositor, so an event
|
|
24
|
+
* with fewer than two keys is not a deposit this can read and is skipped rather
|
|
25
|
+
* than counted as an anonymous one.
|
|
26
|
+
*/
|
|
27
|
+
export function countDepositors(events, infra = {}) {
|
|
28
|
+
const ignore = new Set([infra.paymaster, infra.feeCollector].filter(Boolean).map((a) => BigInt(a).toString()));
|
|
29
|
+
const depositors = new Set();
|
|
30
|
+
let excluded = 0;
|
|
31
|
+
for (const event of events) {
|
|
32
|
+
const who = event.keys?.[1];
|
|
33
|
+
if (!who)
|
|
34
|
+
continue;
|
|
35
|
+
let normalised;
|
|
36
|
+
try {
|
|
37
|
+
normalised = BigInt(who).toString();
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (ignore.has(normalised)) {
|
|
43
|
+
excluded += 1;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
depositors.add(normalised);
|
|
47
|
+
}
|
|
48
|
+
return { depositors: depositors.size, deposits: events.length, excluded };
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=crowd.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crowd.js","sourceRoot":"","sources":["../src/crowd.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AA8BH;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,MAAmB,EAAE,QAAwB,EAAE;IAC7E,MAAM,MAAM,GAAG,IAAI,GAAG,CACpB,CAAC,KAAK,CAAC,SAAS,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,CACxF,CAAA;IAED,MAAM,UAAU,GAAG,IAAI,GAAG,EAAU,CAAA;IACpC,IAAI,QAAQ,GAAG,CAAC,CAAA;IAEhB,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAA;QAC3B,IAAI,CAAC,GAAG;YAAE,SAAQ;QAClB,IAAI,UAAkB,CAAA;QACtB,IAAI,CAAC;YACH,UAAU,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAA;QACrC,CAAC;QAAC,MAAM,CAAC;YACP,SAAQ;QACV,CAAC;QACD,IAAI,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;YAC3B,QAAQ,IAAI,CAAC,CAAA;YACb,SAAQ;QACV,CAAC;QACD,UAAU,CAAC,GAAG,CAAC,UAAU,CAAC,CAAA;IAC5B,CAAC;IAED,OAAO,EAAE,UAAU,EAAE,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAA;AAC3E,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a plan reveals.
|
|
3
|
+
*
|
|
4
|
+
* The pool hides which note paid for a plan. It does not hide that the plan
|
|
5
|
+
* happened, and it does not hide a single amount. Most privacy mistakes are made
|
|
6
|
+
* by people who believed they were private, so this module says out loud what an
|
|
7
|
+
* observer sees before the plan is signed rather than after.
|
|
8
|
+
*
|
|
9
|
+
* Everything here is derived from the plan alone. It cannot see the anonymity set,
|
|
10
|
+
* it cannot see your other activity, and it does not pretend to score you.
|
|
11
|
+
*/
|
|
12
|
+
import { type Plan } from './plan.ts';
|
|
13
|
+
export interface Disclosure {
|
|
14
|
+
/** What an observer cannot learn from this transaction. */
|
|
15
|
+
hidden: string[];
|
|
16
|
+
/** What an observer can read directly off the chain. */
|
|
17
|
+
visible: string[];
|
|
18
|
+
/** Choices in this particular plan that give privacy back. */
|
|
19
|
+
warnings: string[];
|
|
20
|
+
}
|
|
21
|
+
export declare function describeDisclosure(plan: Plan): Disclosure;
|
|
22
|
+
//# sourceMappingURL=disclosure.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"disclosure.d.ts","sourceRoot":"","sources":["../src/disclosure.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAmB,KAAK,IAAI,EAAE,MAAM,WAAW,CAAA;AAEtD,MAAM,WAAW,UAAU;IACzB,2DAA2D;IAC3D,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,wDAAwD;IACxD,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,8DAA8D;IAC9D,QAAQ,EAAE,MAAM,EAAE,CAAA;CACnB;AAqBD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,IAAI,GAAG,UAAU,CAwFzD"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a plan reveals.
|
|
3
|
+
*
|
|
4
|
+
* The pool hides which note paid for a plan. It does not hide that the plan
|
|
5
|
+
* happened, and it does not hide a single amount. Most privacy mistakes are made
|
|
6
|
+
* by people who believed they were private, so this module says out loud what an
|
|
7
|
+
* observer sees before the plan is signed rather than after.
|
|
8
|
+
*
|
|
9
|
+
* Everything here is derived from the plan alone. It cannot see the anonymity set,
|
|
10
|
+
* it cannot see your other activity, and it does not pretend to score you.
|
|
11
|
+
*/
|
|
12
|
+
import { unclaimedTokens } from "./plan.js";
|
|
13
|
+
/**
|
|
14
|
+
* A felt too large to be an amount is almost certainly an address.
|
|
15
|
+
*
|
|
16
|
+
* Starknet addresses run to 2^251; token amounts, even eighteen decimals of a
|
|
17
|
+
* whale's position, do not reach 2^160. There is no ambiguity in practice, and
|
|
18
|
+
* the cost of a false positive here is one sentence of caution.
|
|
19
|
+
*/
|
|
20
|
+
const ADDRESS_SHAPED = 2n ** 160n;
|
|
21
|
+
/** Amounts that look chosen rather than drawn from a crowd. */
|
|
22
|
+
function isDistinctive(amount) {
|
|
23
|
+
if (amount === 0n)
|
|
24
|
+
return false;
|
|
25
|
+
// Round to some power of ten, or a clean multiple of one, and it hides in a
|
|
26
|
+
// crowd of similar transfers. Everything else is close to a fingerprint.
|
|
27
|
+
const text = amount.toString();
|
|
28
|
+
const trailingZeros = text.length - text.replace(/0+$/, '').length;
|
|
29
|
+
return trailingZeros < Math.max(1, Math.floor(text.length / 2));
|
|
30
|
+
}
|
|
31
|
+
export function describeDisclosure(plan) {
|
|
32
|
+
const hidden = [
|
|
33
|
+
'Which shielded note funded this plan.',
|
|
34
|
+
'Which address authored it. The pool pays the router, and the pool is the only party that knows who asked.',
|
|
35
|
+
];
|
|
36
|
+
const visible = [
|
|
37
|
+
'That the pool transferred tokens to the Jalin router, and how much.',
|
|
38
|
+
'Every contract this plan calls, the selector it calls, and every felt of calldata it is called with. An invoke action carries all three in the clear.',
|
|
39
|
+
'The amount credited back into a note, because the pool pulls it by allowance.',
|
|
40
|
+
];
|
|
41
|
+
const warnings = [];
|
|
42
|
+
if (plan.steps.length > 1) {
|
|
43
|
+
hidden.push(`That the ${plan.steps.length} legs belong to separate intentions. They are one transaction, so there is no gap between them to correlate.`);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
warnings.push('A single-step plan reveals the same action a direct call would. What it hides is who did it, not what was done.');
|
|
47
|
+
}
|
|
48
|
+
const leaving = unclaimedTokens(plan);
|
|
49
|
+
if (leaving.length > 0) {
|
|
50
|
+
visible.push(`That ${leaving.length === 1 ? 'a token' : `${leaving.length} tokens`} left entirely: ${leaving.join(', ')}. Where they went is as public as the contract that took them.`);
|
|
51
|
+
}
|
|
52
|
+
for (const output of plan.outputs) {
|
|
53
|
+
if (output.minAmount === 0n) {
|
|
54
|
+
warnings.push(`Output ${output.token} accepts any amount above zero. A route that returns almost nothing would still succeed; set a floor.`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Calldata is public. An address sitting in it that is not one of the
|
|
58
|
+
// contracts already named is a party to the plan the plan is naming out loud,
|
|
59
|
+
// and if it is the author's own account then the pool hid nothing.
|
|
60
|
+
const named = new Set([
|
|
61
|
+
...plan.steps.map((step) => String(BigInt(step.target))),
|
|
62
|
+
...plan.steps.flatMap((step) => step.approvals.map((a) => String(BigInt(a.token)))),
|
|
63
|
+
...plan.outputs.map((output) => String(BigInt(output.token))),
|
|
64
|
+
]);
|
|
65
|
+
const strangers = new Set();
|
|
66
|
+
for (const step of plan.steps) {
|
|
67
|
+
for (const felt of step.calldata) {
|
|
68
|
+
// Placeholders are resolved by the wallet, so there is nothing to leak.
|
|
69
|
+
if (typeof felt === 'string' && felt.startsWith('${'))
|
|
70
|
+
continue;
|
|
71
|
+
let value;
|
|
72
|
+
try {
|
|
73
|
+
value = BigInt(felt);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (value < ADDRESS_SHAPED)
|
|
79
|
+
continue;
|
|
80
|
+
if (named.has(String(value)))
|
|
81
|
+
continue;
|
|
82
|
+
strangers.add(`0x${value.toString(16)}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (strangers.size > 0) {
|
|
86
|
+
warnings.push(`Calldata carries ${[...strangers].slice(0, 3).join(', ')}, which ${strangers.size === 1 ? 'is an address' : 'are addresses'} this plan does not otherwise name. Calldata is public. If one of them is your own account, the plan says so in the clear and the pool hid nothing.`);
|
|
87
|
+
}
|
|
88
|
+
const amounts = plan.steps.flatMap((step) => step.approvals.map((a) => a.amount));
|
|
89
|
+
const distinctive = amounts.filter(isDistinctive);
|
|
90
|
+
if (distinctive.length > 0) {
|
|
91
|
+
warnings.push(`Amounts like ${distinctive.slice(0, 3).join(', ')} are distinctive enough to match against a shield or a withdrawal by size alone. Rounder numbers sit in a larger crowd.`);
|
|
92
|
+
}
|
|
93
|
+
if (plan.outputs.length === 0) {
|
|
94
|
+
warnings.push('Nothing is credited back, so this plan sends value away for good. Check the destination is somewhere you meant, because the router will not stop it.');
|
|
95
|
+
}
|
|
96
|
+
return { hidden, visible, warnings };
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=disclosure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"disclosure.js","sourceRoot":"","sources":["../src/disclosure.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from './plan.ts';
|
|
2
|
+
export * from './subaccounts.ts';
|
|
3
|
+
export * from './shadow.ts';
|
|
4
|
+
export * from './recipes.ts';
|
|
5
|
+
export * from './disclosure.ts';
|
|
6
|
+
export * from './wallet.ts';
|
|
7
|
+
export * from './crowd.ts';
|
|
8
|
+
export * from './anonymity.ts';
|
|
9
|
+
export * from './rpc-response.ts';
|
|
10
|
+
export * from './share.ts';
|
|
11
|
+
export * from './receipt.ts';
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAA;AACzB,cAAc,kBAAkB,CAAA;AAChC,cAAc,aAAa,CAAA;AAE3B,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,mBAAmB,CAAA;AACjC,cAAc,YAAY,CAAA;AAC1B,cAAc,cAAc,CAAA"}
|