@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.
- package/CHANGELOG.md +51 -0
- package/acps/acp.ts +25 -6
- package/acps/index.ts +20 -15
- package/acps/onchain-utils.ts +15 -10
- package/acps/registry.ts +166 -8
- package/acps/sealing.ts +31 -5
- 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/aclServedAddresses.test.ts +65 -0
- 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 +74 -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 +796 -132
- package/dist/acps.d.cts +367 -35
- package/dist/acps.d.ts +367 -35
- package/dist/acps.js +2 -2
- package/dist/{chunk-UJYOBHYA.js → chunk-3L2IVPMI.js} +688 -203
- package/dist/chunk-J23FYAI2.js +174 -0
- package/dist/{chunk-GGI7Y6AW.js → chunk-O2ITEUEU.js} +110 -269
- package/dist/chunk-WJGTPW7K.js +228 -0
- package/dist/{clientTypes-D3aIXmUp.d.cts → clientTypes-IO6b5Um8.d.cts} +61 -21
- package/dist/{clientTypes-5YPPR16m.d.ts → clientTypes-b1pOxOUE.d.ts} +61 -21
- package/dist/core.cjs +3115 -2584
- package/dist/core.d.cts +6 -3
- package/dist/core.d.ts +6 -3
- package/dist/core.js +4 -3
- package/dist/node.cjs +3001 -2470
- 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-CgOslubH.d.cts} +139 -15
- package/dist/{onchain-utils-CUAK8glx.d.ts → onchain-utils-CgOslubH.d.ts} +139 -15
- package/dist/web.cjs +3011 -2487
- 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,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;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { keccak256, size, stringToHex, type Hex } from 'viem';
|
|
3
|
+
import { encodeShareMetadata } from '../encode';
|
|
4
|
+
import { decodeShareAppKey, decodeShareMetadata, decodeShareMetadataFull } from '../decode';
|
|
5
|
+
import { shareAppKey } from '../appKey';
|
|
6
|
+
import type { ShareLabel } from '../schema';
|
|
7
|
+
import { CASES, CONFIDENTIAL_TRANSFER, FUSD, SAMPLE, WORD } from './fixtures';
|
|
8
|
+
|
|
9
|
+
const handles = (count: number): Hex[] => Array.from({ length: count }, () => WORD);
|
|
10
|
+
|
|
11
|
+
describe('share metadata codec (v3)', () => {
|
|
12
|
+
for (const { name, labels, blob } of CASES) {
|
|
13
|
+
it(`encodes ${name}`, () => {
|
|
14
|
+
expect(encodeShareMetadata(labels)).toBe(blob);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it(`decodes ${name}`, () => {
|
|
18
|
+
expect(decodeShareMetadata(blob as Hex, handles(labels.length))).toEqual(labels);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
it('encodes the real sample into the 122-byte blob, function and event selectors in one list', () => {
|
|
23
|
+
const blob = encodeShareMetadata(SAMPLE.labels);
|
|
24
|
+
expect(blob).toBe(SAMPLE.blob);
|
|
25
|
+
expect(size(blob)).toBe(122);
|
|
26
|
+
expect(decodeShareMetadata(blob, SAMPLE.handles)).toEqual(SAMPLE.labels);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('app key', () => {
|
|
30
|
+
const appKey = shareAppKey('explorer.fhenix.io');
|
|
31
|
+
|
|
32
|
+
it('closes the blob with 0x01 and the 32-byte key, after the entries', () => {
|
|
33
|
+
const blob = encodeShareMetadata(SAMPLE.labels, { appKey });
|
|
34
|
+
expect(blob).toBe(SAMPLE.blob + '01' + appKey.slice(2));
|
|
35
|
+
expect(size(blob)).toBe(122 + 33);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('decodes to the same labels, with the key alongside', () => {
|
|
39
|
+
const blob = encodeShareMetadata(SAMPLE.labels, { appKey });
|
|
40
|
+
expect(decodeShareMetadata(blob, SAMPLE.handles)).toEqual(SAMPLE.labels);
|
|
41
|
+
expect(decodeShareMetadataFull(blob, SAMPLE.handles)).toEqual({ labels: SAMPLE.labels, appKey });
|
|
42
|
+
expect(decodeShareAppKey(blob, SAMPLE.handles)).toBe(appKey);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('is absent from a blob encoded without one, and from a share without metadata', () => {
|
|
46
|
+
expect(decodeShareAppKey(SAMPLE.blob, SAMPLE.handles)).toBeUndefined();
|
|
47
|
+
expect(decodeShareAppKey('0x', [])).toBeUndefined();
|
|
48
|
+
expect(decodeShareAppKey(undefined, [])).toBeUndefined();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('fits a share with no handles: a blob of the key alone', () => {
|
|
52
|
+
const blob = encodeShareMetadata([], { appKey });
|
|
53
|
+
expect(blob).toBe('0x03' + '00' + '00' + '00' + '01' + appKey.slice(2));
|
|
54
|
+
expect(decodeShareMetadataFull(blob, [])).toEqual({ labels: [], appKey });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('is the keccak256 of the app name', () => {
|
|
58
|
+
expect(appKey).toBe(keccak256(stringToHex('explorer.fhenix.io')));
|
|
59
|
+
expect(shareAppKey('other.app')).not.toBe(appKey);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('rejects a key that is not 32 bytes, and a trailer that is not a key', () => {
|
|
63
|
+
expect(() => encodeShareMetadata([], { appKey: '0x1234' })).toThrow('an app key must be 32 bytes');
|
|
64
|
+
expect(() => decodeShareMetadata((SAMPLE.blob + '02' + appKey.slice(2)) as Hex, SAMPLE.handles)).toThrow(
|
|
65
|
+
'more entries'
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('lists a transaction once however many labels name it', () => {
|
|
71
|
+
const labels: ShareLabel[] = [
|
|
72
|
+
{ kind: 'event', contract: FUSD, block: 10n, txIndex: 1, logIndex: 2, selector: CONFIDENTIAL_TRANSFER },
|
|
73
|
+
{ kind: 'event', contract: FUSD, block: 10n, txIndex: 1, logIndex: 5, selector: CONFIDENTIAL_TRANSFER },
|
|
74
|
+
];
|
|
75
|
+
// version, 1 contract, 1 selector, 1 transaction, two 6-byte entries
|
|
76
|
+
expect(size(encodeShareMetadata(labels))).toBe(1 + 21 + 5 + 7 + 12);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('rejects', () => {
|
|
80
|
+
it('another version', () => {
|
|
81
|
+
expect(() => decodeShareMetadata('0x02000000ff', handles(1))).toThrow('unsupported version 2');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('a blob that ends in the middle of a field', () => {
|
|
85
|
+
expect(() => decodeShareMetadata(SAMPLE.blob.slice(0, -2) as Hex, SAMPLE.handles)).toThrow('ends in the middle');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('more entries than ctHashes', () => {
|
|
89
|
+
expect(() => decodeShareMetadata(SAMPLE.blob, SAMPLE.handles.slice(1))).toThrow('more entries');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('fewer entries than ctHashes', () => {
|
|
93
|
+
expect(() => decodeShareMetadata(SAMPLE.blob, [...SAMPLE.handles, WORD])).toThrow('ends in the middle');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('a position outside its list', () => {
|
|
97
|
+
// one contract, no selectors: the event entry points at selector 0
|
|
98
|
+
const blob = ('0x03' +
|
|
99
|
+
'01' +
|
|
100
|
+
FUSD.slice(2) +
|
|
101
|
+
'00' +
|
|
102
|
+
'01' +
|
|
103
|
+
'0000000a0001' +
|
|
104
|
+
'01' +
|
|
105
|
+
'00' +
|
|
106
|
+
'00' +
|
|
107
|
+
'0002' +
|
|
108
|
+
'00') as Hex;
|
|
109
|
+
expect(() => decodeShareMetadata(blob, handles(1))).toThrow('selector 0 is outside the selector list');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('an unknown entry kind', () => {
|
|
113
|
+
expect(() => decodeShareMetadata(('0x03000000' + '07') as Hex, handles(1))).toThrow('unknown entry kind 7');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('a log index past uint16 when encoding', () => {
|
|
117
|
+
const label: ShareLabel = {
|
|
118
|
+
kind: 'event',
|
|
119
|
+
contract: FUSD,
|
|
120
|
+
block: 1n,
|
|
121
|
+
txIndex: 0,
|
|
122
|
+
logIndex: 70_000,
|
|
123
|
+
selector: CONFIDENTIAL_TRANSFER,
|
|
124
|
+
};
|
|
125
|
+
expect(() => encodeShareMetadata([label])).toThrow();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('a selector that is not 4 bytes when encoding', () => {
|
|
129
|
+
const label: ShareLabel = {
|
|
130
|
+
kind: 'stored',
|
|
131
|
+
contract: FUSD,
|
|
132
|
+
selector: '0x1234',
|
|
133
|
+
args: [],
|
|
134
|
+
returnWord: 0,
|
|
135
|
+
block: 1n,
|
|
136
|
+
};
|
|
137
|
+
expect(() => encodeShareMetadata([label])).toThrow('a selector must be 4 bytes');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { padHex, parseAbi, toFunctionSelector, type Hex } from 'viem';
|
|
3
|
+
import { describeShareMetadata } from '../describe';
|
|
4
|
+
import { encodeShareMetadata } from '../encode';
|
|
5
|
+
import { BALANCE_OF, CONFIDENTIAL_TRANSFER, FUSD, FUSDY, ISSUER, SAMPLE } from './fixtures';
|
|
6
|
+
|
|
7
|
+
const TOKEN_ABI = parseAbi([
|
|
8
|
+
'function confidentialBalanceOf(address account) view returns (bytes32 balance)',
|
|
9
|
+
'function allowanceAt(address owner, uint256 epoch) view returns (bytes32 previous, bytes32 current)',
|
|
10
|
+
'event ConfidentialTransfer(address indexed from, address indexed to, bytes32 indexed amount)',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
/** A share by the sample issuer carrying that blob. */
|
|
14
|
+
const share = (metadata: Hex | undefined, handles: readonly Hex[] = SAMPLE.handles) => ({
|
|
15
|
+
issuer: ISSUER,
|
|
16
|
+
handles,
|
|
17
|
+
metadata,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('describeShareMetadata', () => {
|
|
21
|
+
it('names the functions and events of the real sample from the ABIs', () => {
|
|
22
|
+
const items = describeShareMetadata(share(SAMPLE.blob), { [FUSD]: TOKEN_ABI, [FUSDY]: TOKEN_ABI })!;
|
|
23
|
+
expect(items).toHaveLength(7);
|
|
24
|
+
|
|
25
|
+
const [balance] = items;
|
|
26
|
+
expect(balance).toMatchObject({
|
|
27
|
+
kind: 'stored',
|
|
28
|
+
index: 0,
|
|
29
|
+
handle: SAMPLE.handles[0],
|
|
30
|
+
contract: FUSD,
|
|
31
|
+
block: 314285327n,
|
|
32
|
+
selector: BALANCE_OF,
|
|
33
|
+
function: { name: 'confidentialBalanceOf', signature: 'confidentialBalanceOf(address)' },
|
|
34
|
+
args: [{ name: 'account', type: 'address', isIssuer: true, value: ISSUER, raw: padHex(ISSUER, { size: 32 }) }],
|
|
35
|
+
output: { name: 'balance', type: 'bytes32' },
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const transfer = items[6];
|
|
39
|
+
expect(transfer).toMatchObject({
|
|
40
|
+
kind: 'event',
|
|
41
|
+
index: 6,
|
|
42
|
+
contract: FUSDY,
|
|
43
|
+
block: 314217333n,
|
|
44
|
+
txIndex: 12,
|
|
45
|
+
logIndex: 115,
|
|
46
|
+
selector: CONFIDENTIAL_TRANSFER,
|
|
47
|
+
event: {
|
|
48
|
+
name: 'ConfidentialTransfer',
|
|
49
|
+
signature: 'ConfidentialTransfer(address,address,bytes32)',
|
|
50
|
+
inputs: [
|
|
51
|
+
{ name: 'from', type: 'address', indexed: true },
|
|
52
|
+
{ name: 'to', type: 'address', indexed: true },
|
|
53
|
+
{ name: 'amount', type: 'bytes32', indexed: true },
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('matches ABI addresses case-insensitively', () => {
|
|
60
|
+
const [item] = describeShareMetadata(share(SAMPLE.blob), {
|
|
61
|
+
[FUSD.toLowerCase() as `0x${string}`]: TOKEN_ABI,
|
|
62
|
+
})!;
|
|
63
|
+
expect(item.kind === 'stored' && item.function?.name).toBe('confidentialBalanceOf');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('decodes literal arguments with their ABI types, and names a later return word', () => {
|
|
67
|
+
const blob = encodeShareMetadata([
|
|
68
|
+
{
|
|
69
|
+
kind: 'stored',
|
|
70
|
+
contract: FUSD,
|
|
71
|
+
selector: toFunctionSelector('allowanceAt(address,uint256)'),
|
|
72
|
+
args: [{ type: 'issuer' }, { type: 'word', value: padHex('0x07', { size: 32 }) }],
|
|
73
|
+
returnWord: 1,
|
|
74
|
+
block: 1n,
|
|
75
|
+
},
|
|
76
|
+
]);
|
|
77
|
+
const [item] = describeShareMetadata(share(blob, [SAMPLE.handles[0]]), { [FUSD]: TOKEN_ABI })!;
|
|
78
|
+
expect(item).toMatchObject({
|
|
79
|
+
kind: 'stored',
|
|
80
|
+
function: { name: 'allowanceAt' },
|
|
81
|
+
args: [
|
|
82
|
+
{ name: 'owner', type: 'address', isIssuer: true, value: ISSUER },
|
|
83
|
+
{ name: 'epoch', type: 'uint256', isIssuer: false, value: 7n },
|
|
84
|
+
],
|
|
85
|
+
output: { name: 'current', type: 'bytes32' },
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('leaves an item raw without an ABI, and never throws for it', () => {
|
|
90
|
+
const items = describeShareMetadata(share(SAMPLE.blob))!;
|
|
91
|
+
expect(items[0]).toMatchObject({ kind: 'stored', function: null, output: null, selector: BALANCE_OF });
|
|
92
|
+
// the issuer comes from the share, ABI or not
|
|
93
|
+
expect(items[0].kind === 'stored' && items[0].args[0]).toMatchObject({
|
|
94
|
+
isIssuer: true,
|
|
95
|
+
raw: padHex(ISSUER, { size: 32 }),
|
|
96
|
+
value: ISSUER,
|
|
97
|
+
});
|
|
98
|
+
expect(items[2]).toMatchObject({ kind: 'event', event: null, selector: CONFIDENTIAL_TRANSFER, logIndex: 30 });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('marks an unlabelled value', () => {
|
|
102
|
+
const blob = encodeShareMetadata([{ kind: 'unlabelled' }]);
|
|
103
|
+
expect(describeShareMetadata(share(blob, [SAMPLE.handles[0]]))).toEqual([
|
|
104
|
+
{ kind: 'unlabelled', index: 0, handle: SAMPLE.handles[0], label: { kind: 'unlabelled' } },
|
|
105
|
+
]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('is null for a share without labels', () => {
|
|
109
|
+
expect(describeShareMetadata(share('0x'))).toBeNull();
|
|
110
|
+
expect(describeShareMetadata(share(undefined))).toBeNull();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { Address, Hex } from 'viem';
|
|
2
|
+
import type { ShareLabel } from '../schema';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shared test fixtures: two confidential tokens on Arbitrum Sepolia, labels with the exact blob
|
|
6
|
+
* each encodes to, and one real share whose labels the chain confirms.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const FUSD: Address = '0x389be1Eb842290F264A6851f49c0149b8035a052';
|
|
10
|
+
export const FUSDY: Address = '0x06a52cf2e0cEAFc7d1f522400500ca7f3df18754';
|
|
11
|
+
export const ISSUER: Address = '0x1F547C0A6088856B9d58F843d9dd8C6e26f4969d';
|
|
12
|
+
/** confidentialBalanceOf(address) */
|
|
13
|
+
export const BALANCE_OF: Hex = '0x344ff101';
|
|
14
|
+
/** ConfidentialTransfer(address indexed from, address indexed to, bytes32 indexed amount), first 4 bytes of topic0 */
|
|
15
|
+
export const CONFIDENTIAL_TRANSFER: Hex = '0x67500e8d';
|
|
16
|
+
export const CONFIDENTIAL_TRANSFER_TOPIC0: Hex = '0x67500e8d0ed826d2194f514dd0d8124f35648ab6e3fb5e6ed867134cffe661e9';
|
|
17
|
+
export const WORD: Hex = '0x00000000000000000000000000000000000000000000000000000000000000aa';
|
|
18
|
+
|
|
19
|
+
const fusd = FUSD.slice(2).toLowerCase();
|
|
20
|
+
const fusdy = FUSDY.slice(2).toLowerCase();
|
|
21
|
+
|
|
22
|
+
/** Label lists with the blob each must encode to, written field by field. */
|
|
23
|
+
export const CASES: { name: string; labels: ShareLabel[]; blob: string }[] = [
|
|
24
|
+
{
|
|
25
|
+
name: 'a stored value read with the issuer as its one argument',
|
|
26
|
+
labels: [
|
|
27
|
+
{
|
|
28
|
+
kind: 'stored',
|
|
29
|
+
contract: FUSD,
|
|
30
|
+
selector: BALANCE_OF,
|
|
31
|
+
args: [{ type: 'issuer' }],
|
|
32
|
+
returnWord: 0,
|
|
33
|
+
block: 0x12ba88b2n,
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
blob: '0x03' + '01' + fusd + '01' + '344ff101' + '00' + ('00' + '00' + '00' + '12ba88b2' + '01'),
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'a stored value read with no arguments',
|
|
40
|
+
labels: [{ kind: 'stored', contract: FUSD, selector: '0x18160ddd', args: [], returnWord: 0, block: 1n }],
|
|
41
|
+
blob: '0x03' + '01' + fusd + '01' + '18160ddd' + '00' + ('00' + '00' + '00' + '00000001' + '00'),
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'a stored value with explicit arguments and a later return word',
|
|
45
|
+
labels: [
|
|
46
|
+
{
|
|
47
|
+
kind: 'stored',
|
|
48
|
+
contract: FUSDY,
|
|
49
|
+
selector: '0x70a08231',
|
|
50
|
+
args: [{ type: 'issuer' }, { type: 'word', value: WORD }],
|
|
51
|
+
returnWord: 1,
|
|
52
|
+
block: 0x12ba88b2n,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
blob:
|
|
56
|
+
'0x03' +
|
|
57
|
+
'01' +
|
|
58
|
+
fusdy +
|
|
59
|
+
'01' +
|
|
60
|
+
'70a08231' +
|
|
61
|
+
'00' +
|
|
62
|
+
('00' + '00' + '00' + '12ba88b2' + 'ff' + '01' + '02' + '01' + '00' + WORD.slice(2)),
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'an event value',
|
|
66
|
+
labels: [
|
|
67
|
+
{ kind: 'event', contract: FUSD, block: 0x12ba9304n, txIndex: 7, logIndex: 300, selector: CONFIDENTIAL_TRANSFER },
|
|
68
|
+
],
|
|
69
|
+
blob: '0x03' + '01' + fusd + '01' + '67500e8d' + '01' + '12ba9304' + '0007' + ('01' + '00' + '00' + '012c' + '00'),
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'two event values of one transaction on two contracts',
|
|
73
|
+
labels: [
|
|
74
|
+
{
|
|
75
|
+
kind: 'event',
|
|
76
|
+
contract: FUSD,
|
|
77
|
+
block: 0x12ba9375n,
|
|
78
|
+
txIndex: 12,
|
|
79
|
+
logIndex: 106,
|
|
80
|
+
selector: CONFIDENTIAL_TRANSFER,
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
kind: 'event',
|
|
84
|
+
contract: FUSDY,
|
|
85
|
+
block: 0x12ba9375n,
|
|
86
|
+
txIndex: 12,
|
|
87
|
+
logIndex: 115,
|
|
88
|
+
selector: CONFIDENTIAL_TRANSFER,
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
blob:
|
|
92
|
+
'0x03' +
|
|
93
|
+
('02' + fusd + fusdy) +
|
|
94
|
+
('01' + '67500e8d') +
|
|
95
|
+
('01' + '12ba9375' + '000c') +
|
|
96
|
+
('01' + '00' + '00' + '006a' + '00') +
|
|
97
|
+
('01' + '01' + '00' + '0073' + '00'),
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'an unlabelled value',
|
|
101
|
+
labels: [{ kind: 'unlabelled' }],
|
|
102
|
+
blob: '0x03' + '00' + '00' + '00' + 'ff',
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* A real share on Arbitrum Sepolia: two balances read at one block, and five transfer amounts from
|
|
108
|
+
* four transactions. The last two are the two values of one vault deposit (the fUSD that left and
|
|
109
|
+
* the fUSDy that was minted), so they name the same transaction. The fUSDy amount is carried by two
|
|
110
|
+
* logs of that transaction (115, the ConfidentialTransfer, and 118); the label names the first.
|
|
111
|
+
*/
|
|
112
|
+
export const SAMPLE = {
|
|
113
|
+
issuer: ISSUER,
|
|
114
|
+
blob: ('0x03' +
|
|
115
|
+
('02' + fusd + fusdy) +
|
|
116
|
+
('02' + '344ff101' + '67500e8d') +
|
|
117
|
+
('04' + '12ba93040005' + '12ba93070008' + '12ba9309000a' + '12ba9375000c') +
|
|
118
|
+
'00000012bb9d0f01' +
|
|
119
|
+
'00010012bb9d0f01' +
|
|
120
|
+
'010000001e01' +
|
|
121
|
+
'010001004101' +
|
|
122
|
+
'010002008a01' +
|
|
123
|
+
'010003006a01' +
|
|
124
|
+
'010103007301') as Hex,
|
|
125
|
+
handles: [
|
|
126
|
+
'0xabaaf65246a7b1763ed4f04486115a27ffbd7abac4a0d10f139cfdac9a910500',
|
|
127
|
+
'0x987a1e51721082556753153555c4f2242a8a592d80ff9551fb471f7427660500',
|
|
128
|
+
'0xbb0c6b1feb27e5c2aec9667ce478006db331205407c754c7a47befa4b2c28500',
|
|
129
|
+
'0x409fec2a564efb61ad75d544ca1933fe2d450e5d9ba847089b15375d1c0d8500',
|
|
130
|
+
'0x39a2c1973989ae9e52dc4d0eed55ceb761d8b83dae6c1a8a66aa9d1defda8500',
|
|
131
|
+
'0xf726d299727dd5c150ce1f31f42447495b3f4f42204d043ab9d21e78aced0500',
|
|
132
|
+
'0x9b0420892380838a0d1fc858c0c2314f9248adb4cacaa766fe869645c1830500',
|
|
133
|
+
] as Hex[],
|
|
134
|
+
labels: [
|
|
135
|
+
{
|
|
136
|
+
kind: 'stored',
|
|
137
|
+
contract: FUSD,
|
|
138
|
+
selector: BALANCE_OF,
|
|
139
|
+
args: [{ type: 'issuer' }],
|
|
140
|
+
returnWord: 0,
|
|
141
|
+
block: 314285327n,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
kind: 'stored',
|
|
145
|
+
contract: FUSDY,
|
|
146
|
+
selector: BALANCE_OF,
|
|
147
|
+
args: [{ type: 'issuer' }],
|
|
148
|
+
returnWord: 0,
|
|
149
|
+
block: 314285327n,
|
|
150
|
+
},
|
|
151
|
+
{ kind: 'event', contract: FUSD, block: 314217220n, txIndex: 5, logIndex: 30, selector: CONFIDENTIAL_TRANSFER },
|
|
152
|
+
{ kind: 'event', contract: FUSD, block: 314217223n, txIndex: 8, logIndex: 65, selector: CONFIDENTIAL_TRANSFER },
|
|
153
|
+
{ kind: 'event', contract: FUSD, block: 314217225n, txIndex: 10, logIndex: 138, selector: CONFIDENTIAL_TRANSFER },
|
|
154
|
+
{ kind: 'event', contract: FUSD, block: 314217333n, txIndex: 12, logIndex: 106, selector: CONFIDENTIAL_TRANSFER },
|
|
155
|
+
{ kind: 'event', contract: FUSDY, block: 314217333n, txIndex: 12, logIndex: 115, selector: CONFIDENTIAL_TRANSFER },
|
|
156
|
+
] as ShareLabel[],
|
|
157
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createPublicClient, http, type PublicClient } from 'viem';
|
|
3
|
+
import { arbitrumSepolia } from 'viem/chains';
|
|
4
|
+
import { TEST_PRIVATE_KEY } from '@cofhe/test-setup';
|
|
5
|
+
import { decodeShareMetadata } from '../decode';
|
|
6
|
+
import { verifyShareLabels } from '../verify';
|
|
7
|
+
import { SAMPLE } from './fixtures';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The real sample against Arbitrum Sepolia, read from the public RPC. A live-chain suite like
|
|
11
|
+
* `core/test/decrypt.test.ts`: it runs where TEST_PRIVATE_KEY is set (the key itself is not used),
|
|
12
|
+
* so `pnpm test` stays offline and `pnpm test:live` covers it.
|
|
13
|
+
*/
|
|
14
|
+
describe.skipIf(!TEST_PRIVATE_KEY)('share metadata against Arbitrum Sepolia', () => {
|
|
15
|
+
const publicClient = createPublicClient({
|
|
16
|
+
chain: arbitrumSepolia,
|
|
17
|
+
transport: http(arbitrumSepolia.rpcUrls.default.http[0]),
|
|
18
|
+
}) as PublicClient;
|
|
19
|
+
|
|
20
|
+
it('confirms every event label of the sample at its log index', async () => {
|
|
21
|
+
const labels = decodeShareMetadata(SAMPLE.blob, SAMPLE.handles);
|
|
22
|
+
const checks = await verifyShareLabels({
|
|
23
|
+
labels,
|
|
24
|
+
handles: SAMPLE.handles,
|
|
25
|
+
issuer: SAMPLE.issuer,
|
|
26
|
+
publicClient,
|
|
27
|
+
verify: 'events',
|
|
28
|
+
});
|
|
29
|
+
expect(checks).toEqual(['skipped', 'skipped', 'verified', 'verified', 'verified', 'verified', 'verified']);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('rejects the fUSDy amount labelled at the deposit’s other log that carries it', async () => {
|
|
33
|
+
const labels = decodeShareMetadata(SAMPLE.blob, SAMPLE.handles);
|
|
34
|
+
const last = labels[6];
|
|
35
|
+
if (last.kind !== 'event') throw new Error('expected an event label');
|
|
36
|
+
const [check] = await verifyShareLabels({
|
|
37
|
+
labels: [{ ...last, logIndex: 118 }],
|
|
38
|
+
handles: [SAMPLE.handles[6]],
|
|
39
|
+
issuer: SAMPLE.issuer,
|
|
40
|
+
publicClient,
|
|
41
|
+
});
|
|
42
|
+
// log 118 carries the ctHash but is not a ConfidentialTransfer: the selector check fails it
|
|
43
|
+
expect(check).toBe('mismatch');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('checks the stored values while a node still has the state of their block', async () => {
|
|
47
|
+
const labels = decodeShareMetadata(SAMPLE.blob, SAMPLE.handles);
|
|
48
|
+
const checks = await verifyShareLabels({
|
|
49
|
+
labels: labels.slice(0, 2),
|
|
50
|
+
handles: SAMPLE.handles.slice(0, 2),
|
|
51
|
+
issuer: SAMPLE.issuer,
|
|
52
|
+
publicClient,
|
|
53
|
+
});
|
|
54
|
+
for (const check of checks) expect(['verified', 'unverifiable']).toContain(check);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { padHex, parseAbi, toEventSelector, toFunctionSelector, type Log } from 'viem';
|
|
3
|
+
import { encodeShareMetadata } from '../encode';
|
|
4
|
+
import { ERC7984_ABI } from '../erc7984';
|
|
5
|
+
import { eventLogLabel, viewFunctionLabel } from '../presets';
|
|
6
|
+
import {
|
|
7
|
+
BALANCE_OF,
|
|
8
|
+
CONFIDENTIAL_TRANSFER,
|
|
9
|
+
CONFIDENTIAL_TRANSFER_TOPIC0,
|
|
10
|
+
FUSD,
|
|
11
|
+
FUSDY,
|
|
12
|
+
ISSUER,
|
|
13
|
+
SAMPLE,
|
|
14
|
+
WORD,
|
|
15
|
+
} from './fixtures';
|
|
16
|
+
|
|
17
|
+
const ISSUER_WORD = padHex(ISSUER, { size: 32 }).toLowerCase();
|
|
18
|
+
|
|
19
|
+
describe('label presets', () => {
|
|
20
|
+
it('labels a log with its position and event selector', () => {
|
|
21
|
+
const log = {
|
|
22
|
+
address: FUSD,
|
|
23
|
+
blockNumber: 314217220n,
|
|
24
|
+
transactionIndex: 5,
|
|
25
|
+
logIndex: 30,
|
|
26
|
+
topics: [CONFIDENTIAL_TRANSFER_TOPIC0],
|
|
27
|
+
} as unknown as Log;
|
|
28
|
+
expect(eventLogLabel(log)).toEqual(SAMPLE.labels[2]);
|
|
29
|
+
expect(eventLogLabel(log).selector).toBe(CONFIDENTIAL_TRANSFER);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('refuses a pending or anonymous log', () => {
|
|
33
|
+
const base = {
|
|
34
|
+
address: FUSD,
|
|
35
|
+
blockNumber: 1n,
|
|
36
|
+
transactionIndex: 0,
|
|
37
|
+
logIndex: 0,
|
|
38
|
+
topics: [CONFIDENTIAL_TRANSFER_TOPIC0],
|
|
39
|
+
};
|
|
40
|
+
expect(() => eventLogLabel({ ...base, blockNumber: null } as unknown as Log)).toThrow('pending');
|
|
41
|
+
expect(() => eventLogLabel({ ...base, topics: [] } as unknown as Log)).toThrow('anonymous');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('labels the ERC-7984 balance read from its ABI; the issuer argument compresses to the real share', () => {
|
|
45
|
+
const [balanceOf, transfer] = ERC7984_ABI;
|
|
46
|
+
expect(toFunctionSelector(balanceOf)).toBe(BALANCE_OF);
|
|
47
|
+
expect(toEventSelector(transfer)).toBe(CONFIDENTIAL_TRANSFER_TOPIC0);
|
|
48
|
+
|
|
49
|
+
const label = viewFunctionLabel({
|
|
50
|
+
contract: FUSD,
|
|
51
|
+
abi: ERC7984_ABI,
|
|
52
|
+
functionName: 'confidentialBalanceOf',
|
|
53
|
+
args: [ISSUER],
|
|
54
|
+
block: 314285327n,
|
|
55
|
+
});
|
|
56
|
+
expect(label).toEqual({ ...SAMPLE.labels[0], args: [{ type: 'word', value: ISSUER_WORD }] });
|
|
57
|
+
// encoded for a share by ISSUER, the address becomes "the issuer": the sample's own bytes
|
|
58
|
+
expect(encodeShareMetadata([label], { issuer: ISSUER })).toBe(encodeShareMetadata([SAMPLE.labels[0]]));
|
|
59
|
+
// for anyone else's share it stays a literal address
|
|
60
|
+
expect(encodeShareMetadata([label], { issuer: FUSDY })).toBe(encodeShareMetadata([label]));
|
|
61
|
+
expect(encodeShareMetadata([label])).not.toBe(encodeShareMetadata([SAMPLE.labels[0]]));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('labels any view function from its ABI, encoding literal arguments to words', () => {
|
|
65
|
+
const abi = parseAbi([
|
|
66
|
+
'function balanceOfAt(address account, uint256 snapshot) view returns (uint256 total, bytes32 encrypted)',
|
|
67
|
+
'function totalSupply() view returns (bytes32)',
|
|
68
|
+
]);
|
|
69
|
+
expect(
|
|
70
|
+
viewFunctionLabel({
|
|
71
|
+
contract: FUSDY,
|
|
72
|
+
abi,
|
|
73
|
+
functionName: 'balanceOfAt',
|
|
74
|
+
args: [ISSUER, 0xaan],
|
|
75
|
+
returnWord: 1,
|
|
76
|
+
block: 0x12ba88b2n,
|
|
77
|
+
})
|
|
78
|
+
).toEqual({
|
|
79
|
+
kind: 'stored',
|
|
80
|
+
contract: FUSDY,
|
|
81
|
+
selector: toFunctionSelector(abi[0]),
|
|
82
|
+
args: [
|
|
83
|
+
{ type: 'word', value: ISSUER_WORD },
|
|
84
|
+
{ type: 'word', value: WORD },
|
|
85
|
+
],
|
|
86
|
+
returnWord: 1,
|
|
87
|
+
block: 0x12ba88b2n,
|
|
88
|
+
});
|
|
89
|
+
expect(viewFunctionLabel({ contract: FUSD, abi, functionName: 'totalSupply', block: 1n })).toEqual({
|
|
90
|
+
kind: 'stored',
|
|
91
|
+
contract: FUSD,
|
|
92
|
+
selector: '0x18160ddd',
|
|
93
|
+
args: [],
|
|
94
|
+
returnWord: 0,
|
|
95
|
+
block: 1n,
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('refuses what the layout cannot carry or the ABI does not have', () => {
|
|
100
|
+
const abi = parseAbi([
|
|
101
|
+
'function nameOf(string key) view returns (bytes32)',
|
|
102
|
+
'function balanceOf(address account) view returns (bytes32)',
|
|
103
|
+
'function balanceOf(address account, uint256 id) view returns (bytes32)',
|
|
104
|
+
'function mint(address to) returns (bytes32)',
|
|
105
|
+
]);
|
|
106
|
+
const base = { contract: FUSD, abi, block: 1n } as const;
|
|
107
|
+
expect(() => viewFunctionLabel({ ...base, functionName: 'nameOf', args: ['x'] })).toThrow('static 32-byte word');
|
|
108
|
+
expect(() => viewFunctionLabel({ ...base, functionName: 'balanceOf', args: [] as never })).toThrow(
|
|
109
|
+
'no single overload'
|
|
110
|
+
);
|
|
111
|
+
expect(() =>
|
|
112
|
+
viewFunctionLabel({ ...base, abi: parseAbi(['function f(address a) view returns (bytes32)']), functionName: 'f' })
|
|
113
|
+
).toThrow('takes 1 arguments, 0 given');
|
|
114
|
+
expect(() => viewFunctionLabel({ ...base, functionName: 'missing' as never })).toThrow('no function missing');
|
|
115
|
+
expect(() => viewFunctionLabel({ ...base, functionName: 'mint' as never, args: [ISSUER] as never })).toThrow(
|
|
116
|
+
'not a view function'
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
});
|