web3ql-client 1.2.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/README.md +66 -0
- package/contracts/PublicKeyRegistry.sol +87 -0
- package/dist/src/access.d.ts +176 -0
- package/dist/src/access.d.ts.map +1 -0
- package/dist/src/access.js +283 -0
- package/dist/src/access.js.map +1 -0
- package/dist/src/batch.d.ts +107 -0
- package/dist/src/batch.d.ts.map +1 -0
- package/dist/src/batch.js +188 -0
- package/dist/src/batch.js.map +1 -0
- package/dist/src/cli.d.ts +40 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +361 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/constraints.d.ts +126 -0
- package/dist/src/constraints.d.ts.map +1 -0
- package/dist/src/constraints.js +192 -0
- package/dist/src/constraints.js.map +1 -0
- package/dist/src/crypto.d.ts +118 -0
- package/dist/src/crypto.d.ts.map +1 -0
- package/dist/src/crypto.js +192 -0
- package/dist/src/crypto.js.map +1 -0
- package/dist/src/factory-client.d.ts +106 -0
- package/dist/src/factory-client.d.ts.map +1 -0
- package/dist/src/factory-client.js +202 -0
- package/dist/src/factory-client.js.map +1 -0
- package/dist/src/index-cache.d.ts +156 -0
- package/dist/src/index-cache.d.ts.map +1 -0
- package/dist/src/index-cache.js +265 -0
- package/dist/src/index-cache.js.map +1 -0
- package/dist/src/index.d.ts +60 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +60 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/migrations.d.ts +114 -0
- package/dist/src/migrations.d.ts.map +1 -0
- package/dist/src/migrations.js +173 -0
- package/dist/src/migrations.js.map +1 -0
- package/dist/src/model.d.ts +198 -0
- package/dist/src/model.d.ts.map +1 -0
- package/dist/src/model.js +379 -0
- package/dist/src/model.js.map +1 -0
- package/dist/src/query.d.ts +155 -0
- package/dist/src/query.d.ts.map +1 -0
- package/dist/src/query.js +386 -0
- package/dist/src/query.js.map +1 -0
- package/dist/src/registry.d.ts +45 -0
- package/dist/src/registry.d.ts.map +1 -0
- package/dist/src/registry.js +80 -0
- package/dist/src/registry.js.map +1 -0
- package/dist/src/schema-manager.d.ts +109 -0
- package/dist/src/schema-manager.d.ts.map +1 -0
- package/dist/src/schema-manager.js +259 -0
- package/dist/src/schema-manager.js.map +1 -0
- package/dist/src/table-client.d.ts +156 -0
- package/dist/src/table-client.d.ts.map +1 -0
- package/dist/src/table-client.js +292 -0
- package/dist/src/table-client.js.map +1 -0
- package/dist/src/typed-table.d.ts +159 -0
- package/dist/src/typed-table.d.ts.map +1 -0
- package/dist/src/typed-table.js +246 -0
- package/dist/src/typed-table.js.map +1 -0
- package/dist/src/types.d.ts +48 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +222 -0
- package/dist/src/types.js.map +1 -0
- package/keyManager.js +337 -0
- package/package.json +38 -0
- package/src/access.ts +421 -0
- package/src/batch.ts +259 -0
- package/src/cli.ts +349 -0
- package/src/constraints.ts +283 -0
- package/src/crypto.ts +239 -0
- package/src/factory-client.ts +237 -0
- package/src/index-cache.ts +351 -0
- package/src/index.ts +171 -0
- package/src/migrations.ts +215 -0
- package/src/model.ts +538 -0
- package/src/query.ts +508 -0
- package/src/registry.ts +100 -0
- package/src/schema-manager.ts +301 -0
- package/src/table-client.ts +393 -0
- package/src/typed-table.ts +340 -0
- package/src/types.ts +284 -0
- package/tsconfig.json +22 -0
- package/walletUtils.js +204 -0
package/src/crypto.ts
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file crypto.ts
|
|
3
|
+
* @notice Off-chain encryption layer for Web3QL.
|
|
4
|
+
*
|
|
5
|
+
* Security model:
|
|
6
|
+
* ─────────────────────────────────────────────────────────────
|
|
7
|
+
* • One random 32-byte symmetric key is generated per record.
|
|
8
|
+
* • Record data is encrypted with that key using NaCl secretbox
|
|
9
|
+
* (XSalsa20-Poly1305, 256-bit key, 192-bit nonce, 128-bit MAC).
|
|
10
|
+
* • The symmetric key is encrypted separately for each authorised
|
|
11
|
+
* user using NaCl box (X25519-XSalsa20-Poly1305, ECDH key agreement).
|
|
12
|
+
* • Only the encrypted blobs ever touch the chain — the symmetric key
|
|
13
|
+
* and plaintext never leave the client device.
|
|
14
|
+
*
|
|
15
|
+
* Key derivation from Ethereum wallet:
|
|
16
|
+
* ─────────────────────────────────────────────────────────────
|
|
17
|
+
* X25519 private key = SHA-256(Ethereum private key bytes)
|
|
18
|
+
* X25519 public key = scalar multiplication (computed by nacl)
|
|
19
|
+
*
|
|
20
|
+
* This means no separate key management — the wallet IS the
|
|
21
|
+
* encryption identity. Losing the wallet private key = losing
|
|
22
|
+
* access to encrypted records (same as losing any crypto key).
|
|
23
|
+
*
|
|
24
|
+
* Wire format (self-describing, no external framing needed):
|
|
25
|
+
* ─────────────────────────────────────────────────────────────
|
|
26
|
+
* secretbox blob: [ 24-byte nonce | encrypted payload ]
|
|
27
|
+
* box blob: [ 24-byte nonce | encrypted payload ]
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import nacl from 'tweetnacl';
|
|
31
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
32
|
+
import { hexToBytes } from '@noble/hashes/utils';
|
|
33
|
+
import type { Signer } from 'ethers';
|
|
34
|
+
|
|
35
|
+
// ─────────────────────────────────────────────────────────────
|
|
36
|
+
// Types
|
|
37
|
+
// ─────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
export interface EncryptionKeypair {
|
|
40
|
+
/** X25519 public key — safe to publish on-chain */
|
|
41
|
+
publicKey : Uint8Array; // 32 bytes
|
|
42
|
+
/** X25519 private key — never leaves the client */
|
|
43
|
+
privateKey : Uint8Array; // 32 bytes
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─────────────────────────────────────────────────────────────
|
|
47
|
+
// Keypair derivation
|
|
48
|
+
// ─────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/** Message signed by the wallet to derive the X25519 keypair.
|
|
51
|
+
* Must be identical to the constant in cloud/lib/browser-crypto.ts so
|
|
52
|
+
* browser and SDK produce the same keypair for the same wallet.
|
|
53
|
+
*/
|
|
54
|
+
export const KEY_DERIVATION_MESSAGE = 'Web3QL encryption key derivation v1';
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Derive an X25519 keypair from an ethers Signer by signing a fixed
|
|
58
|
+
* derivation message. Because Ethereum personal_sign (RFC 6979) is
|
|
59
|
+
* deterministic, the same wallet always produces the same keypair.
|
|
60
|
+
*
|
|
61
|
+
* ✅ Use this when both SDK and browser clients need to share the same
|
|
62
|
+
* encryption identity — the resulting pubkey matches what the browser
|
|
63
|
+
* derives via `deriveKeypairFromSignature` in browser-crypto.ts.
|
|
64
|
+
*
|
|
65
|
+
* @param signer Any ethers v6 Signer (e.g. `new ethers.Wallet(privKey)`).
|
|
66
|
+
*/
|
|
67
|
+
export async function deriveKeypairFromWallet(
|
|
68
|
+
signer: Signer,
|
|
69
|
+
): Promise<EncryptionKeypair> {
|
|
70
|
+
const sig = await signer.signMessage(KEY_DERIVATION_MESSAGE);
|
|
71
|
+
const sigHex = sig.startsWith('0x') ? sig.slice(2) : sig;
|
|
72
|
+
const seed = sha256(hexToBytes(sigHex));
|
|
73
|
+
const kp = nacl.box.keyPair.fromSecretKey(seed);
|
|
74
|
+
return { publicKey: kp.publicKey, privateKey: kp.secretKey };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Derive an X25519 encryption keypair from an Ethereum private key.
|
|
79
|
+
*
|
|
80
|
+
* @deprecated Prefer `deriveKeypairFromWallet(signer)` — it produces a
|
|
81
|
+
* keypair that is compatible with the Web3QL browser Explorer and any
|
|
82
|
+
* other client that uses wallet-signature derivation. Raw-private-key
|
|
83
|
+
* derivation generates a DIFFERENT keypair, so records written with this
|
|
84
|
+
* function cannot be decrypted in the browser (and vice-versa).
|
|
85
|
+
*
|
|
86
|
+
* @param ethPrivateKey Hex string, with or without "0x" prefix.
|
|
87
|
+
*/
|
|
88
|
+
export function deriveKeypair(ethPrivateKey: string): EncryptionKeypair {
|
|
89
|
+
const stripped = ethPrivateKey.startsWith('0x')
|
|
90
|
+
? ethPrivateKey.slice(2)
|
|
91
|
+
: ethPrivateKey;
|
|
92
|
+
const seed = sha256(hexToBytes(stripped));
|
|
93
|
+
const kp = nacl.box.keyPair.fromSecretKey(seed);
|
|
94
|
+
return { publicKey: kp.publicKey, privateKey: kp.secretKey };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Derive ONLY the public key (useful when you only have a signed
|
|
99
|
+
* message and want to register — not when you have the private key).
|
|
100
|
+
* Exposed for completeness; normally call deriveKeypair() directly.
|
|
101
|
+
*/
|
|
102
|
+
export function publicKeyFromPrivate(ethPrivateKey: string): Uint8Array {
|
|
103
|
+
return deriveKeypair(ethPrivateKey).publicKey;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ─────────────────────────────────────────────────────────────
|
|
107
|
+
// Symmetric encryption (record data)
|
|
108
|
+
// ─────────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Generate a random 32-byte symmetric key for a new record.
|
|
112
|
+
* Call this once per write — never reuse across records.
|
|
113
|
+
*/
|
|
114
|
+
export function generateSymmetricKey(): Uint8Array {
|
|
115
|
+
return nacl.randomBytes(nacl.secretbox.keyLength); // 32 bytes
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Encrypt plaintext with a symmetric key.
|
|
120
|
+
* Wire format: [ 24-byte nonce | MAC+ciphertext ]
|
|
121
|
+
*/
|
|
122
|
+
export function encryptData(
|
|
123
|
+
plaintext : Uint8Array,
|
|
124
|
+
symmetricKey : Uint8Array,
|
|
125
|
+
): Uint8Array {
|
|
126
|
+
const nonce = nacl.randomBytes(nacl.secretbox.nonceLength);
|
|
127
|
+
const encrypted = nacl.secretbox(plaintext, nonce, symmetricKey);
|
|
128
|
+
if (!encrypted) throw new Error('encryptData: nacl.secretbox returned null');
|
|
129
|
+
return concat(nonce, encrypted);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Decrypt a secretbox blob (nonce||ciphertext) with a symmetric key.
|
|
134
|
+
* Throws if the MAC check fails (data tampered or wrong key).
|
|
135
|
+
*/
|
|
136
|
+
export function decryptData(
|
|
137
|
+
blob : Uint8Array,
|
|
138
|
+
symmetricKey : Uint8Array,
|
|
139
|
+
): Uint8Array {
|
|
140
|
+
const nonce = blob.subarray(0, nacl.secretbox.nonceLength);
|
|
141
|
+
const ciphertext = blob.subarray(nacl.secretbox.nonceLength);
|
|
142
|
+
const plaintext = nacl.secretbox.open(ciphertext, nonce, symmetricKey);
|
|
143
|
+
if (!plaintext) throw new Error('decryptData: authentication failed — wrong key or tampered data');
|
|
144
|
+
return plaintext;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ─────────────────────────────────────────────────────────────
|
|
148
|
+
// Asymmetric key wrapping (per-recipient symmetric key copies)
|
|
149
|
+
// ─────────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Encrypt a symmetric key for a specific recipient.
|
|
153
|
+
*
|
|
154
|
+
* The caller (sharer) needs their own X25519 private key and the
|
|
155
|
+
* recipient's X25519 public key (registered on-chain in PublicKeyRegistry).
|
|
156
|
+
*
|
|
157
|
+
* Wire format: [ 24-byte nonce | MAC+ciphertext ]
|
|
158
|
+
*/
|
|
159
|
+
export function encryptKeyForRecipient(
|
|
160
|
+
symmetricKey : Uint8Array,
|
|
161
|
+
recipientPublicKey : Uint8Array,
|
|
162
|
+
senderPrivateKey : Uint8Array,
|
|
163
|
+
): Uint8Array {
|
|
164
|
+
const nonce = nacl.randomBytes(nacl.box.nonceLength);
|
|
165
|
+
const encrypted = nacl.box(symmetricKey, nonce, recipientPublicKey, senderPrivateKey);
|
|
166
|
+
if (!encrypted) throw new Error('encryptKeyForRecipient: nacl.box returned null');
|
|
167
|
+
return concat(nonce, encrypted);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Decrypt a symmetric key that was encrypted for us.
|
|
172
|
+
* Throws if authentication fails.
|
|
173
|
+
*/
|
|
174
|
+
export function decryptKeyFromSender(
|
|
175
|
+
blob : Uint8Array,
|
|
176
|
+
senderPublicKey : Uint8Array,
|
|
177
|
+
recipientPrivKey : Uint8Array,
|
|
178
|
+
): Uint8Array {
|
|
179
|
+
const nonce = blob.subarray(0, nacl.box.nonceLength);
|
|
180
|
+
const ciphertext = blob.subarray(nacl.box.nonceLength);
|
|
181
|
+
const key = nacl.box.open(ciphertext, nonce, senderPublicKey, recipientPrivKey);
|
|
182
|
+
if (!key) throw new Error('decryptKeyFromSender: authentication failed — wrong key or tampered data');
|
|
183
|
+
return key;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Encrypt the symmetric key to yourself — used on the initial write
|
|
188
|
+
* so the owner can always recover their own key.
|
|
189
|
+
*/
|
|
190
|
+
export function encryptKeyForSelf(
|
|
191
|
+
symmetricKey : Uint8Array,
|
|
192
|
+
keypair : EncryptionKeypair,
|
|
193
|
+
): Uint8Array {
|
|
194
|
+
return encryptKeyForRecipient(symmetricKey, keypair.publicKey, keypair.privateKey);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Decrypt a symmetric key that was encrypted to yourself.
|
|
199
|
+
*/
|
|
200
|
+
export function decryptKeyForSelf(
|
|
201
|
+
blob : Uint8Array,
|
|
202
|
+
keypair : EncryptionKeypair,
|
|
203
|
+
): Uint8Array {
|
|
204
|
+
return decryptKeyFromSender(blob, keypair.publicKey, keypair.privateKey);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ─────────────────────────────────────────────────────────────
|
|
208
|
+
// Encode/decode public key for on-chain storage (bytes32)
|
|
209
|
+
// ─────────────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Encode a 32-byte X25519 public key as a 0x-prefixed hex string
|
|
213
|
+
* suitable for passing to PublicKeyRegistry.register().
|
|
214
|
+
*/
|
|
215
|
+
export function publicKeyToHex(pubKey: Uint8Array): string {
|
|
216
|
+
if (pubKey.length !== 32) throw new Error('publicKeyToHex: expected 32 bytes');
|
|
217
|
+
return '0x' + Buffer.from(pubKey).toString('hex');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Decode a 0x-prefixed hex bytes32 from the registry back to Uint8Array.
|
|
222
|
+
*/
|
|
223
|
+
export function hexToPublicKey(hex: string): Uint8Array {
|
|
224
|
+
const stripped = hex.startsWith('0x') ? hex.slice(2) : hex;
|
|
225
|
+
const bytes = hexToBytes(stripped);
|
|
226
|
+
if (bytes.length !== 32) throw new Error('hexToPublicKey: expected 32 hex bytes');
|
|
227
|
+
return bytes;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ─────────────────────────────────────────────────────────────
|
|
231
|
+
// Utility
|
|
232
|
+
// ─────────────────────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
function concat(a: Uint8Array, b: Uint8Array): Uint8Array {
|
|
235
|
+
const out = new Uint8Array(a.length + b.length);
|
|
236
|
+
out.set(a, 0);
|
|
237
|
+
out.set(b, a.length);
|
|
238
|
+
return out;
|
|
239
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file factory-client.ts
|
|
3
|
+
* @notice High-level client for the Web3QL Factory + Database contracts.
|
|
4
|
+
*
|
|
5
|
+
* Entry point for every Web3QL app:
|
|
6
|
+
* ─────────────────────────────────────────────────────────────
|
|
7
|
+
* const web3ql = new Web3QLClient(factoryAddress, signer, keypair, registryAddress);
|
|
8
|
+
*
|
|
9
|
+
* // Register your public key once (one-time, ~40k gas)
|
|
10
|
+
* await web3ql.register();
|
|
11
|
+
*
|
|
12
|
+
* // Create a personal database (or get existing)
|
|
13
|
+
* const db = await web3ql.getOrCreateDatabase();
|
|
14
|
+
*
|
|
15
|
+
* // Create a table
|
|
16
|
+
* const tableAddr = await db.createTable('users', schemaBytes);
|
|
17
|
+
*
|
|
18
|
+
* // Get an encrypted client for that table
|
|
19
|
+
* const users = db.table(tableAddr);
|
|
20
|
+
*
|
|
21
|
+
* // Write (auto-encrypts)
|
|
22
|
+
* await users.writeRaw(users.deriveKey('users', 1n), '{"name":"Alice"}');
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { ethers } from 'ethers';
|
|
26
|
+
import { EncryptedTableClient } from './table-client.js';
|
|
27
|
+
import { PublicKeyRegistryClient } from './registry.js';
|
|
28
|
+
import type { EncryptionKeypair } from './crypto.js';
|
|
29
|
+
|
|
30
|
+
// ─────────────────────────────────────────────────────────────
|
|
31
|
+
// ABIs
|
|
32
|
+
// ─────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
const FACTORY_ABI = [
|
|
35
|
+
'function createDatabase(string calldata name) external returns (address)',
|
|
36
|
+
'function getUserDatabases(address user) external view returns (address[] memory)',
|
|
37
|
+
'function databaseImplementation() external view returns (address)',
|
|
38
|
+
'function tableImplementation() external view returns (address)',
|
|
39
|
+
'function databaseCount() external view returns (uint256)',
|
|
40
|
+
'function removeDatabase(address db) external',
|
|
41
|
+
'event DatabaseCreated(address indexed owner, address indexed db, uint256 indexed index)',
|
|
42
|
+
'event DatabaseRemoved(address indexed owner, address indexed db)',
|
|
43
|
+
] as const;
|
|
44
|
+
|
|
45
|
+
const DATABASE_ABI = [
|
|
46
|
+
'function createTable(string calldata name, bytes calldata schemaBytes) external returns (address)',
|
|
47
|
+
'function getTable(string calldata name) external view returns (address)',
|
|
48
|
+
'function listTables() external view returns (string[] memory)',
|
|
49
|
+
'event TableCreated(string name, address indexed tableContract, address indexed owner)',
|
|
50
|
+
] as const;
|
|
51
|
+
|
|
52
|
+
// ─────────────────────────────────────────────────────────────
|
|
53
|
+
// DatabaseClient
|
|
54
|
+
// ─────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
export class DatabaseClient {
|
|
57
|
+
readonly address : string;
|
|
58
|
+
private contract : ethers.Contract;
|
|
59
|
+
private signer : ethers.Signer;
|
|
60
|
+
private keypair : EncryptionKeypair;
|
|
61
|
+
|
|
62
|
+
constructor(
|
|
63
|
+
address : string,
|
|
64
|
+
signer : ethers.Signer,
|
|
65
|
+
keypair : EncryptionKeypair,
|
|
66
|
+
) {
|
|
67
|
+
this.address = address;
|
|
68
|
+
this.signer = signer;
|
|
69
|
+
this.keypair = keypair;
|
|
70
|
+
this.contract = new ethers.Contract(address, DATABASE_ABI, signer);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Create a new encrypted table inside this database.
|
|
75
|
+
* @param name Human-readable table name (matches your schema).
|
|
76
|
+
* @param schemaBytes ABI-encoded schema from @web3ql/compiler's compileSchema().
|
|
77
|
+
* @returns Address of the deployed table proxy.
|
|
78
|
+
*/
|
|
79
|
+
async createTable(name: string, schemaBytes: string): Promise<string> {
|
|
80
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
81
|
+
const tx = await (this.contract as any).createTable(name, schemaBytes) as { wait(): Promise<ethers.TransactionReceipt> };
|
|
82
|
+
const receipt = await tx.wait();
|
|
83
|
+
|
|
84
|
+
const iface = new ethers.Interface(DATABASE_ABI);
|
|
85
|
+
for (const log of receipt.logs) {
|
|
86
|
+
try {
|
|
87
|
+
const parsed = iface.parseLog(log);
|
|
88
|
+
if (parsed?.name === 'TableCreated') {
|
|
89
|
+
return parsed.args['tableContract'] as string;
|
|
90
|
+
}
|
|
91
|
+
} catch { /* skip non-matching logs */ }
|
|
92
|
+
}
|
|
93
|
+
throw new Error('DatabaseClient.createTable: TableCreated event not found in logs');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Get the address of an existing table by name.
|
|
98
|
+
* Returns ethers.ZeroAddress if not found.
|
|
99
|
+
*/
|
|
100
|
+
async getTable(name: string): Promise<string> {
|
|
101
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
102
|
+
return (this.contract as any).getTable(name) as Promise<string>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* List all table names in this database.
|
|
107
|
+
*/
|
|
108
|
+
async listTables(): Promise<string[]> {
|
|
109
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
110
|
+
return (this.contract as any).listTables() as Promise<string[]>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Drop (remove from registry) a table by name.
|
|
115
|
+
* Records inside the table are NOT purged by this call — they remain
|
|
116
|
+
* as unreachable ciphertext. Use SchemaManager.dropTable() first to
|
|
117
|
+
* bulk-delete records if you want storage refunds.
|
|
118
|
+
*/
|
|
119
|
+
async dropTable(name: string): Promise<ethers.TransactionReceipt> {
|
|
120
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
121
|
+
const tx = await (this.contract as any).dropTable(name) as { wait(): Promise<ethers.TransactionReceipt> };
|
|
122
|
+
return tx.wait();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get an EncryptedTableClient for a specific table address.
|
|
127
|
+
* Use this after createTable() or getTable() to read/write records.
|
|
128
|
+
*/
|
|
129
|
+
table(tableAddress: string): EncryptedTableClient {
|
|
130
|
+
return new EncryptedTableClient(tableAddress, this.signer, this.keypair);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ─────────────────────────────────────────────────────────────
|
|
135
|
+
// Web3QLClient (top-level entry point)
|
|
136
|
+
// ─────────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
export class Web3QLClient {
|
|
139
|
+
private factory : ethers.Contract;
|
|
140
|
+
private signer : ethers.Signer;
|
|
141
|
+
private keypair : EncryptionKeypair;
|
|
142
|
+
readonly registry: PublicKeyRegistryClient;
|
|
143
|
+
|
|
144
|
+
constructor(
|
|
145
|
+
factoryAddress : string,
|
|
146
|
+
signer : ethers.Signer,
|
|
147
|
+
keypair : EncryptionKeypair,
|
|
148
|
+
registryAddress : string,
|
|
149
|
+
) {
|
|
150
|
+
this.signer = signer;
|
|
151
|
+
this.keypair = keypair;
|
|
152
|
+
this.factory = new ethers.Contract(factoryAddress, FACTORY_ABI, signer);
|
|
153
|
+
this.registry = new PublicKeyRegistryClient(registryAddress, signer);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Register the caller's encryption public key on-chain.
|
|
158
|
+
* Must be called once per address before anyone can share records
|
|
159
|
+
* with that address. Safe to call again — will overwrite.
|
|
160
|
+
*/
|
|
161
|
+
async register(): Promise<ethers.TransactionReceipt> {
|
|
162
|
+
return this.registry.register(this.keypair);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Check if an address has registered its public key.
|
|
167
|
+
*/
|
|
168
|
+
async isRegistered(address: string): Promise<boolean> {
|
|
169
|
+
return this.registry.hasKey(address);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Create a new personal database for the calling wallet.
|
|
174
|
+
* Most users only ever need one database — use getOrCreateDatabase().
|
|
175
|
+
* @param name Human-readable label stored immutably on-chain.
|
|
176
|
+
*/
|
|
177
|
+
async createDatabase(name: string = ''): Promise<DatabaseClient> {
|
|
178
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
179
|
+
const tx = await (this.factory as any).createDatabase(name) as { wait(): Promise<ethers.TransactionReceipt> };
|
|
180
|
+
const receipt = await tx.wait();
|
|
181
|
+
|
|
182
|
+
const iface = new ethers.Interface(FACTORY_ABI);
|
|
183
|
+
for (const log of receipt.logs) {
|
|
184
|
+
try {
|
|
185
|
+
const parsed = iface.parseLog(log);
|
|
186
|
+
if (parsed?.name === 'DatabaseCreated') {
|
|
187
|
+
return new DatabaseClient(
|
|
188
|
+
parsed.args['db'] as string,
|
|
189
|
+
this.signer,
|
|
190
|
+
this.keypair,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
} catch { /* skip */ }
|
|
194
|
+
}
|
|
195
|
+
throw new Error('Web3QLClient.createDatabase: DatabaseCreated event not found');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get all database proxies owned by a user.
|
|
200
|
+
*/
|
|
201
|
+
async getDatabases(owner?: string): Promise<string[]> {
|
|
202
|
+
const addr = owner ?? await this.signer.getAddress();
|
|
203
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
204
|
+
return (this.factory as any).getUserDatabases(addr) as Promise<string[]>;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Return the first existing database for the caller, or create one
|
|
209
|
+
* if none exists. Idempotent — safe to call on every app startup.
|
|
210
|
+
* @param name Passed to createDatabase() only when a new one is created.
|
|
211
|
+
*/
|
|
212
|
+
async getOrCreateDatabase(name: string = ''): Promise<DatabaseClient> {
|
|
213
|
+
const dbs = await this.getDatabases();
|
|
214
|
+
if (dbs.length > 0) {
|
|
215
|
+
return new DatabaseClient(dbs[0]!, this.signer, this.keypair);
|
|
216
|
+
}
|
|
217
|
+
return this.createDatabase(name);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Wrap an already-known database address (e.g. loaded from config).
|
|
222
|
+
*/
|
|
223
|
+
database(address: string): DatabaseClient {
|
|
224
|
+
return new DatabaseClient(address, this.signer, this.keypair);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Remove a database from the factory’s registry.
|
|
229
|
+
* The proxy contract is NOT destroyed — it remains on-chain.
|
|
230
|
+
* After calling this it will no longer appear in getDatabases().
|
|
231
|
+
*/
|
|
232
|
+
async removeDatabase(dbAddress: string): Promise<ethers.TransactionReceipt> {
|
|
233
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
234
|
+
const tx = await (this.factory as any).removeDatabase(dbAddress) as { wait(): Promise<ethers.TransactionReceipt> };
|
|
235
|
+
return tx.wait();
|
|
236
|
+
}
|
|
237
|
+
}
|