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.
Files changed (86) hide show
  1. package/README.md +66 -0
  2. package/contracts/PublicKeyRegistry.sol +87 -0
  3. package/dist/src/access.d.ts +176 -0
  4. package/dist/src/access.d.ts.map +1 -0
  5. package/dist/src/access.js +283 -0
  6. package/dist/src/access.js.map +1 -0
  7. package/dist/src/batch.d.ts +107 -0
  8. package/dist/src/batch.d.ts.map +1 -0
  9. package/dist/src/batch.js +188 -0
  10. package/dist/src/batch.js.map +1 -0
  11. package/dist/src/cli.d.ts +40 -0
  12. package/dist/src/cli.d.ts.map +1 -0
  13. package/dist/src/cli.js +361 -0
  14. package/dist/src/cli.js.map +1 -0
  15. package/dist/src/constraints.d.ts +126 -0
  16. package/dist/src/constraints.d.ts.map +1 -0
  17. package/dist/src/constraints.js +192 -0
  18. package/dist/src/constraints.js.map +1 -0
  19. package/dist/src/crypto.d.ts +118 -0
  20. package/dist/src/crypto.d.ts.map +1 -0
  21. package/dist/src/crypto.js +192 -0
  22. package/dist/src/crypto.js.map +1 -0
  23. package/dist/src/factory-client.d.ts +106 -0
  24. package/dist/src/factory-client.d.ts.map +1 -0
  25. package/dist/src/factory-client.js +202 -0
  26. package/dist/src/factory-client.js.map +1 -0
  27. package/dist/src/index-cache.d.ts +156 -0
  28. package/dist/src/index-cache.d.ts.map +1 -0
  29. package/dist/src/index-cache.js +265 -0
  30. package/dist/src/index-cache.js.map +1 -0
  31. package/dist/src/index.d.ts +60 -0
  32. package/dist/src/index.d.ts.map +1 -0
  33. package/dist/src/index.js +60 -0
  34. package/dist/src/index.js.map +1 -0
  35. package/dist/src/migrations.d.ts +114 -0
  36. package/dist/src/migrations.d.ts.map +1 -0
  37. package/dist/src/migrations.js +173 -0
  38. package/dist/src/migrations.js.map +1 -0
  39. package/dist/src/model.d.ts +198 -0
  40. package/dist/src/model.d.ts.map +1 -0
  41. package/dist/src/model.js +379 -0
  42. package/dist/src/model.js.map +1 -0
  43. package/dist/src/query.d.ts +155 -0
  44. package/dist/src/query.d.ts.map +1 -0
  45. package/dist/src/query.js +386 -0
  46. package/dist/src/query.js.map +1 -0
  47. package/dist/src/registry.d.ts +45 -0
  48. package/dist/src/registry.d.ts.map +1 -0
  49. package/dist/src/registry.js +80 -0
  50. package/dist/src/registry.js.map +1 -0
  51. package/dist/src/schema-manager.d.ts +109 -0
  52. package/dist/src/schema-manager.d.ts.map +1 -0
  53. package/dist/src/schema-manager.js +259 -0
  54. package/dist/src/schema-manager.js.map +1 -0
  55. package/dist/src/table-client.d.ts +156 -0
  56. package/dist/src/table-client.d.ts.map +1 -0
  57. package/dist/src/table-client.js +292 -0
  58. package/dist/src/table-client.js.map +1 -0
  59. package/dist/src/typed-table.d.ts +159 -0
  60. package/dist/src/typed-table.d.ts.map +1 -0
  61. package/dist/src/typed-table.js +246 -0
  62. package/dist/src/typed-table.js.map +1 -0
  63. package/dist/src/types.d.ts +48 -0
  64. package/dist/src/types.d.ts.map +1 -0
  65. package/dist/src/types.js +222 -0
  66. package/dist/src/types.js.map +1 -0
  67. package/keyManager.js +337 -0
  68. package/package.json +38 -0
  69. package/src/access.ts +421 -0
  70. package/src/batch.ts +259 -0
  71. package/src/cli.ts +349 -0
  72. package/src/constraints.ts +283 -0
  73. package/src/crypto.ts +239 -0
  74. package/src/factory-client.ts +237 -0
  75. package/src/index-cache.ts +351 -0
  76. package/src/index.ts +171 -0
  77. package/src/migrations.ts +215 -0
  78. package/src/model.ts +538 -0
  79. package/src/query.ts +508 -0
  80. package/src/registry.ts +100 -0
  81. package/src/schema-manager.ts +301 -0
  82. package/src/table-client.ts +393 -0
  83. package/src/typed-table.ts +340 -0
  84. package/src/types.ts +284 -0
  85. package/tsconfig.json +22 -0
  86. 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
+ }