@cofhe/sdk 0.8.0 → 1.0.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.
Files changed (87) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/acps/acp.ts +25 -6
  3. package/acps/index.ts +20 -15
  4. package/acps/onchain-utils.ts +15 -10
  5. package/acps/registry.ts +166 -8
  6. package/acps/sealing.ts +31 -5
  7. package/acps/share-metadata/appKey.ts +8 -0
  8. package/acps/share-metadata/decode.ts +120 -0
  9. package/acps/share-metadata/describe.ts +198 -0
  10. package/acps/share-metadata/encode.ts +147 -0
  11. package/acps/share-metadata/erc7984.ts +26 -0
  12. package/acps/share-metadata/index.ts +9 -0
  13. package/acps/share-metadata/layout.ts +69 -0
  14. package/acps/share-metadata/presets.ts +116 -0
  15. package/acps/share-metadata/schema.ts +47 -0
  16. package/acps/share-metadata/test/codec.test.ts +140 -0
  17. package/acps/share-metadata/test/describe.test.ts +112 -0
  18. package/acps/share-metadata/test/fixtures.ts +157 -0
  19. package/acps/share-metadata/test/live.test.ts +56 -0
  20. package/acps/share-metadata/test/presets.test.ts +119 -0
  21. package/acps/share-metadata/test/verify.test.ts +144 -0
  22. package/acps/share-metadata/verify.ts +155 -0
  23. package/acps/store.ts +48 -18
  24. package/acps/test/aclServedAddresses.test.ts +65 -0
  25. package/acps/test/acp.test.ts +102 -7
  26. package/acps/test/localstorage.test.ts +28 -1
  27. package/acps/test/registry.test.ts +144 -0
  28. package/acps/test/sealing.test.ts +74 -0
  29. package/acps/test/store.test.ts +71 -0
  30. package/acps/test/utils.test.ts +63 -0
  31. package/acps/test/validation.test.ts +0 -4
  32. package/acps/types.ts +43 -6
  33. package/acps/utils.ts +15 -2
  34. package/acps/validation.ts +3 -1
  35. package/core/acps.ts +123 -74
  36. package/core/client.ts +33 -7
  37. package/core/clientTypes.ts +25 -7
  38. package/core/config.ts +8 -0
  39. package/core/decrypt/apiError.ts +4 -1
  40. package/core/decrypt/verifyDecryptResult.ts +4 -1
  41. package/core/encrypt/cofheMocksZkVerifySign.ts +19 -2
  42. package/core/test/acpDefaults.test.ts +108 -19
  43. package/core/test/acpStoreAccountCase.test.ts +80 -0
  44. package/core/test/acps.test.ts +38 -3
  45. package/core/test/apiError.test.ts +8 -0
  46. package/core/test/appKeyConfig.test.ts +94 -0
  47. package/core/test/config.test.ts +8 -0
  48. package/core/test/decryptBuilders.test.ts +110 -2
  49. package/core/test/incomingShares.test.ts +84 -0
  50. package/core/test/mocksEncryptRange.test.ts +90 -0
  51. package/core/test/utils.test.ts +128 -0
  52. package/core/test/verifyDecryptResult.test.ts +51 -0
  53. package/core/utils.ts +16 -9
  54. package/dist/acps.cjs +796 -132
  55. package/dist/acps.d.cts +367 -35
  56. package/dist/acps.d.ts +367 -35
  57. package/dist/acps.js +2 -2
  58. package/dist/{chunk-UJYOBHYA.js → chunk-3L2IVPMI.js} +688 -203
  59. package/dist/chunk-J23FYAI2.js +174 -0
  60. package/dist/{chunk-GGI7Y6AW.js → chunk-O2ITEUEU.js} +110 -269
  61. package/dist/chunk-WJGTPW7K.js +228 -0
  62. package/dist/{clientTypes-D3aIXmUp.d.cts → clientTypes-IO6b5Um8.d.cts} +61 -21
  63. package/dist/{clientTypes-5YPPR16m.d.ts → clientTypes-b1pOxOUE.d.ts} +61 -21
  64. package/dist/core.cjs +3115 -2584
  65. package/dist/core.d.cts +6 -3
  66. package/dist/core.d.ts +6 -3
  67. package/dist/core.js +4 -3
  68. package/dist/node.cjs +3001 -2470
  69. package/dist/node.d.cts +2 -2
  70. package/dist/node.d.ts +2 -2
  71. package/dist/node.js +18 -17
  72. package/dist/{onchain-utils-CUAK8glx.d.cts → onchain-utils-CgOslubH.d.cts} +139 -15
  73. package/dist/{onchain-utils-CUAK8glx.d.ts → onchain-utils-CgOslubH.d.ts} +139 -15
  74. package/dist/web.cjs +3011 -2487
  75. package/dist/web.d.cts +2 -2
  76. package/dist/web.d.ts +2 -2
  77. package/dist/web.js +4 -10
  78. package/dist/zkProve.worker.cjs +14 -7
  79. package/dist/zkProve.worker.js +2 -8
  80. package/node/index.ts +1 -10
  81. package/node/storage.ts +18 -7
  82. package/node/test/storage-permissions.test.ts +40 -0
  83. package/package.json +4 -4
  84. package/web/index.ts +1 -10
  85. package/web/test/threadPoolBenchHelper.ts +1 -7
  86. package/web/zkProve.worker.ts +1 -10
  87. package/dist/chunk-NMKMCAI3.js +0 -14
@@ -0,0 +1,120 @@
1
+ import { getAddress, hexToBigInt, hexToNumber, size, sliceHex, type Hex } from 'viem';
2
+ import { APP_KEY_TAG, ARGS_CODE, ARG_TAG, ENTRY_KIND, LAYOUT_VERSION, BYTE_WIDTH } from './layout';
3
+ import type { ShareLabel, ShareLabelReadArg } from './schema.js';
4
+
5
+ /** Sequential reads over a blob. Each take throws when the blob ends before the field does. */
6
+ type Reader = {
7
+ take: (width: number) => Hex;
8
+ takeNumber: (width: number) => number;
9
+ done: () => boolean;
10
+ remaining: () => number;
11
+ };
12
+
13
+ const readerOf = (blob: Hex): Reader => {
14
+ let offset = 0;
15
+ const take = (width: number): Hex => {
16
+ if (offset + width > size(blob)) throw new Error('share metadata: the blob ends in the middle of a field');
17
+ offset += width;
18
+ return sliceHex(blob, offset - width, offset);
19
+ };
20
+ return {
21
+ take,
22
+ takeNumber: (width) => hexToNumber(take(width)),
23
+ done: () => offset === size(blob),
24
+ remaining: () => size(blob) - offset,
25
+ };
26
+ };
27
+
28
+ /** A counted list: its length in one byte, then that many items. */
29
+ const takeList = <T>(reader: Reader, takeItem: () => T): T[] =>
30
+ Array.from({ length: reader.takeNumber(BYTE_WIDTH.count) }, takeItem);
31
+
32
+ /** The item an entry points to. Throws when the position is outside the list. */
33
+ const takeFrom = <T>(reader: Reader, list: readonly T[], what: string): T => {
34
+ const position = reader.takeNumber(BYTE_WIDTH.index);
35
+ if (position >= list.length) {
36
+ throw new Error(`share metadata: ${what} ${position} is outside the ${what} list`);
37
+ }
38
+ return list[position];
39
+ };
40
+
41
+ const decodeArg = (reader: Reader): ShareLabelReadArg => {
42
+ const tag = reader.takeNumber(BYTE_WIDTH.argTag);
43
+ if (tag === ARG_TAG.issuer) return { type: 'issuer' };
44
+ if (tag === ARG_TAG.word) return { type: 'word', value: reader.take(BYTE_WIDTH.word) };
45
+ throw new Error(`share metadata: unknown argument tag ${tag}`);
46
+ };
47
+
48
+ const decodeArgs = (reader: Reader): { args: ShareLabelReadArg[]; returnWord: number } => {
49
+ const code = reader.takeNumber(BYTE_WIDTH.argsCode);
50
+ if (code === ARGS_CODE.none) return { args: [], returnWord: 0 };
51
+ if (code === ARGS_CODE.issuer) return { args: [{ type: 'issuer' }], returnWord: 0 };
52
+ if (code !== ARGS_CODE.explicit) throw new Error(`share metadata: unknown args code ${code}`);
53
+ const returnWord = reader.takeNumber(BYTE_WIDTH.returnWord);
54
+ return { args: takeList(reader, () => decodeArg(reader)), returnWord };
55
+ };
56
+
57
+ /** The optional app key that closes the blob: nothing, or the tag byte and 32 bytes. */
58
+ const takeAppKey = (reader: Reader): Hex | undefined => {
59
+ if (reader.done()) return undefined;
60
+ const isKey =
61
+ reader.remaining() === BYTE_WIDTH.count + BYTE_WIDTH.word && reader.takeNumber(BYTE_WIDTH.count) === APP_KEY_TAG;
62
+ if (!isKey) throw new Error('share metadata: the blob has more entries than the share has ctHashes');
63
+ return reader.take(BYTE_WIDTH.word);
64
+ };
65
+
66
+ /** The two things a blob holds: one label per ctHash, and the app key when the share names an app. */
67
+ export type DecodedShareMetadata = { labels: ShareLabel[]; appKey: Hex | undefined };
68
+
69
+ /**
70
+ * Decodes a version 3 blob: one label per ctHash, in the order of `acp.handles`, and the app key
71
+ * when there is one. The exact inverse of `encodeShareMetadata`. Throws on another version, an
72
+ * unknown kind, code or tag, a position outside its list, and unless the blob holds exactly one
73
+ * entry per ctHash.
74
+ */
75
+ export const decodeShareMetadataFull = (blob: Hex, handles: readonly Hex[]): DecodedShareMetadata => {
76
+ const reader = readerOf(blob);
77
+ const version = reader.takeNumber(BYTE_WIDTH.version);
78
+ if (version !== LAYOUT_VERSION) throw new Error(`share metadata: unsupported version ${version}`);
79
+
80
+ const contracts = takeList(reader, () => getAddress(reader.take(BYTE_WIDTH.address)));
81
+ const selectors = takeList(reader, () => reader.take(BYTE_WIDTH.selector));
82
+ const transactions = takeList(reader, () => ({
83
+ block: hexToBigInt(reader.take(BYTE_WIDTH.block)),
84
+ txIndex: reader.takeNumber(BYTE_WIDTH.txIndex),
85
+ }));
86
+
87
+ const labels = handles.map((): ShareLabel => {
88
+ const kind = reader.takeNumber(BYTE_WIDTH.kind);
89
+ switch (kind) {
90
+ case ENTRY_KIND.stored: {
91
+ const contract = takeFrom(reader, contracts, 'contract');
92
+ const selector = takeFrom(reader, selectors, 'selector');
93
+ const block = hexToBigInt(reader.take(BYTE_WIDTH.block));
94
+ return { kind: 'stored', contract, selector, ...decodeArgs(reader), block };
95
+ }
96
+ case ENTRY_KIND.event: {
97
+ const contract = takeFrom(reader, contracts, 'contract');
98
+ const { block, txIndex } = takeFrom(reader, transactions, 'transaction');
99
+ const logIndex = reader.takeNumber(BYTE_WIDTH.logIndex);
100
+ const selector = takeFrom(reader, selectors, 'selector');
101
+ return { kind: 'event', contract, block, txIndex, logIndex, selector };
102
+ }
103
+ case ENTRY_KIND.unlabelled:
104
+ return { kind: 'unlabelled' };
105
+ default:
106
+ throw new Error(`share metadata: unknown entry kind ${kind}`);
107
+ }
108
+ });
109
+ const appKey = takeAppKey(reader);
110
+ if (!reader.done()) throw new Error('share metadata: the blob has more entries than the share has ctHashes');
111
+ return { labels, appKey };
112
+ };
113
+
114
+ /** The labels of a blob, one per ctHash in the order of `acp.handles` (see `decodeShareMetadataFull`). */
115
+ export const decodeShareMetadata = (blob: Hex, handles: readonly Hex[]): ShareLabel[] =>
116
+ decodeShareMetadataFull(blob, handles).labels;
117
+
118
+ /** The app key a share was posted with, or undefined: for a share without metadata, or without a key. */
119
+ export const decodeShareAppKey = (metadata: Hex | undefined, handles: readonly Hex[]): Hex | undefined =>
120
+ metadata == null || metadata === '0x' ? undefined : decodeShareMetadataFull(metadata, handles).appKey;
@@ -0,0 +1,198 @@
1
+ import {
2
+ decodeAbiParameters,
3
+ padHex,
4
+ toEventSelector,
5
+ toEventSignature,
6
+ toFunctionSelector,
7
+ toFunctionSignature,
8
+ type Abi,
9
+ type AbiEvent,
10
+ type AbiFunction,
11
+ type AbiParameter,
12
+ type Address,
13
+ type Hex,
14
+ } from 'viem';
15
+ import { decodeShareMetadata } from './decode.js';
16
+ import type { EventValueLabel, ShareLabel, StoredValueLabel } from './schema.js';
17
+ import type { LabelledShare } from '../types';
18
+
19
+ /** A parameter as the ABI names it. `name` is absent when the ABI leaves it unnamed. */
20
+ export type ShareLabelParam = { name?: string; type: string; indexed?: boolean };
21
+
22
+ /** One argument of a stored-value read, decoded with the function's input type when the ABI has one. */
23
+ export type ShareLabelArg = ShareLabelParam & {
24
+ /** The argument stands for the share issuer. */
25
+ isIssuer: boolean;
26
+ /** The 32-byte ABI word, or undefined for an issuer argument when no issuer was given. */
27
+ raw: Hex | undefined;
28
+ /** The decoded value (an address, a bigint, ...), or the raw word when there is no ABI type for it. */
29
+ value: unknown;
30
+ };
31
+
32
+ type DescriptionBase = {
33
+ /** Position of the ctHash in `acp.handles`. */
34
+ index: number;
35
+ handle: Hex;
36
+ };
37
+
38
+ export type StoredValueDescription = DescriptionBase & {
39
+ kind: 'stored';
40
+ label: StoredValueLabel;
41
+ contract: Address;
42
+ block: bigint;
43
+ selector: Hex;
44
+ /** The view function, or null when the ABI of the contract is missing or has no function with this selector. */
45
+ function: { name: string; signature: string; inputs: ShareLabelParam[] } | null;
46
+ args: ShareLabelArg[];
47
+ /** The returned field that holds the ctHash, when the ABI names the outputs. */
48
+ output: ShareLabelParam | null;
49
+ };
50
+
51
+ export type EventValueDescription = DescriptionBase & {
52
+ kind: 'event';
53
+ label: EventValueLabel;
54
+ contract: Address;
55
+ block: bigint;
56
+ txIndex: number;
57
+ logIndex: number;
58
+ selector: Hex;
59
+ /**
60
+ * The event, or null when the ABI of the contract is missing or has no event with this selector.
61
+ * Parameter values are not in the label; they come with the log fetched to verify it.
62
+ */
63
+ event: { name: string; signature: string; inputs: ShareLabelParam[] } | null;
64
+ };
65
+
66
+ export type UnlabelledDescription = DescriptionBase & {
67
+ kind: 'unlabelled';
68
+ label: ShareLabel & { kind: 'unlabelled' };
69
+ };
70
+
71
+ export type ShareLabelDescription = StoredValueDescription | EventValueDescription | UnlabelledDescription;
72
+
73
+ /** ABIs by contract address; addresses compare case-insensitively. */
74
+ export type ShareLabelAbis = Readonly<Record<Address, Abi>>;
75
+
76
+ const abiOf = (abis: ShareLabelAbis, contract: Address): Abi | undefined => {
77
+ const wanted = contract.toLowerCase();
78
+ const key = (Object.keys(abis) as Address[]).find((address) => address.toLowerCase() === wanted);
79
+ return key ? abis[key] : undefined;
80
+ };
81
+
82
+ const paramOf = ({ name, type, ...rest }: AbiParameter): ShareLabelParam => {
83
+ const param: ShareLabelParam = { type };
84
+ if (name) param.name = name;
85
+ if ('indexed' in rest && typeof rest.indexed === 'boolean') param.indexed = rest.indexed;
86
+ return param;
87
+ };
88
+
89
+ const findFunction = (abi: Abi | undefined, selector: Hex): AbiFunction | undefined =>
90
+ abi?.find(
91
+ (item): item is AbiFunction =>
92
+ item.type === 'function' && toFunctionSelector(item).toLowerCase() === selector.toLowerCase()
93
+ );
94
+
95
+ const findEvent = (abi: Abi | undefined, selector: Hex): AbiEvent | undefined =>
96
+ abi?.find(
97
+ (item): item is AbiEvent =>
98
+ item.type === 'event' && toEventSelector(item).toLowerCase().startsWith(selector.toLowerCase())
99
+ );
100
+
101
+ const decodeWord = (input: AbiParameter | undefined, raw: Hex): unknown => {
102
+ if (input == null) return raw;
103
+ try {
104
+ return decodeAbiParameters([input], raw)[0];
105
+ } catch {
106
+ return raw;
107
+ }
108
+ };
109
+
110
+ const describeStored = (
111
+ base: DescriptionBase,
112
+ label: StoredValueLabel,
113
+ abis: ShareLabelAbis,
114
+ issuer: Address | undefined
115
+ ): StoredValueDescription => {
116
+ const fn = findFunction(abiOf(abis, label.contract), label.selector);
117
+ const args = label.args.map((arg, i): ShareLabelArg => {
118
+ const input = fn?.inputs[i];
119
+ const param = input ? paramOf(input) : { type: 'bytes32' };
120
+ if (arg.type === 'issuer') {
121
+ return {
122
+ ...param,
123
+ isIssuer: true,
124
+ raw: issuer ? padHex(issuer, { size: 32 }) : undefined,
125
+ value: issuer,
126
+ };
127
+ }
128
+ return { ...param, isIssuer: false, raw: arg.value, value: decodeWord(input, arg.value) };
129
+ });
130
+ const output = fn?.outputs[label.returnWord];
131
+ return {
132
+ ...base,
133
+ kind: 'stored',
134
+ label,
135
+ contract: label.contract,
136
+ block: label.block,
137
+ selector: label.selector,
138
+ function: fn ? { name: fn.name, signature: toFunctionSignature(fn), inputs: fn.inputs.map(paramOf) } : null,
139
+ args,
140
+ output: output ? paramOf(output) : null,
141
+ };
142
+ };
143
+
144
+ const describeEvent = (base: DescriptionBase, label: EventValueLabel, abis: ShareLabelAbis): EventValueDescription => {
145
+ const event = findEvent(abiOf(abis, label.contract), label.selector);
146
+ return {
147
+ ...base,
148
+ kind: 'event',
149
+ label,
150
+ contract: label.contract,
151
+ block: label.block,
152
+ txIndex: label.txIndex,
153
+ logIndex: label.logIndex,
154
+ selector: label.selector,
155
+ event: event ? { name: event.name, signature: toEventSignature(event), inputs: event.inputs.map(paramOf) } : null,
156
+ };
157
+ };
158
+
159
+ /**
160
+ * Turns decoded labels into structured, human-readable items, one per ctHash in the order of
161
+ * `handles`: the function name and decoded arguments of a stored value, the event name and
162
+ * parameters of an event value. A contract without an ABI, or a selector its ABI doesn't have,
163
+ * leaves the item in raw form (`function` / `event` null); this never throws. `issuer` (the share
164
+ * issuer) fills in the arguments that stand for it.
165
+ */
166
+ export const describeShareLabels = (
167
+ labels: readonly ShareLabel[],
168
+ handles: readonly Hex[],
169
+ abis: ShareLabelAbis = {},
170
+ options: { issuer?: Address } = {}
171
+ ): ShareLabelDescription[] =>
172
+ labels.map((label, index): ShareLabelDescription => {
173
+ const base = { index, handle: handles[index] };
174
+ switch (label.kind) {
175
+ case 'stored':
176
+ return describeStored(base, label, abis, options.issuer);
177
+ case 'event':
178
+ return describeEvent(base, label, abis);
179
+ case 'unlabelled':
180
+ return { ...base, kind: 'unlabelled', label };
181
+ }
182
+ });
183
+
184
+ /**
185
+ * Decodes a share's metadata blob and describes it: `decodeShareMetadata` then
186
+ * `describeShareLabels`, with the share's issuer filling in the arguments that stand for it. The
187
+ * share is one read from the registry or an acp imported from exported JSON. Null for a share
188
+ * without labels (no metadata, or `0x`). Throws only when the blob itself is malformed; missing
189
+ * ABIs leave items in raw form.
190
+ */
191
+ export const describeShareMetadata = (
192
+ share: LabelledShare,
193
+ abis: ShareLabelAbis = {}
194
+ ): ShareLabelDescription[] | null => {
195
+ const { issuer, handles, metadata } = share;
196
+ if (metadata == null || metadata === '0x') return null;
197
+ return describeShareLabels(decodeShareMetadata(metadata, handles), handles, abis, { issuer });
198
+ };
@@ -0,0 +1,147 @@
1
+ import { concatHex, isHex, numberToHex, padHex, size, type Address, type Hex } from 'viem';
2
+ import { APP_KEY_TAG, ARGS_CODE, ARG_TAG, ENTRY_KIND, LAYOUT_VERSION, BYTE_WIDTH } from './layout';
3
+ import type { ShareLabel, ShareLabelReadArg, StoredValueLabel } from './schema';
4
+
5
+ /** An unsigned big-endian integer in exactly that many bytes. Throws when the value does not fit. */
6
+ const uint = (value: number | bigint, width: number): Hex => numberToHex(value, { size: width });
7
+
8
+ /** The byte-string fields of the blob: the width each must have, and how an error names it. */
9
+ const BYTES_FIELD = {
10
+ contract: { width: BYTE_WIDTH.address, name: 'a contract address' },
11
+ selector: { width: BYTE_WIDTH.selector, name: 'a selector' },
12
+ viewCallArgument: { width: BYTE_WIDTH.word, name: 'a view call argument' },
13
+ appKey: { width: BYTE_WIDTH.word, name: 'an app key' },
14
+ } as const;
15
+
16
+ /** A byte string for that field, checked to be exactly as wide as the field. */
17
+ const bytesField = (field: keyof typeof BYTES_FIELD, value: Hex): Hex => {
18
+ const { width, name } = BYTES_FIELD[field];
19
+ if (!isHex(value, { strict: true }) || size(value) !== width) {
20
+ throw new Error(`share metadata: ${name} must be ${width} bytes`);
21
+ }
22
+ return value.toLowerCase() as Hex;
23
+ };
24
+
25
+ /**
26
+ * A list of distinct items in order of first use. `insert` adds an item unless an equal one is
27
+ * already listed; `positionOf` looks an item up and throws when it was never inserted.
28
+ */
29
+ const listOf = <T>(keyOf: (item: T) => string, what: string) => {
30
+ const items: T[] = [];
31
+ const positions = new Map<string, number>();
32
+ const insert = (item: T): void => {
33
+ const key = keyOf(item);
34
+ if (positions.has(key)) return;
35
+ positions.set(key, items.length);
36
+ items.push(item);
37
+ };
38
+ const positionOf = (item: T): number => {
39
+ const position = positions.get(keyOf(item));
40
+ if (position === undefined) throw new Error(`share metadata: ${what} ${keyOf(item)} was not collected`);
41
+ return position;
42
+ };
43
+ return { items, insert, positionOf };
44
+ };
45
+
46
+ type Transaction = { block: bigint; txIndex: number };
47
+
48
+ const keyOfContract = (contract: Address): string => contract.toLowerCase();
49
+ const keyOfSelector = (selector: Hex): string => selector.toLowerCase();
50
+ const keyOfTransaction = ({ block, txIndex }: Transaction): string => `${block}:${txIndex}`;
51
+
52
+ const transactionOf = ({ block, txIndex }: Transaction): Transaction => ({ block, txIndex });
53
+
54
+ const encodeArg = (arg: ShareLabelReadArg): Hex => {
55
+ if (arg.type === 'issuer') return uint(ARG_TAG.issuer, BYTE_WIDTH.argTag);
56
+ return concatHex([uint(ARG_TAG.word, BYTE_WIDTH.argTag), bytesField('viewCallArgument', arg.value)]);
57
+ };
58
+
59
+ /** The issuer's address as a 32-byte word: the value `ARGS_CODE.issuer` and `ARG_TAG.issuer` stand for. */
60
+ const issuerWordOf = (issuer: Address | undefined): Hex | undefined =>
61
+ issuer == null ? undefined : (padHex(issuer, { size: BYTE_WIDTH.word }).toLowerCase() as Hex);
62
+
63
+ /** A literal argument that is the issuer's address becomes "the issuer": one byte, and no address in the blob. */
64
+ const asIssuer = (arg: ShareLabelReadArg, issuerWord: Hex | undefined): ShareLabelReadArg =>
65
+ arg.type === 'word' && issuerWord != null && arg.value.toLowerCase() === issuerWord ? { type: 'issuer' } : arg;
66
+
67
+ /**
68
+ * The two common shapes take one byte: no arguments, or the share issuer alone, both with the
69
+ * ctHash as the first return word. Anything else is spelled out.
70
+ */
71
+ const encodeArgs = ({ args: given, returnWord }: StoredValueLabel, issuerWord: Hex | undefined): Hex => {
72
+ const args = given.map((arg) => asIssuer(arg, issuerWord));
73
+ if (returnWord === 0 && args.length === 0) return uint(ARGS_CODE.none, BYTE_WIDTH.argsCode);
74
+ if (returnWord === 0 && args.length === 1 && args[0].type === 'issuer') {
75
+ return uint(ARGS_CODE.issuer, BYTE_WIDTH.argsCode);
76
+ }
77
+ return concatHex([
78
+ uint(ARGS_CODE.explicit, BYTE_WIDTH.argsCode),
79
+ uint(returnWord, BYTE_WIDTH.returnWord),
80
+ uint(args.length, BYTE_WIDTH.count),
81
+ ...args.map(encodeArg),
82
+ ]);
83
+ };
84
+
85
+ /**
86
+ * Encodes the labels of a share into a version 3 blob: one label per ctHash, in the order of
87
+ * `acp.handles`. The contracts, selectors and transactions the labels name are listed once each,
88
+ * in order of first use; function and event selectors share one list. Throws on a value that does
89
+ * not fit its field (more than 255 contracts, a block past uint32, ...). Given the share `issuer`, a
90
+ * stored-value argument equal to their address is stored as "the issuer" instead of the address.
91
+ * Given an `appKey` (`shareAppKey(app)`), it closes the blob, so the app can tell its shares apart.
92
+ */
93
+ export const encodeShareMetadata = (
94
+ labels: readonly ShareLabel[],
95
+ { issuer, appKey }: { issuer?: Address; appKey?: Hex } = {}
96
+ ): Hex => {
97
+ const issuerWord = issuerWordOf(issuer);
98
+
99
+ // First pass: collect what the labels name, so each list is complete before any entry points into it.
100
+ const contracts = listOf(keyOfContract, 'contract');
101
+ const selectors = listOf(keyOfSelector, 'selector');
102
+ const transactions = listOf(keyOfTransaction, 'transaction');
103
+ for (const label of labels) {
104
+ if (label.kind === 'unlabelled') continue;
105
+ contracts.insert(label.contract);
106
+ selectors.insert(label.selector);
107
+ if (label.kind === 'event') transactions.insert(transactionOf(label));
108
+ }
109
+
110
+ // Second pass: one entry per label, pointing into the lists by position.
111
+ const entries = labels.map((label): Hex => {
112
+ switch (label.kind) {
113
+ case 'stored':
114
+ return concatHex([
115
+ uint(ENTRY_KIND.stored, BYTE_WIDTH.kind),
116
+ uint(contracts.positionOf(label.contract), BYTE_WIDTH.index),
117
+ uint(selectors.positionOf(label.selector), BYTE_WIDTH.index),
118
+ uint(label.block, BYTE_WIDTH.block),
119
+ encodeArgs(label, issuerWord),
120
+ ]);
121
+ case 'event':
122
+ return concatHex([
123
+ uint(ENTRY_KIND.event, BYTE_WIDTH.kind),
124
+ uint(contracts.positionOf(label.contract), BYTE_WIDTH.index),
125
+ uint(transactions.positionOf(transactionOf(label)), BYTE_WIDTH.index),
126
+ uint(label.logIndex, BYTE_WIDTH.logIndex),
127
+ uint(selectors.positionOf(label.selector), BYTE_WIDTH.index),
128
+ ]);
129
+ case 'unlabelled':
130
+ return uint(ENTRY_KIND.unlabelled, BYTE_WIDTH.kind);
131
+ }
132
+ });
133
+
134
+ return concatHex([
135
+ uint(LAYOUT_VERSION, BYTE_WIDTH.version),
136
+ uint(contracts.items.length, BYTE_WIDTH.count),
137
+ ...contracts.items.map((contract) => bytesField('contract', contract)),
138
+ uint(selectors.items.length, BYTE_WIDTH.count),
139
+ ...selectors.items.map((selector) => bytesField('selector', selector)),
140
+ uint(transactions.items.length, BYTE_WIDTH.count),
141
+ ...transactions.items.map((tx) =>
142
+ concatHex([uint(tx.block, BYTE_WIDTH.block), uint(tx.txIndex, BYTE_WIDTH.txIndex)])
143
+ ),
144
+ ...entries,
145
+ ...(appKey == null ? [] : [uint(APP_KEY_TAG, BYTE_WIDTH.count), bytesField('appKey', appKey)]),
146
+ ]);
147
+ };
@@ -0,0 +1,26 @@
1
+ import type { Abi } from 'viem';
2
+
3
+ /**
4
+ * The parts of ERC-7984 (confidential tokens) that share labels most often point at: the balance
5
+ * read and the transfer event. One definition for the SDK's label presets and for apps, with
6
+ * `internalType` kept so `@cofhe/abi` recognises the encrypted values.
7
+ */
8
+ export const ERC7984_ABI = [
9
+ {
10
+ type: 'function',
11
+ name: 'confidentialBalanceOf',
12
+ stateMutability: 'view',
13
+ inputs: [{ name: 'account', type: 'address', internalType: 'address' }],
14
+ outputs: [{ name: '', type: 'bytes32', internalType: 'euint64' }],
15
+ },
16
+ {
17
+ type: 'event',
18
+ name: 'ConfidentialTransfer',
19
+ anonymous: false,
20
+ inputs: [
21
+ { name: 'from', type: 'address', indexed: true, internalType: 'address' },
22
+ { name: 'to', type: 'address', indexed: true, internalType: 'address' },
23
+ { name: 'amount', type: 'bytes32', indexed: true, internalType: 'euint64' },
24
+ ],
25
+ },
26
+ ] as const satisfies Abi;
@@ -0,0 +1,9 @@
1
+ export * from './schema';
2
+ export { LAYOUT_VERSION as SHARE_METADATA_VERSION } from './layout';
3
+ export * from './encode';
4
+ export * from './decode';
5
+ export * from './verify';
6
+ export * from './describe';
7
+ export * from './presets';
8
+ export * from './erc7984';
9
+ export * from './appKey';
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Byte layout of a share metadata blob, version 3. Integers are big-endian; sizes in bytes.
3
+ *
4
+ * blob = version (1)
5
+ * | contract count (1) | contracts (20 each)
6
+ * | selector count (1) | selectors (4 each) function selectors, and event
7
+ * selectors (first 4 bytes of topic0)
8
+ * | transaction count (1) | transactions (6 each: block 4, tx index 2)
9
+ * | entries, one per ctHash, in the order of acp.handles
10
+ * | app key, optional: 0x01 | key (32) keccak256 of the name of the app the
11
+ * share is for; absent when the blob ends
12
+ *
13
+ * stored value = 0x00 | contract (1) | selector (1) | block (4) | args 8 bytes or more
14
+ * event value = 0x01 | contract (1) | transaction (1) | log index (2) | selector (1) 6 bytes
15
+ * unlabelled = 0xff 1 byte
16
+ *
17
+ * args = 0x00 no arguments, return word 0
18
+ * | 0x01 one argument, the share issuer, return word 0
19
+ * | 0xff | return word (1) | count (1) | argument, count times
20
+ * argument = 0x00 | word (32) a literal ABI word
21
+ * | 0x01 the share issuer
22
+ *
23
+ * Contracts, selectors and transactions are listed once; entries refer to them by position.
24
+ * The log index is block-wide, as RPC nodes return it.
25
+ */
26
+
27
+ export const LAYOUT_VERSION = 0x03;
28
+
29
+ export const ENTRY_KIND = {
30
+ stored: 0x00,
31
+ event: 0x01,
32
+ unlabelled: 0xff,
33
+ } as const;
34
+
35
+ /** The args byte of a stored-value entry: the two common shapes in one byte, or an explicit list. */
36
+ export const ARGS_CODE = {
37
+ none: 0x00,
38
+ issuer: 0x01,
39
+ explicit: 0xff,
40
+ } as const;
41
+
42
+ export const ARG_TAG = {
43
+ word: 0x00,
44
+ issuer: 0x01,
45
+ } as const;
46
+
47
+ /** The one byte that opens the optional app key section at the end of the blob. */
48
+ export const APP_KEY_TAG = 0x01;
49
+
50
+ /** Field widths in bytes. */
51
+ export const BYTE_WIDTH = {
52
+ version: 1,
53
+ /** Length of a list, and a position in it: at most 255 of each per share. */
54
+ count: 1,
55
+ index: 1,
56
+ address: 20,
57
+ selector: 4,
58
+ /** uint32: 4.29 billion blocks. */
59
+ block: 4,
60
+ /** uint16: the position of a transaction within its block. */
61
+ txIndex: 2,
62
+ /** uint16: the block-wide position of a log (a busy block passes 255 logs). */
63
+ logIndex: 2,
64
+ kind: 1,
65
+ argsCode: 1,
66
+ returnWord: 1,
67
+ argTag: 1,
68
+ word: 32,
69
+ } as const;
@@ -0,0 +1,116 @@
1
+ import {
2
+ encodeAbiParameters,
3
+ size,
4
+ sliceHex,
5
+ toFunctionSelector,
6
+ type Abi,
7
+ type AbiFunction,
8
+ type Address,
9
+ type ContractFunctionArgs,
10
+ type ContractFunctionName,
11
+ type Log,
12
+ } from 'viem';
13
+ import { BYTE_WIDTH } from './layout';
14
+ import type { EventValueLabel, ShareLabel, ShareLabelReadArg, StoredValueLabel } from './schema';
15
+
16
+ /**
17
+ * Builders for labels. The format knows nothing about tokens: a balance is the value a view
18
+ * function returned, a transfer amount the value of one event.
19
+ */
20
+
21
+ export type ViewFunctionLabelOptions<
22
+ abi extends Abi,
23
+ functionName extends ContractFunctionName<abi, 'pure' | 'view'>,
24
+ > = {
25
+ contract: Address;
26
+ abi: abi;
27
+ functionName: functionName;
28
+ /**
29
+ * The arguments of the call, static ABI types only. One equal to the share issuer's address
30
+ * (your own, when you share your balance) is stored as "the issuer" when the labels are encoded.
31
+ */
32
+ args?: ContractFunctionArgs<abi, 'pure' | 'view', functionName>;
33
+ /** Block the value was read at (on Arbitrum: the L2 block number). */
34
+ block: bigint;
35
+ /** Which 32-byte word of the return data is the ctHash; 0 (the default) for a single return value. */
36
+ returnWord?: number;
37
+ };
38
+
39
+ const findViewFunction = (abi: Abi, name: string, arity: number): AbiFunction => {
40
+ const named = abi.filter((item): item is AbiFunction => item.type === 'function' && item.name === name);
41
+ if (named.length === 0) throw new Error(`share metadata: the ABI has no function ${name}`);
42
+ const fitting = named.filter((fn) => fn.inputs.length === arity);
43
+ if (fitting.length !== 1) {
44
+ throw new Error(
45
+ named.length === 1
46
+ ? `share metadata: ${name} takes ${named[0].inputs.length} arguments, ${arity} given`
47
+ : `share metadata: no single overload of ${name} takes ${arity} arguments`
48
+ );
49
+ }
50
+ const [fn] = fitting;
51
+ if (fn.stateMutability !== 'view' && fn.stateMutability !== 'pure') {
52
+ throw new Error(`share metadata: ${name} is not a view function`);
53
+ }
54
+ return fn;
55
+ };
56
+
57
+ const encodeReadArg = (fn: AbiFunction, index: number, value: unknown): ShareLabelReadArg => {
58
+ const input = fn.inputs[index];
59
+ const word = encodeAbiParameters([input], [value]);
60
+ if (size(word) !== BYTE_WIDTH.word) {
61
+ throw new Error(
62
+ `share metadata: argument ${index} of ${fn.name} is ${input.type}; only arguments of one static 32-byte word can be labelled`
63
+ );
64
+ }
65
+ return { type: 'word', value: word };
66
+ };
67
+
68
+ /**
69
+ * The label of a value a view function returned at a block, built from the contract's ABI: the
70
+ * function's selector, and each argument ABI-encoded to its word. Throws when the ABI has no such
71
+ * function, the arity is off, or an argument is not one static word.
72
+ */
73
+ export const viewFunctionLabel = <
74
+ const abi extends Abi,
75
+ functionName extends ContractFunctionName<abi, 'pure' | 'view'>,
76
+ >(
77
+ options: ViewFunctionLabelOptions<abi, functionName>
78
+ ): StoredValueLabel => {
79
+ const { contract, block, returnWord = 0 } = options;
80
+ const values = (options.args ?? []) as readonly unknown[];
81
+ const fn = findViewFunction(options.abi, options.functionName, values.length);
82
+ return {
83
+ kind: 'stored',
84
+ contract,
85
+ selector: toFunctionSelector(fn),
86
+ args: values.map((value, i) => encodeReadArg(fn, i, value)),
87
+ returnWord,
88
+ block,
89
+ };
90
+ };
91
+
92
+ /**
93
+ * The label of a value carried by a log, as viem returns it from `getLogs` / `getContractEvents`:
94
+ * its contract, block, transaction and log index, and its event selector. Throws for a pending log.
95
+ */
96
+ export const eventLogLabel = (
97
+ log: Pick<Log, 'address' | 'blockNumber' | 'transactionIndex' | 'logIndex' | 'topics'>
98
+ ): EventValueLabel => {
99
+ const { address, blockNumber, transactionIndex, logIndex, topics } = log;
100
+ const topic0 = topics[0];
101
+ if (blockNumber == null || transactionIndex == null || logIndex == null) {
102
+ throw new Error('share metadata: a pending log has no position to label');
103
+ }
104
+ if (topic0 == null || size(topic0) !== 32) throw new Error('share metadata: an anonymous log has no event selector');
105
+ return {
106
+ kind: 'event',
107
+ contract: address,
108
+ block: blockNumber,
109
+ txIndex: transactionIndex,
110
+ logIndex,
111
+ selector: sliceHex(topic0, 0, 4),
112
+ };
113
+ };
114
+
115
+ /** The label of a ctHash the issuer chooses not to describe. */
116
+ export const UNLABELLED: ShareLabel = { kind: 'unlabelled' };