@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
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { type EncryptableItem, FheTypes } from '../types.js';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type VerifyBatchResult,
|
|
4
|
+
MAX_UINT8,
|
|
5
|
+
MAX_UINT16,
|
|
6
|
+
MAX_UINT32,
|
|
7
|
+
MAX_UINT64,
|
|
8
|
+
MAX_UINT128,
|
|
9
|
+
MAX_UINT160,
|
|
10
|
+
} from './zkPackProveVerify.js';
|
|
11
|
+
import { toBigIntOrThrow, validateBigIntInRange } from '../utils.js';
|
|
3
12
|
import { createWalletClient, http, encodePacked, keccak256, type PublicClient, type WalletClient } from 'viem';
|
|
4
13
|
import { MockZkVerifierAbi } from './MockZkVerifierAbi.js';
|
|
5
14
|
import { hardhat } from 'viem/chains';
|
|
@@ -19,7 +28,9 @@ function createMockZkVerifierSigner() {
|
|
|
19
28
|
});
|
|
20
29
|
}
|
|
21
30
|
/**
|
|
22
|
-
* The mocks don't use a tfhe builder, so we check the encryptable bits here to
|
|
31
|
+
* The mocks don't use a tfhe builder, so we check the encryptable bits and value ranges here to
|
|
32
|
+
* preserve parity with `zkPack`. Without the range check, MockCoFHE masks an out-of-range value
|
|
33
|
+
* with the utype mask (e.g. uint8(256) -> 0) instead of rejecting it like the production path.
|
|
23
34
|
*/
|
|
24
35
|
export async function cofheMocksCheckEncryptableBits(items: EncryptableItem[]): Promise<void> {
|
|
25
36
|
let totalBits = 0;
|
|
@@ -30,22 +41,27 @@ export async function cofheMocksCheckEncryptableBits(items: EncryptableItem[]):
|
|
|
30
41
|
break;
|
|
31
42
|
}
|
|
32
43
|
case FheTypes.Uint8: {
|
|
44
|
+
validateBigIntInRange(toBigIntOrThrow(item.data), MAX_UINT8);
|
|
33
45
|
totalBits += 8;
|
|
34
46
|
break;
|
|
35
47
|
}
|
|
36
48
|
case FheTypes.Uint16: {
|
|
49
|
+
validateBigIntInRange(toBigIntOrThrow(item.data), MAX_UINT16);
|
|
37
50
|
totalBits += 16;
|
|
38
51
|
break;
|
|
39
52
|
}
|
|
40
53
|
case FheTypes.Uint32: {
|
|
54
|
+
validateBigIntInRange(toBigIntOrThrow(item.data), MAX_UINT32);
|
|
41
55
|
totalBits += 32;
|
|
42
56
|
break;
|
|
43
57
|
}
|
|
44
58
|
case FheTypes.Uint64: {
|
|
59
|
+
validateBigIntInRange(toBigIntOrThrow(item.data), MAX_UINT64);
|
|
45
60
|
totalBits += 64;
|
|
46
61
|
break;
|
|
47
62
|
}
|
|
48
63
|
case FheTypes.Uint128: {
|
|
64
|
+
validateBigIntInRange(toBigIntOrThrow(item.data), MAX_UINT128);
|
|
49
65
|
totalBits += 128;
|
|
50
66
|
break;
|
|
51
67
|
}
|
|
@@ -55,6 +71,7 @@ export async function cofheMocksCheckEncryptableBits(items: EncryptableItem[]):
|
|
|
55
71
|
// break;
|
|
56
72
|
// }
|
|
57
73
|
case FheTypes.Uint160: {
|
|
74
|
+
validateBigIntInRange(toBigIntOrThrow(item.data), MAX_UINT160);
|
|
58
75
|
totalBits += 160;
|
|
59
76
|
break;
|
|
60
77
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
1
|
+
import { beforeEach, describe, it, expect, vi } from 'vitest';
|
|
2
2
|
import { acps } from '../acps.js';
|
|
3
|
+
import { clearTaskManagerAddressCache } from '../taskManager.js';
|
|
3
4
|
|
|
4
5
|
type ScopeOpts = {
|
|
5
6
|
revokerData?: number;
|
|
@@ -12,7 +13,10 @@ const opts = (o: ScopeOpts): ScopeOpts => o;
|
|
|
12
13
|
|
|
13
14
|
const CHAIN = 31337;
|
|
14
15
|
const REVOKER = '0x00000000000000000000000000000000000000aa' as `0x${string}`;
|
|
16
|
+
const ACL_REVOKER = '0x00000000000000000000000000000000000000cc' as `0x${string}`;
|
|
15
17
|
const CONTRACT_A = '0x00000000000000000000000000000000000000bb' as `0x${string}`;
|
|
18
|
+
const ACL = '0x00000000000000000000000000000000000000dd' as `0x${string}`;
|
|
19
|
+
const TM = '0x0000000000000000000000000000000000005000' as `0x${string}`;
|
|
16
20
|
|
|
17
21
|
const acpConfig = {
|
|
18
22
|
defaultRevoker: { [CHAIN]: REVOKER },
|
|
@@ -20,33 +24,118 @@ const acpConfig = {
|
|
|
20
24
|
};
|
|
21
25
|
|
|
22
26
|
describe('applyACPDefaults', () => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
acps.clearAclServedAddresses();
|
|
29
|
+
clearTaskManagerAddressCache();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('uses the exact chain block timestamp for an ACL-served revoker', async () => {
|
|
33
|
+
const latestBlockTimestamp = 1_700_000_000;
|
|
34
|
+
const publicClient = {
|
|
35
|
+
readContract: vi.fn(({ functionName }: { functionName: string }) => {
|
|
36
|
+
if (functionName === 'getTm') return Promise.resolve(TM);
|
|
37
|
+
if (functionName === 'acl') return Promise.resolve(ACL);
|
|
38
|
+
if (functionName === 'defaultRevokerContract') return Promise.resolve(ACL_REVOKER);
|
|
39
|
+
return Promise.reject(new Error(`unexpected ACL read: ${functionName}`));
|
|
40
|
+
}),
|
|
41
|
+
getBlock: vi.fn().mockResolvedValue({ timestamp: BigInt(latestBlockTimestamp) }),
|
|
42
|
+
} as any;
|
|
43
|
+
|
|
44
|
+
const result = await acps.applyACPDefaultsFromChain(opts({}), undefined, publicClient, CHAIN);
|
|
45
|
+
|
|
46
|
+
expect(publicClient.getBlock).toHaveBeenCalledWith();
|
|
47
|
+
expect(result.revokerContract).toBe(ACL_REVOKER);
|
|
48
|
+
expect(result.revokerData).toBe(latestBlockTimestamp);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('prefers configured revoker over ACL but still uses the latest block timestamp', async () => {
|
|
52
|
+
const latestBlockTimestamp = 1_700_000_001;
|
|
53
|
+
const publicClient = {
|
|
54
|
+
readContract: vi.fn(({ functionName }: { functionName: string }) => {
|
|
55
|
+
if (functionName === 'getTm') return Promise.resolve(TM);
|
|
56
|
+
if (functionName === 'acl') return Promise.resolve(ACL);
|
|
57
|
+
if (functionName === 'defaultRevokerContract') return Promise.resolve(ACL_REVOKER);
|
|
58
|
+
return Promise.reject(new Error(`unexpected ACL read: ${functionName}`));
|
|
59
|
+
}),
|
|
60
|
+
getBlock: vi.fn().mockResolvedValue({ timestamp: BigInt(latestBlockTimestamp) }),
|
|
61
|
+
} as any;
|
|
62
|
+
|
|
63
|
+
const result = await acps.applyACPDefaultsFromChain(opts({}), acpConfig, publicClient, CHAIN);
|
|
64
|
+
|
|
65
|
+
expect(publicClient.readContract).not.toHaveBeenCalled();
|
|
66
|
+
expect(publicClient.getBlock).toHaveBeenCalledWith();
|
|
26
67
|
expect(result.revokerContract).toBe(REVOKER);
|
|
27
|
-
expect(result.revokerData).
|
|
28
|
-
|
|
68
|
+
expect(result.revokerData).toBe(latestBlockTimestamp);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('avoids getBlock for explicit revoker options', async () => {
|
|
72
|
+
const publicClient = {
|
|
73
|
+
readContract: vi.fn(),
|
|
74
|
+
getBlock: vi.fn(),
|
|
75
|
+
} as any;
|
|
76
|
+
|
|
77
|
+
const result = await acps.applyACPDefaultsFromChain(
|
|
78
|
+
opts({ revokerData: 42, revokerContract: CONTRACT_A }),
|
|
79
|
+
acpConfig,
|
|
80
|
+
publicClient,
|
|
81
|
+
CHAIN
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
expect(publicClient.readContract).not.toHaveBeenCalled();
|
|
85
|
+
expect(publicClient.getBlock).not.toHaveBeenCalled();
|
|
86
|
+
expect(result).toMatchObject({ revokerData: 42, revokerContract: CONTRACT_A });
|
|
29
87
|
});
|
|
30
88
|
|
|
31
|
-
it('
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
89
|
+
it('avoids getBlock when neither config nor ACL provide a revoker', async () => {
|
|
90
|
+
const publicClient = {
|
|
91
|
+
readContract: vi.fn().mockRejectedValue(new Error('no ACL deployment')),
|
|
92
|
+
getBlock: vi.fn(),
|
|
93
|
+
} as any;
|
|
94
|
+
|
|
95
|
+
const result = await acps.applyACPDefaultsFromChain(opts({}), undefined, publicClient, CHAIN);
|
|
96
|
+
|
|
97
|
+
expect(publicClient.getBlock).not.toHaveBeenCalled();
|
|
98
|
+
expect(result).toEqual({});
|
|
35
99
|
});
|
|
36
100
|
|
|
37
|
-
it('
|
|
38
|
-
const
|
|
101
|
+
it('rejects a block timestamp outside JavaScript safe integer range', async () => {
|
|
102
|
+
const publicClient = {
|
|
103
|
+
readContract: vi.fn(({ functionName }: { functionName: string }) => {
|
|
104
|
+
if (functionName === 'getTm') return Promise.resolve(TM);
|
|
105
|
+
if (functionName === 'acl') return Promise.resolve(ACL);
|
|
106
|
+
if (functionName === 'defaultRevokerContract') return Promise.resolve(ACL_REVOKER);
|
|
107
|
+
return Promise.reject(new Error(`unexpected ACL read: ${functionName}`));
|
|
108
|
+
}),
|
|
109
|
+
getBlock: vi.fn().mockResolvedValue({ timestamp: BigInt(Number.MAX_SAFE_INTEGER) + 1n }),
|
|
110
|
+
} as any;
|
|
111
|
+
|
|
112
|
+
await expect(acps.applyACPDefaultsFromChain(opts({}), undefined, publicClient, CHAIN)).rejects.toThrow(
|
|
113
|
+
/ACP revokerData cannot represent the chain block timestamp .* as a safe integer/
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('injects default contract scopes when no scope options given', async () => {
|
|
118
|
+
const result = await acps.applyACPDefaultsFromChain(opts({}), acpConfig, noAclClient(), CHAIN);
|
|
39
119
|
expect(result.contracts).toEqual([CONTRACT_A]);
|
|
40
120
|
});
|
|
41
121
|
|
|
42
|
-
it('does not inject scopes when the caller provides any scope option (incl. explicit scope)', () => {
|
|
43
|
-
|
|
44
|
-
expect(
|
|
45
|
-
expect(
|
|
122
|
+
it('does not inject scopes when the caller provides any scope option (incl. explicit scope)', async () => {
|
|
123
|
+
const apply = (o: ScopeOpts) => acps.applyACPDefaultsFromChain(o, acpConfig, noAclClient(), CHAIN);
|
|
124
|
+
expect((await apply(opts({ scope: 0 }))).contracts).toBeUndefined();
|
|
125
|
+
expect((await apply(opts({ handles: [1n] }))).contracts).toBeUndefined();
|
|
126
|
+
expect((await apply(opts({ contracts: [] }))).contracts).toEqual([]);
|
|
46
127
|
});
|
|
47
128
|
|
|
48
|
-
it('no-op for chains without defaults and for missing config', () => {
|
|
49
|
-
expect(acps.
|
|
50
|
-
expect(acps.
|
|
129
|
+
it('no-op for chains without defaults and for missing config', async () => {
|
|
130
|
+
expect(await acps.applyACPDefaultsFromChain(opts({}), acpConfig, noAclClient(), 999)).toEqual({});
|
|
131
|
+
expect(await acps.applyACPDefaultsFromChain(opts({}), undefined, noAclClient(), CHAIN)).toEqual({});
|
|
51
132
|
});
|
|
52
133
|
});
|
|
134
|
+
|
|
135
|
+
/** A chain with no ACL deployment: the ACL lookup serves no revoker. */
|
|
136
|
+
function noAclClient() {
|
|
137
|
+
return {
|
|
138
|
+
readContract: vi.fn().mockRejectedValue(new Error('no ACL deployment')),
|
|
139
|
+
getBlock: vi.fn().mockResolvedValue({ timestamp: 1_700_000_000n }),
|
|
140
|
+
} as any;
|
|
141
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment happy-dom
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
5
|
+
|
|
6
|
+
// Stub the on-chain EIP-712 domain read so the test needs no RPC (same approach as acps.test.ts).
|
|
7
|
+
vi.mock('../../acps/onchain-utils.js', async (importOriginal) => ({
|
|
8
|
+
...(await importOriginal<typeof import('../../acps/onchain-utils.js')>()),
|
|
9
|
+
getAclEIP712Domain: async () => ({
|
|
10
|
+
name: 'ACL',
|
|
11
|
+
version: '2',
|
|
12
|
+
chainId: 421614,
|
|
13
|
+
verifyingContract: '0x1111111111111111111111111111111111111111' as `0x${string}`,
|
|
14
|
+
}),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
import { custom, createWalletClient, type PublicClient, type WalletClient } from 'viem';
|
|
18
|
+
import { arbitrumSepolia } from 'viem/chains';
|
|
19
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
20
|
+
import { acpStore } from '@/acps';
|
|
21
|
+
import { acps } from '../acps.js';
|
|
22
|
+
|
|
23
|
+
const chainId = 421614;
|
|
24
|
+
// Offline clients: the wallet signs typed data locally, and any RPC call fails the test.
|
|
25
|
+
const noRpc = custom({
|
|
26
|
+
request: async ({ method }) => {
|
|
27
|
+
throw new Error(`unexpected RPC call: ${method}`);
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
const publicClient = { getChainId: async () => chainId } as unknown as PublicClient;
|
|
31
|
+
const walletClient: WalletClient = createWalletClient({
|
|
32
|
+
chain: arbitrumSepolia,
|
|
33
|
+
transport: noRpc,
|
|
34
|
+
account: privateKeyToAccount('0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'),
|
|
35
|
+
});
|
|
36
|
+
const checksummed = walletClient.account!.address; // 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
|
|
37
|
+
const lowercase = checksummed.toLowerCase();
|
|
38
|
+
|
|
39
|
+
describe('ACP store account keys are case-insensitive', () => {
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
acpStore.store.setState({ acps: {}, activeACPHash: {} });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('finds the active ACP when the account is passed in lowercase', async () => {
|
|
45
|
+
const created = await acps.createSelf({ issuer: checksummed }, publicClient, walletClient);
|
|
46
|
+
|
|
47
|
+
expect(acps.getActiveACP(chainId, checksummed)?.hash).toBe(created.hash);
|
|
48
|
+
expect(acps.getActiveACP(chainId, lowercase)?.hash).toBe(created.hash);
|
|
49
|
+
expect(acps.getACP(chainId, lowercase, created.hash)?.hash).toBe(created.hash);
|
|
50
|
+
expect(Object.keys(acps.getACPs(chainId, lowercase))).toEqual([created.hash]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('getOrCreateSelfACP with a lowercase account reuses the stored ACP instead of signing a new one', async () => {
|
|
54
|
+
const signSpy = vi.spyOn(walletClient, 'signTypedData');
|
|
55
|
+
|
|
56
|
+
const first = await acps.getOrCreateSelfACP(publicClient, walletClient, chainId, lowercase);
|
|
57
|
+
const second = await acps.getOrCreateSelfACP(publicClient, walletClient, chainId, lowercase);
|
|
58
|
+
|
|
59
|
+
expect(second.hash).toBe(first.hash);
|
|
60
|
+
expect(signSpy).toHaveBeenCalledTimes(1);
|
|
61
|
+
expect(Object.keys(acps.getACPs(chainId, checksummed))).toHaveLength(1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('keeps one bucket per account whatever case it is written and read in', async () => {
|
|
65
|
+
const created = await acps.createSelf({ issuer: checksummed }, publicClient, walletClient);
|
|
66
|
+
acpStore.store.setState({ acps: {}, activeACPHash: {} });
|
|
67
|
+
|
|
68
|
+
// Stored under the lowercase spelling (e.g. a wallet client built from a lowercase address)...
|
|
69
|
+
acpStore.setACP(chainId, lowercase, created);
|
|
70
|
+
acpStore.setActiveACPHash(chainId, lowercase, created.hash);
|
|
71
|
+
// ...still found with the checksummed one, and a write in that case reuses the same bucket.
|
|
72
|
+
expect(acpStore.getActiveACP(chainId, checksummed)?.hash).toBe(created.hash);
|
|
73
|
+
acpStore.setACP(chainId, checksummed, created);
|
|
74
|
+
expect(Object.keys(acpStore.store.getState().acps[chainId])).toEqual([lowercase]);
|
|
75
|
+
|
|
76
|
+
acpStore.removeACP(chainId, checksummed, created.hash);
|
|
77
|
+
expect(acpStore.getActiveACPHash(chainId, lowercase)).toBeUndefined();
|
|
78
|
+
expect(acpStore.getACP(chainId, lowercase, created.hash)).toBeUndefined();
|
|
79
|
+
});
|
|
80
|
+
});
|
package/core/test/acps.test.ts
CHANGED
|
@@ -143,7 +143,8 @@ describe('Core ACPs Tests', () => {
|
|
|
143
143
|
// Import the acp as Alice (recipient)
|
|
144
144
|
const acp = await acps.importShared(acpJson, publicClient, aliceWalletClient);
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
// the issuer's name is not taken over: the acp is named after its issuer
|
|
147
|
+
expect(acp.name).toBe(`Shared by ${bobAddress.slice(0, 6)}…${bobAddress.slice(-4)}`);
|
|
147
148
|
expect(acp.type).toBe('recipient');
|
|
148
149
|
expect(acp.issuer).toBe(bobAddress);
|
|
149
150
|
expect(acp.recipient).toBe(aliceAddress);
|
|
@@ -190,7 +191,7 @@ describe('Core ACPs Tests', () => {
|
|
|
190
191
|
|
|
191
192
|
const toIncomingShare = (sharing: SharingACP): IncomingShare => {
|
|
192
193
|
const { sealingKey: _sealingKey, recipientSignature: _recipientSignature, ...pub } = ACPUtils.getPublic(sharing);
|
|
193
|
-
return { ...pub, shareId: ZERO_SHARE_ID };
|
|
194
|
+
return { ...pub, shareId: ZERO_SHARE_ID, metadata: '0x' };
|
|
194
195
|
};
|
|
195
196
|
|
|
196
197
|
it('importShared activates the imported acp by default', async () => {
|
|
@@ -231,6 +232,40 @@ describe('Core ACPs Tests', () => {
|
|
|
231
232
|
expect(acps.getActiveACPHash(chainId, aliceAddress)).toBe(imported.hash);
|
|
232
233
|
});
|
|
233
234
|
|
|
235
|
+
it('importFromChain keeps the share metadata on the stored recipient acp', async () => {
|
|
236
|
+
const { sharing } = await setUp();
|
|
237
|
+
const imported = await acps.importFromChain(
|
|
238
|
+
{ ...toIncomingShare(sharing), metadata: '0x03aa' },
|
|
239
|
+
publicClient,
|
|
240
|
+
aliceWalletClient
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
expect(imported.metadata).toBe('0x03aa');
|
|
244
|
+
expect(acps.getACPs(chainId, aliceAddress)?.[imported.hash]?.metadata).toBe('0x03aa');
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('importShared and importFromChain take the recipient name as an option', async () => {
|
|
248
|
+
const { sharing } = await setUp();
|
|
249
|
+
const fromJson = await acps.importShared(ACPUtils.export(sharing), publicClient, aliceWalletClient, {
|
|
250
|
+
name: 'From Bob (JSON)',
|
|
251
|
+
});
|
|
252
|
+
const fromChain = await acps.importFromChain(toIncomingShare(sharing), publicClient, aliceWalletClient, {
|
|
253
|
+
name: 'From Bob (chain)',
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
expect(fromJson.name).toBe('From Bob (JSON)');
|
|
257
|
+
expect(fromChain.name).toBe('From Bob (chain)');
|
|
258
|
+
// the same share, so the second import replaced the first in the store
|
|
259
|
+
expect(acps.getACPs(chainId, aliceAddress)?.[fromChain.hash]?.name).toBe('From Bob (chain)');
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('importFromChain of a share without metadata leaves none on the acp', async () => {
|
|
263
|
+
const { sharing } = await setUp();
|
|
264
|
+
const imported = await acps.importFromChain(toIncomingShare(sharing), publicClient, aliceWalletClient);
|
|
265
|
+
|
|
266
|
+
expect(imported.metadata).toBeUndefined();
|
|
267
|
+
});
|
|
268
|
+
|
|
234
269
|
it('importFromChain with activate: false stores the acp and keeps the active one', async () => {
|
|
235
270
|
const { self, sharing } = await setUp();
|
|
236
271
|
const imported = await acps.importFromChain(toIncomingShare(sharing), publicClient, aliceWalletClient, {
|
|
@@ -618,7 +653,7 @@ describe('Core ACPs Tests', () => {
|
|
|
618
653
|
const exported = acps.export(acp);
|
|
619
654
|
const parsed = JSON.parse(exported);
|
|
620
655
|
|
|
621
|
-
expect(parsed
|
|
656
|
+
expect(parsed).not.toHaveProperty('name');
|
|
622
657
|
expect(parsed.type).toBe('sharing');
|
|
623
658
|
expect(parsed.issuer).toBe(bobAddress);
|
|
624
659
|
expect(parsed.recipient).toBe(aliceAddress);
|
|
@@ -47,6 +47,14 @@ describe('mapApiErrorCodeToCofheErrorCode', () => {
|
|
|
47
47
|
CofheErrorCode.SealOutputFailed
|
|
48
48
|
);
|
|
49
49
|
});
|
|
50
|
+
|
|
51
|
+
it('falls back for a code that only exists on Object.prototype', () => {
|
|
52
|
+
for (const inherited of ['toString', 'constructor', 'valueOf', 'hasOwnProperty']) {
|
|
53
|
+
expect(mapApiErrorCodeToCofheErrorCode(inherited, CofheErrorCode.DecryptFailed)).toBe(
|
|
54
|
+
CofheErrorCode.DecryptFailed
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
50
58
|
});
|
|
51
59
|
|
|
52
60
|
describe('parseApiErrorResponseBody', () => {
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
2
|
+
import { type PublicClient, type WalletClient } from 'viem';
|
|
3
|
+
import { createCofheClientBase } from '../client';
|
|
4
|
+
import { type CofheClient } from '../clientTypes';
|
|
5
|
+
import { createCofheConfigBase, type CofheInputConfig } from '../config';
|
|
6
|
+
import { acps } from '../acps';
|
|
7
|
+
import { shareAppKey } from '../../acps/share-metadata';
|
|
8
|
+
|
|
9
|
+
vi.mock('../keyStore', () => ({
|
|
10
|
+
createKeysStore: vi.fn(() => ({
|
|
11
|
+
rehydrateKeysStore: vi.fn().mockResolvedValue(undefined),
|
|
12
|
+
getFheKey: vi.fn(),
|
|
13
|
+
getCrs: vi.fn(),
|
|
14
|
+
setFheKey: vi.fn(),
|
|
15
|
+
setCrs: vi.fn(),
|
|
16
|
+
clearKeysStorage: vi.fn(),
|
|
17
|
+
store: {} as any,
|
|
18
|
+
})),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
const CHAIN_ID = 11155111;
|
|
22
|
+
const ACCOUNT = '0x1234567890123456789012345678901234567890';
|
|
23
|
+
const REGISTRY = '0x00000000000000000000000000000000000000e0';
|
|
24
|
+
const APP = 'explorer.fhenix.io';
|
|
25
|
+
const APP_KEY = shareAppKey(APP);
|
|
26
|
+
|
|
27
|
+
const publicClient = { getChainId: vi.fn().mockResolvedValue(CHAIN_ID) } as unknown as PublicClient;
|
|
28
|
+
const walletClient = {
|
|
29
|
+
getAddresses: vi.fn().mockResolvedValue([ACCOUNT]),
|
|
30
|
+
account: { address: ACCOUNT },
|
|
31
|
+
} as unknown as WalletClient;
|
|
32
|
+
|
|
33
|
+
/** A connected client; `acp.sharingRegistry` is set so the on-chain calls need no ACL lookup. */
|
|
34
|
+
const connectedClient = async (acp: CofheInputConfig['acp']): Promise<CofheClient> => {
|
|
35
|
+
const client = createCofheClientBase({
|
|
36
|
+
config: createCofheConfigBase({ supportedChains: [], acp: { sharingRegistry: { [CHAIN_ID]: REGISTRY }, ...acp } }),
|
|
37
|
+
zkBuilderAndCrsGenerator: {} as any,
|
|
38
|
+
tfhePublicKeyDeserializer: {} as any,
|
|
39
|
+
compactPkeCrsDeserializer: {} as any,
|
|
40
|
+
initTfhe: () => Promise.resolve(false),
|
|
41
|
+
});
|
|
42
|
+
await client.connect(publicClient, walletClient);
|
|
43
|
+
return client;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const sharingOptions = { issuer: ACCOUNT, recipient: '0x00000000000000000000000000000000000000a1' };
|
|
47
|
+
|
|
48
|
+
describe('the app key from config.acp.app', () => {
|
|
49
|
+
const spies = {
|
|
50
|
+
defaults: vi.spyOn(acps, 'applyACPDefaultsFromChain').mockImplementation(async (options) => options),
|
|
51
|
+
createSharing: vi.spyOn(acps, 'createSharing').mockResolvedValue({} as any),
|
|
52
|
+
getOrCreate: vi.spyOn(acps, 'getOrCreateSharingACP').mockResolvedValue({} as any),
|
|
53
|
+
shareOnChain: vi.spyOn(acps, 'shareOnChain').mockResolvedValue({ txHash: '0x', shareId: '0x' }),
|
|
54
|
+
getIncoming: vi.spyOn(acps, 'getIncomingShares').mockResolvedValue([]),
|
|
55
|
+
};
|
|
56
|
+
afterEach(() => Object.values(spies).forEach((spy) => spy.mockClear()));
|
|
57
|
+
|
|
58
|
+
it('goes on every share the client creates', async () => {
|
|
59
|
+
const client = await connectedClient({ app: APP });
|
|
60
|
+
await client.acp.createSharing(sharingOptions);
|
|
61
|
+
expect(spies.createSharing.mock.calls[0][0]).toMatchObject({ ...sharingOptions, appKey: APP_KEY });
|
|
62
|
+
|
|
63
|
+
await client.acp.getOrCreateSharingACP(sharingOptions);
|
|
64
|
+
expect(spies.getOrCreate.mock.calls[0][2]).toMatchObject({ ...sharingOptions, appKey: APP_KEY });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('goes with labels posted by shareOnChain', async () => {
|
|
68
|
+
const client = await connectedClient({ app: APP });
|
|
69
|
+
await client.acp.shareOnChain({} as any, { labels: [] });
|
|
70
|
+
expect(spies.shareOnChain.mock.calls[0][3]).toEqual({ labels: [], appKey: APP_KEY });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('filters the inbox', async () => {
|
|
74
|
+
const client = await connectedClient({ app: APP });
|
|
75
|
+
await client.acp.getIncomingShares();
|
|
76
|
+
expect(spies.getIncoming).toHaveBeenCalledWith(publicClient, REGISTRY, ACCOUNT, { appKey: APP_KEY });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('is the keccak256 of the app name, and an explicit appKey wins over it', async () => {
|
|
80
|
+
const client = await connectedClient({ app: APP });
|
|
81
|
+
const other = shareAppKey('other.app');
|
|
82
|
+
await client.acp.createSharing({ ...sharingOptions, appKey: other });
|
|
83
|
+
expect(spies.createSharing.mock.calls[0][0]).toMatchObject({ appKey: other });
|
|
84
|
+
expect(APP_KEY).toMatch(/^0x[0-9a-f]{64}$/);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('is absent without an app: shares carry no key and every share is listed', async () => {
|
|
88
|
+
const client = await connectedClient({});
|
|
89
|
+
await client.acp.createSharing(sharingOptions);
|
|
90
|
+
expect(spies.createSharing.mock.calls[0][0]).not.toHaveProperty('appKey');
|
|
91
|
+
await client.acp.getIncomingShares();
|
|
92
|
+
expect(spies.getIncoming).toHaveBeenCalledWith(publicClient, REGISTRY, ACCOUNT, {});
|
|
93
|
+
});
|
|
94
|
+
});
|
package/core/test/config.test.ts
CHANGED
|
@@ -62,6 +62,14 @@ describe('createCofheConfigBase', () => {
|
|
|
62
62
|
expectValidConfigItem('environment', 'react', 'react');
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
+
it('acp.app', () => {
|
|
66
|
+
expectInvalidConfigItem('acp.app', '');
|
|
67
|
+
expectInvalidConfigItem('acp.app', 123);
|
|
68
|
+
|
|
69
|
+
expectValidConfigItem('acp.app', 'explorer.fhenix.io', 'explorer.fhenix.io');
|
|
70
|
+
expectValidConfigItem('acp.app', undefined, undefined);
|
|
71
|
+
});
|
|
72
|
+
|
|
65
73
|
it('supportedChains', () => {
|
|
66
74
|
expectInvalidConfigItem('supportedChains', {});
|
|
67
75
|
expectInvalidConfigItem('supportedChains', 'not-an-array');
|
|
@@ -1,13 +1,26 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { type PublicClient, type WalletClient, createPublicClient, createWalletClient, http } from 'viem';
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { type PublicClient, type WalletClient, createPublicClient, createWalletClient, http, zeroAddress } from 'viem';
|
|
3
3
|
import { privateKeyToAccount } from 'viem/accounts';
|
|
4
4
|
import { arbitrumSepolia } from 'viem/chains';
|
|
5
|
+
import { ACPUtils, acpStore, type ACP } from '@/acps';
|
|
5
6
|
import { DecryptForTxBuilder } from '../decrypt/decryptForTxBuilder.js';
|
|
6
7
|
import { DecryptForViewBuilder } from '../decrypt/decryptForViewBuilder.js';
|
|
8
|
+
import { tnDecryptV2 } from '../decrypt/tnDecryptV2';
|
|
9
|
+
import { tnSealOutputV2 } from '../decrypt/tnSealOutputV2';
|
|
7
10
|
import { createCofheConfigBase, type CofheConfig } from '../config.js';
|
|
8
11
|
import { CofheErrorCode } from '../error.js';
|
|
9
12
|
import { FheTypes } from '../types.js';
|
|
10
13
|
|
|
14
|
+
// Stub the threshold network calls so tests can assert whether a request was dispatched
|
|
15
|
+
vi.mock('../decrypt/tnDecryptV2', async (importOriginal) => ({
|
|
16
|
+
...(await importOriginal<typeof import('../decrypt/tnDecryptV2')>()),
|
|
17
|
+
tnDecryptV2: vi.fn(),
|
|
18
|
+
}));
|
|
19
|
+
vi.mock('../decrypt/tnSealOutputV2', async (importOriginal) => ({
|
|
20
|
+
...(await importOriginal<typeof import('../decrypt/tnSealOutputV2')>()),
|
|
21
|
+
tnSealOutputV2: vi.fn(),
|
|
22
|
+
}));
|
|
23
|
+
|
|
11
24
|
const TEST_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';
|
|
12
25
|
const account = privateKeyToAccount(TEST_PRIVATE_KEY);
|
|
13
26
|
const TEST_CHAIN_ID = 421614;
|
|
@@ -416,3 +429,98 @@ describe('DecryptForViewBuilder', () => {
|
|
|
416
429
|
});
|
|
417
430
|
});
|
|
418
431
|
});
|
|
432
|
+
|
|
433
|
+
// ---------------------------------------------------------------------------
|
|
434
|
+
// Expired acp (regression, issue #272)
|
|
435
|
+
//
|
|
436
|
+
// The getOrCreate* half of #272 is covered in acps.test.ts. These cover the decrypt path:
|
|
437
|
+
// execute() must reject an expired acp (ACPUtils.validate) before any request is dispatched
|
|
438
|
+
// to the threshold network, however the acp was selected.
|
|
439
|
+
// ---------------------------------------------------------------------------
|
|
440
|
+
|
|
441
|
+
describe('execute – expired acp (regression, issue #272)', () => {
|
|
442
|
+
type Selection = 'active acp' | 'acp hash' | 'acp object';
|
|
443
|
+
const selections: Selection[] = ['active acp', 'acp hash', 'acp object'];
|
|
444
|
+
|
|
445
|
+
const now = () => Math.floor(Date.now() / 1000);
|
|
446
|
+
|
|
447
|
+
// Built by hand rather than with ACPUtils.sign, which reads the ACL's EIP-712 domain over RPC.
|
|
448
|
+
// validate() only checks that a signature is present, and the threshold network is stubbed.
|
|
449
|
+
function createSignedSelfACP(expiration: number): ACP {
|
|
450
|
+
return {
|
|
451
|
+
...ACPUtils.createSelf({ name: 'Test Self ACP', issuer: account.address, expiration }),
|
|
452
|
+
issuerSignature: `0x${'11'.repeat(65)}`,
|
|
453
|
+
_signedDomain: { name: 'ACL', version: '1', chainId: TEST_CHAIN_ID, verifyingContract: zeroAddress },
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Stores the acp and makes it the active acp for the test chain + account
|
|
458
|
+
function seedACP(acp: ACP) {
|
|
459
|
+
acpStore.setACP(TEST_CHAIN_ID, account.address, acp);
|
|
460
|
+
acpStore.setActiveACPHash(TEST_CHAIN_ID, account.address, acp.hash);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function selectTxACP(selection: Selection, acp: ACP) {
|
|
464
|
+
const builder = createTxBuilder();
|
|
465
|
+
if (selection === 'acp hash') return builder.withACP(acp.hash);
|
|
466
|
+
if (selection === 'acp object') return builder.withACP(acp);
|
|
467
|
+
return builder.withACP();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function selectViewACP(selection: Selection, acp: ACP) {
|
|
471
|
+
const builder = createViewBuilder(FheTypes.Uint32);
|
|
472
|
+
if (selection === 'acp hash') return builder.withACP(acp.hash);
|
|
473
|
+
if (selection === 'acp object') return builder.withACP(acp);
|
|
474
|
+
return builder.withACP();
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
beforeEach(() => {
|
|
478
|
+
acpStore.store.setState({ acps: {}, activeACPHash: {} });
|
|
479
|
+
vi.mocked(tnDecryptV2).mockReset();
|
|
480
|
+
vi.mocked(tnSealOutputV2).mockReset();
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
afterEach(() => {
|
|
484
|
+
acpStore.store.setState({ acps: {}, activeACPHash: {} });
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
describe('decryptForTx', () => {
|
|
488
|
+
it.each(selections)('rejects an expired acp selected via %s without dispatching', async (selection) => {
|
|
489
|
+
const acp = createSignedSelfACP(now() - 3600);
|
|
490
|
+
seedACP(acp);
|
|
491
|
+
|
|
492
|
+
await expect(selectTxACP(selection, acp).execute()).rejects.toThrow('ACP is expired');
|
|
493
|
+
expect(tnDecryptV2).not.toHaveBeenCalled();
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it('dispatches a valid acp (control for the expired cases)', async () => {
|
|
497
|
+
vi.mocked(tnDecryptV2).mockResolvedValue({ decryptedValue: 7n, signature: '0x1234' });
|
|
498
|
+
const acp = createSignedSelfACP(now() + 3600);
|
|
499
|
+
seedACP(acp);
|
|
500
|
+
|
|
501
|
+
const result = await selectTxACP('active acp', acp).execute();
|
|
502
|
+
|
|
503
|
+
expect(result.decryptedValue).toBe(7n);
|
|
504
|
+
expect(tnDecryptV2).toHaveBeenCalledOnce();
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
describe('decryptForView', () => {
|
|
509
|
+
it.each(selections)('rejects an expired acp selected via %s without dispatching', async (selection) => {
|
|
510
|
+
const acp = createSignedSelfACP(now() - 3600);
|
|
511
|
+
seedACP(acp);
|
|
512
|
+
|
|
513
|
+
await expect(selectViewACP(selection, acp).execute()).rejects.toThrow('ACP is expired');
|
|
514
|
+
expect(tnSealOutputV2).not.toHaveBeenCalled();
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it('dispatches a valid acp (control for the expired cases)', async () => {
|
|
518
|
+
vi.mocked(tnSealOutputV2).mockRejectedValue(new Error('stubbed threshold network'));
|
|
519
|
+
const acp = createSignedSelfACP(now() + 3600);
|
|
520
|
+
seedACP(acp);
|
|
521
|
+
|
|
522
|
+
await expect(selectViewACP('active acp', acp).execute()).rejects.toThrow('stubbed threshold network');
|
|
523
|
+
expect(tnSealOutputV2).toHaveBeenCalledOnce();
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
});
|