@cofhe/sdk 0.8.0 → 1.0.0-alpha.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/acps/acp.ts +25 -6
- package/acps/index.ts +20 -15
- package/acps/registry.ts +166 -8
- package/acps/sealing.ts +9 -3
- package/acps/share-metadata/appKey.ts +8 -0
- package/acps/share-metadata/decode.ts +120 -0
- package/acps/share-metadata/describe.ts +198 -0
- package/acps/share-metadata/encode.ts +147 -0
- package/acps/share-metadata/erc7984.ts +26 -0
- package/acps/share-metadata/index.ts +9 -0
- package/acps/share-metadata/layout.ts +69 -0
- package/acps/share-metadata/presets.ts +116 -0
- package/acps/share-metadata/schema.ts +47 -0
- package/acps/share-metadata/test/codec.test.ts +140 -0
- package/acps/share-metadata/test/describe.test.ts +112 -0
- package/acps/share-metadata/test/fixtures.ts +157 -0
- package/acps/share-metadata/test/live.test.ts +56 -0
- package/acps/share-metadata/test/presets.test.ts +119 -0
- package/acps/share-metadata/test/verify.test.ts +144 -0
- package/acps/share-metadata/verify.ts +155 -0
- package/acps/store.ts +48 -18
- package/acps/test/acp.test.ts +102 -7
- package/acps/test/localstorage.test.ts +28 -1
- package/acps/test/registry.test.ts +144 -0
- package/acps/test/sealing.test.ts +23 -0
- package/acps/test/store.test.ts +71 -0
- package/acps/test/utils.test.ts +63 -0
- package/acps/test/validation.test.ts +0 -4
- package/acps/types.ts +43 -6
- package/acps/utils.ts +15 -2
- package/acps/validation.ts +3 -1
- package/core/acps.ts +123 -74
- package/core/client.ts +33 -7
- package/core/clientTypes.ts +25 -7
- package/core/config.ts +8 -0
- package/core/decrypt/apiError.ts +4 -1
- package/core/decrypt/verifyDecryptResult.ts +4 -1
- package/core/encrypt/cofheMocksZkVerifySign.ts +19 -2
- package/core/test/acpDefaults.test.ts +108 -19
- package/core/test/acpStoreAccountCase.test.ts +80 -0
- package/core/test/acps.test.ts +38 -3
- package/core/test/apiError.test.ts +8 -0
- package/core/test/appKeyConfig.test.ts +94 -0
- package/core/test/config.test.ts +8 -0
- package/core/test/decryptBuilders.test.ts +110 -2
- package/core/test/incomingShares.test.ts +84 -0
- package/core/test/mocksEncryptRange.test.ts +90 -0
- package/core/test/utils.test.ts +128 -0
- package/core/test/verifyDecryptResult.test.ts +51 -0
- package/core/utils.ts +16 -9
- package/dist/acps.cjs +674 -33
- package/dist/acps.d.cts +367 -35
- package/dist/acps.d.ts +367 -35
- package/dist/acps.js +2 -2
- package/dist/chunk-6ZW76ZKB.js +228 -0
- package/dist/{chunk-UJYOBHYA.js → chunk-BQWHPIYU.js} +660 -198
- package/dist/{chunk-GGI7Y6AW.js → chunk-OL3DZN7A.js} +110 -269
- package/dist/chunk-XQ32JG3T.js +174 -0
- package/dist/{clientTypes-D3aIXmUp.d.cts → clientTypes-C0A9ji5O.d.cts} +61 -21
- package/dist/{clientTypes-5YPPR16m.d.ts → clientTypes-CcU5WvdV.d.ts} +61 -21
- package/dist/core.cjs +3239 -2731
- package/dist/core.d.cts +6 -3
- package/dist/core.d.ts +6 -3
- package/dist/core.js +4 -3
- package/dist/node.cjs +3046 -2538
- package/dist/node.d.cts +2 -2
- package/dist/node.d.ts +2 -2
- package/dist/node.js +18 -17
- package/dist/{onchain-utils-CUAK8glx.d.cts → onchain-utils-D9Z33IP7.d.cts} +136 -12
- package/dist/{onchain-utils-CUAK8glx.d.ts → onchain-utils-D9Z33IP7.d.ts} +136 -12
- package/dist/web.cjs +3074 -2573
- package/dist/web.d.cts +2 -2
- package/dist/web.d.ts +2 -2
- package/dist/web.js +4 -10
- package/dist/zkProve.worker.cjs +14 -7
- package/dist/zkProve.worker.js +2 -8
- package/node/index.ts +1 -10
- package/node/storage.ts +18 -7
- package/node/test/storage-permissions.test.ts +40 -0
- package/package.json +4 -4
- package/web/index.ts +1 -10
- package/web/test/threadPoolBenchHelper.ts +1 -7
- package/web/zkProve.worker.ts +1 -10
- package/dist/chunk-NMKMCAI3.js +0 -14
|
@@ -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' };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Address, Hex } from 'viem';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A label says where one ctHash of a SNAPSHOT share came from, as something the recipient can
|
|
5
|
+
* check against the chain. It speaks of contracts, selectors and positions in the chain only:
|
|
6
|
+
* it does not know what a token, a balance or a transfer is. A reader that recognises a selector
|
|
7
|
+
* can present the value accordingly (see `describeShareMetadata`). A share carries one label per
|
|
8
|
+
* ctHash, in the order of `acp.handles`, so the ctHashes themselves are never repeated.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** One argument of a stored-value read: the share issuer, or a literal 32-byte ABI word. */
|
|
12
|
+
export type ShareLabelReadArg = { type: 'issuer' } | { type: 'word'; value: Hex };
|
|
13
|
+
|
|
14
|
+
/** A stored value: what a view function of a contract returned at a block. */
|
|
15
|
+
export type StoredValueLabel = {
|
|
16
|
+
kind: 'stored';
|
|
17
|
+
contract: Address;
|
|
18
|
+
/** 4-byte selector of the view function. */
|
|
19
|
+
selector: Hex;
|
|
20
|
+
/** Static ABI types only: each argument is one 32-byte word. */
|
|
21
|
+
args: ShareLabelReadArg[];
|
|
22
|
+
/** Which 32-byte word of the return data is the ctHash (0 for a single return value). */
|
|
23
|
+
returnWord: number;
|
|
24
|
+
/** Block the value was read at (on Arbitrum: the L2 block number). */
|
|
25
|
+
block: bigint;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** An event value: a value carried by one event (log) that a contract emitted. */
|
|
29
|
+
export type EventValueLabel = {
|
|
30
|
+
kind: 'event';
|
|
31
|
+
contract: Address;
|
|
32
|
+
/** Block of the log (on Arbitrum: the L2 block number). */
|
|
33
|
+
block: bigint;
|
|
34
|
+
/** Index of the log's transaction within its block. */
|
|
35
|
+
txIndex: number;
|
|
36
|
+
/** Block-wide index of the log, as RPC nodes return it. */
|
|
37
|
+
logIndex: number;
|
|
38
|
+
/** First 4 bytes of the log's topic0: the event selector. */
|
|
39
|
+
selector: Hex;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** The issuer chose not to say where the ctHash came from. */
|
|
43
|
+
export type UnlabelledLabel = {
|
|
44
|
+
kind: 'unlabelled';
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type ShareLabel = StoredValueLabel | EventValueLabel | UnlabelledLabel;
|