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/model.ts
ADDED
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file model.ts
|
|
3
|
+
* @notice Web3QL ORM — Prisma-style Model class that unifies:
|
|
4
|
+
*
|
|
5
|
+
* • Encrypted record CRUD (via TypedTableClient under the hood)
|
|
6
|
+
* • On-chain COUNTER fields — automatically merged into every find result
|
|
7
|
+
* • Relation writes — `relatedCreate()` pays with native CELO or any ERC-20
|
|
8
|
+
* and atomically writes the source record + increments target counters
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* ─────────────────────────────────────────────────────────────
|
|
12
|
+
* // 1. Define your models
|
|
13
|
+
* const projects = new Model<Project>('projects', projectsTableAddr, signer, keypair, {
|
|
14
|
+
* counterFields: ['vote_total', 'vote_count', 'tip_total', 'tip_count'],
|
|
15
|
+
* schema: projectSchema,
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* const votes = new Model<Vote>('votes', votesTableAddr, signer, keypair);
|
|
19
|
+
*
|
|
20
|
+
* // 2. Standard CRUD
|
|
21
|
+
* await projects.create(1n, { id: 1n, name: 'Web3QL' });
|
|
22
|
+
*
|
|
23
|
+
* // 3. findUnique — counter fields merged in automatically
|
|
24
|
+
* const p = await projects.findUnique(1n);
|
|
25
|
+
* // p = { id: 1n, name: 'Web3QL', vote_total: 420n, vote_count: 3n, ... }
|
|
26
|
+
*
|
|
27
|
+
* // 4. Relation write — pay 2 CELO, atomically save vote + update project counters
|
|
28
|
+
* await votes.relatedCreate({
|
|
29
|
+
* wire : wireAddress,
|
|
30
|
+
* id : 10n,
|
|
31
|
+
* data : { project_id: 1n, voter: myAddr, amount: 2n * 10n**18n },
|
|
32
|
+
* targetId : 1n,
|
|
33
|
+
* amount : 2n * 10n**18n, // native CELO
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* // 5. ERC-20 vote: approve first, then:
|
|
37
|
+
* await votes.relatedCreate({
|
|
38
|
+
* wire : wireAddress,
|
|
39
|
+
* id : 11n,
|
|
40
|
+
* data : { project_id: 1n, voter: myAddr, amount: 5_000000n },
|
|
41
|
+
* targetId : 1n,
|
|
42
|
+
* amount : 5_000000n, // 5 cUSD (6 decimals)
|
|
43
|
+
* token : CUSD_ADDRESS, // ERC-20 token — pre-approve wire contract first
|
|
44
|
+
* });
|
|
45
|
+
* ─────────────────────────────────────────────────────────────
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
import { ethers } from 'ethers';
|
|
49
|
+
import { EncryptedTableClient, Role } from './table-client.js';
|
|
50
|
+
import { TypedTableClient } from './typed-table.js';
|
|
51
|
+
import type {
|
|
52
|
+
FindManyOptions,
|
|
53
|
+
RecordWithId,
|
|
54
|
+
WhereTuple,
|
|
55
|
+
SchemaDefinition,
|
|
56
|
+
} from './typed-table.js';
|
|
57
|
+
import type { EncryptionKeypair } from './crypto.js';
|
|
58
|
+
import type { PublicKeyRegistryClient } from './registry.js';
|
|
59
|
+
export { Role };
|
|
60
|
+
|
|
61
|
+
// ─────────────────────────────────────────────────────────────
|
|
62
|
+
// Minimal ABIs
|
|
63
|
+
// ─────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
const COUNTER_ABI = [
|
|
66
|
+
'function counterValue(bytes32 targetKey, bytes32 field) external view returns (uint256)',
|
|
67
|
+
] as const;
|
|
68
|
+
|
|
69
|
+
const WIRE_ABI = [
|
|
70
|
+
'function relatedWrite(bytes32 sourceKey, bytes ciphertext, bytes encryptedKey, bytes32 targetKey, address token, uint256 amount) payable external',
|
|
71
|
+
'function relatedWriteWithPermit(bytes32 sourceKey, bytes ciphertext, bytes encryptedKey, bytes32 targetKey, address token, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external',
|
|
72
|
+
'function withdrawProjectFunds(bytes32 targetKey, address token, address to) external',
|
|
73
|
+
'function withdrawAllProjectFunds(bytes32 targetKey, address to) external',
|
|
74
|
+
'function projectBalances(bytes32 targetKey) external view returns (address[] tokens, uint256[] balances)',
|
|
75
|
+
'function getAllowedTokens() external view returns (address[])',
|
|
76
|
+
] as const;
|
|
77
|
+
|
|
78
|
+
const ERC20_ABI = [
|
|
79
|
+
'function allowance(address owner, address spender) external view returns (uint256)',
|
|
80
|
+
'function approve(address spender, uint256 amount) external returns (bool)',
|
|
81
|
+
'function nonces(address owner) external view returns (uint256)',
|
|
82
|
+
'function name() external view returns (string)',
|
|
83
|
+
'function version() external view returns (string)',
|
|
84
|
+
'function DOMAIN_SEPARATOR() external view returns (bytes32)',
|
|
85
|
+
] as const;
|
|
86
|
+
|
|
87
|
+
// ─────────────────────────────────────────────────────────────
|
|
88
|
+
// Types
|
|
89
|
+
// ─────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
export interface ModelOptions {
|
|
92
|
+
/**
|
|
93
|
+
* Names of COUNTER fields on this table.
|
|
94
|
+
* Counter values are stored in the on-chain `counters` mapping — NOT
|
|
95
|
+
* inside the encrypted ciphertext. They are automatically fetched
|
|
96
|
+
* and merged into every `findUnique` / `findMany` result.
|
|
97
|
+
*/
|
|
98
|
+
counterFields?: string[];
|
|
99
|
+
/** Optional schema for validation, type coercion, NOT NULL, DEFAULT. */
|
|
100
|
+
schema? : SchemaDefinition;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Options for `relatedCreate()` */
|
|
104
|
+
export interface RelatedCreateOptions<T> {
|
|
105
|
+
/** Address of the deployed Web3QLRelationWire contract. */
|
|
106
|
+
wire : string;
|
|
107
|
+
/** Primary key of the NEW record being written to THIS table. */
|
|
108
|
+
id : bigint;
|
|
109
|
+
/** Payload to encrypt and store (COUNTER fields are excluded automatically). */
|
|
110
|
+
data : T;
|
|
111
|
+
/** Primary key of the TARGET record to increment counters on. */
|
|
112
|
+
targetId : bigint;
|
|
113
|
+
/** Name of the target table (needed for key derivation). */
|
|
114
|
+
targetTable : string;
|
|
115
|
+
/**
|
|
116
|
+
* Payment amount.
|
|
117
|
+
* - Native wire (token undefined / address(0)): amount in wei (msg.value)
|
|
118
|
+
* - ERC-20 wire (token set): amount in token's native units
|
|
119
|
+
*/
|
|
120
|
+
amount? : bigint;
|
|
121
|
+
/**
|
|
122
|
+
* ERC-20 token address. Omit (or pass `undefined`) for native CELO.
|
|
123
|
+
* The wire contract must be pre-approved: call `model.approveWire(wire, token, amount)` once.
|
|
124
|
+
*/
|
|
125
|
+
token? : string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─────────────────────────────────────────────────────────────
|
|
129
|
+
// Model
|
|
130
|
+
// ─────────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* `Model<T>` — the Web3QL ORM entry point.
|
|
134
|
+
*
|
|
135
|
+
* T is the shape of the ENCRYPTED fields only (not counter fields).
|
|
136
|
+
* Counter fields are added automatically to every returned object.
|
|
137
|
+
*/
|
|
138
|
+
export class Model<T extends Record<string, unknown>> {
|
|
139
|
+
readonly tableName : string;
|
|
140
|
+
readonly tableAddress : string;
|
|
141
|
+
|
|
142
|
+
private inner : TypedTableClient<T>;
|
|
143
|
+
private rawClient : EncryptedTableClient;
|
|
144
|
+
private counterFields : string[];
|
|
145
|
+
private counterFieldHashes: Map<string, string>; // name → keccak256
|
|
146
|
+
private signer : ethers.Signer;
|
|
147
|
+
private provider : ethers.Provider;
|
|
148
|
+
|
|
149
|
+
constructor(
|
|
150
|
+
tableName : string,
|
|
151
|
+
tableAddress : string,
|
|
152
|
+
signer : ethers.Signer,
|
|
153
|
+
keypair : EncryptionKeypair,
|
|
154
|
+
options? : ModelOptions,
|
|
155
|
+
) {
|
|
156
|
+
this.tableName = tableName;
|
|
157
|
+
this.tableAddress = tableAddress;
|
|
158
|
+
this.signer = signer;
|
|
159
|
+
this.provider = signer.provider!;
|
|
160
|
+
|
|
161
|
+
const cf = options?.counterFields ?? [];
|
|
162
|
+
this.counterFields = cf;
|
|
163
|
+
this.counterFieldHashes = new Map(
|
|
164
|
+
cf.map((name) => [name, ethers.keccak256(ethers.toUtf8Bytes(name))])
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
this.rawClient = new EncryptedTableClient(tableAddress, signer, keypair);
|
|
168
|
+
this.inner = new TypedTableClient<T>(tableName, this.rawClient, options?.schema);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ─────────────────────────────────────────────────────────────
|
|
172
|
+
// Key derivation
|
|
173
|
+
// ─────────────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
/** Derive the on-chain bytes32 record key for a given primary key. */
|
|
176
|
+
key(id: bigint): string {
|
|
177
|
+
return this.rawClient.deriveKey(this.tableName, id);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ─────────────────────────────────────────────────────────────
|
|
181
|
+
// Counter reads (public — no auth needed)
|
|
182
|
+
// ─────────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
/** Read a single named counter for the record at `id`. */
|
|
185
|
+
async counter(id: bigint, field: string): Promise<bigint> {
|
|
186
|
+
const hash = this.counterFieldHashes.get(field)
|
|
187
|
+
?? ethers.keccak256(ethers.toUtf8Bytes(field));
|
|
188
|
+
const contract = new ethers.Contract(this.tableAddress, COUNTER_ABI, this.provider);
|
|
189
|
+
const raw = await (contract as any)['counterValue'](this.key(id), hash);
|
|
190
|
+
return BigInt(raw);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Read ALL registered counter fields for the record at `id`.
|
|
195
|
+
* Returns a plain object mapping field name → bigint value.
|
|
196
|
+
*/
|
|
197
|
+
async counters(id: bigint): Promise<Record<string, bigint>> {
|
|
198
|
+
if (this.counterFields.length === 0) return {};
|
|
199
|
+
const contract = new ethers.Contract(this.tableAddress, COUNTER_ABI, this.provider);
|
|
200
|
+
const key = this.key(id);
|
|
201
|
+
const values = await Promise.all(
|
|
202
|
+
this.counterFields.map(async (name) => {
|
|
203
|
+
const hash = this.counterFieldHashes.get(name)!;
|
|
204
|
+
const raw = await (contract as any)['counterValue'](key, hash);
|
|
205
|
+
return [name, BigInt(raw)] as [string, bigint];
|
|
206
|
+
})
|
|
207
|
+
);
|
|
208
|
+
return Object.fromEntries(values);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ─────────────────────────────────────────────────────────────
|
|
212
|
+
// CRUD — counter fields merged into results automatically
|
|
213
|
+
// ─────────────────────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Encrypt and store a new record.
|
|
217
|
+
* Pass only encrypted fields — counter fields are managed by the chain.
|
|
218
|
+
*/
|
|
219
|
+
async create(id: bigint, data: T): Promise<ethers.TransactionReceipt> {
|
|
220
|
+
return this.inner.create(id, data);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Read and decrypt a record, with all counter values merged in.
|
|
225
|
+
* Returns `null` if the record does not exist.
|
|
226
|
+
*/
|
|
227
|
+
async findUnique(id: bigint): Promise<(T & Record<string, bigint>) | null> {
|
|
228
|
+
const [row, counterValues] = await Promise.all([
|
|
229
|
+
this.inner.findUnique(id),
|
|
230
|
+
this.counters(id),
|
|
231
|
+
]);
|
|
232
|
+
if (row === null) return null;
|
|
233
|
+
return { ...row, ...counterValues } as T & Record<string, bigint>;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* List and decrypt all records owned by `ownerAddress`.
|
|
238
|
+
* Counter values are merged into every record.
|
|
239
|
+
*/
|
|
240
|
+
async findMany(
|
|
241
|
+
ownerAddress : string,
|
|
242
|
+
options? : FindManyOptions,
|
|
243
|
+
): Promise<RecordWithId<T & Record<string, bigint>>[]> {
|
|
244
|
+
const rows = await this.inner.findMany(ownerAddress, options);
|
|
245
|
+
if (this.counterFields.length === 0) {
|
|
246
|
+
return rows as RecordWithId<T & Record<string, bigint>>[];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Fetch counters for each record in parallel
|
|
250
|
+
const withCounters = await Promise.all(
|
|
251
|
+
rows.map(async (row) => {
|
|
252
|
+
// We need to re-derive id from recordKey — we can't, but we can derive
|
|
253
|
+
// key from recordKey directly: the recordKey IS the bytes32 key
|
|
254
|
+
const counterValues = await this._countersFromBytes32Key(row.recordKey);
|
|
255
|
+
return {
|
|
256
|
+
...row,
|
|
257
|
+
data: { ...row.data, ...counterValues } as T & Record<string, bigint>,
|
|
258
|
+
};
|
|
259
|
+
})
|
|
260
|
+
);
|
|
261
|
+
return withCounters;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Get all records (no chain limit). Counter values merged. */
|
|
265
|
+
async findAll(ownerAddress: string): Promise<RecordWithId<T & Record<string, bigint>>[]> {
|
|
266
|
+
const total = Number(await this.inner.count(ownerAddress));
|
|
267
|
+
return this.findMany(ownerAddress, { chainOffset: 0n, chainLimit: BigInt(total) });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Patch an existing record — fetches current, merges patch, re-encrypts.
|
|
272
|
+
* Counter fields in patch are silently ignored (they live on-chain, not in ciphertext).
|
|
273
|
+
*/
|
|
274
|
+
async update(id: bigint, patch: Partial<T>): Promise<ethers.TransactionReceipt> {
|
|
275
|
+
// Strip counter fields from patch — they can't be written via update
|
|
276
|
+
const safePatch = { ...patch };
|
|
277
|
+
for (const cf of this.counterFields) delete (safePatch as Record<string, unknown>)[cf];
|
|
278
|
+
return this.inner.update(id, safePatch);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Delete a record (soft-delete — scrubs all encrypted key copies). */
|
|
282
|
+
async delete(id: bigint): Promise<ethers.TransactionReceipt> {
|
|
283
|
+
return this.inner.remove(id);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** True if a live record exists for `id`. */
|
|
287
|
+
async exists(id: bigint): Promise<boolean> {
|
|
288
|
+
return this.inner.exists(id);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Total record count (including deleted) for `ownerAddress`. */
|
|
292
|
+
async count(ownerAddress: string): Promise<bigint> {
|
|
293
|
+
return this.inner.count(ownerAddress);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ─────────────────────────────────────────────────────────────
|
|
297
|
+
// Access control (pass-through to raw client)
|
|
298
|
+
// ─────────────────────────────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
async share(
|
|
301
|
+
id : bigint,
|
|
302
|
+
recipient : string,
|
|
303
|
+
role : Role,
|
|
304
|
+
registry : PublicKeyRegistryClient,
|
|
305
|
+
): Promise<ethers.TransactionReceipt> {
|
|
306
|
+
return this.rawClient.share(this.key(id), recipient, role, registry);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async revoke(id: bigint, user: string): Promise<ethers.TransactionReceipt> {
|
|
310
|
+
return this.rawClient.revoke(this.key(id), user);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ─────────────────────────────────────────────────────────────
|
|
314
|
+
// Relation write (the key feature)
|
|
315
|
+
// ─────────────────────────────────────────────────────────────
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Write a record to THIS table via a RelationWire, atomically incrementing
|
|
319
|
+
* counter fields on the TARGET table in the same transaction.
|
|
320
|
+
*
|
|
321
|
+
* For native CELO: pass `amount` in wei, do NOT pass `token`.
|
|
322
|
+
* For ERC-20: pass `token` address + `amount` in token units.
|
|
323
|
+
* You must have approved the wire contract beforehand —
|
|
324
|
+
* call `model.approveWire(wire, token, amount)` once.
|
|
325
|
+
*/
|
|
326
|
+
async relatedCreate(opts: RelatedCreateOptions<T>): Promise<ethers.TransactionReceipt> {
|
|
327
|
+
const { wire, id, data, targetId, targetTable, amount = 0n, token } = opts;
|
|
328
|
+
const isErc20 = !!token && token !== ethers.ZeroAddress;
|
|
329
|
+
const isNative = !isErc20;
|
|
330
|
+
|
|
331
|
+
const { ciphertextBytes, encryptedKeyBytes } = await this._encryptPayload(id, data);
|
|
332
|
+
|
|
333
|
+
const sourceKey = this.key(id);
|
|
334
|
+
const targetKey = ethers.keccak256(
|
|
335
|
+
ethers.solidityPacked(['string', 'uint256'], [targetTable, targetId])
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
const wireContract = new ethers.Contract(wire, WIRE_ABI, this.signer);
|
|
339
|
+
|
|
340
|
+
if (isNative) {
|
|
341
|
+
const tx = await (wireContract as any)['relatedWrite'](
|
|
342
|
+
sourceKey, ciphertextBytes, encryptedKeyBytes, targetKey,
|
|
343
|
+
ethers.ZeroAddress, 0n,
|
|
344
|
+
{ value: amount },
|
|
345
|
+
);
|
|
346
|
+
return tx.wait();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ── ERC-20: try permit (gasless approve), fall back to standard approve ──
|
|
350
|
+
const signerAddr = await this.signer.getAddress();
|
|
351
|
+
|
|
352
|
+
let usedPermit = false;
|
|
353
|
+
try {
|
|
354
|
+
const sig = await this._signPermit(token!, wire, amount, signerAddr);
|
|
355
|
+
const tx = await (wireContract as any)['relatedWriteWithPermit'](
|
|
356
|
+
sourceKey, ciphertextBytes, encryptedKeyBytes, targetKey,
|
|
357
|
+
token!, amount,
|
|
358
|
+
sig.deadline, sig.v, sig.r, sig.s,
|
|
359
|
+
);
|
|
360
|
+
const receipt = await tx.wait();
|
|
361
|
+
usedPermit = true;
|
|
362
|
+
return receipt;
|
|
363
|
+
} catch {
|
|
364
|
+
// Token doesn't support EIP-2612 permit, or wallet can't sign typed data — fall through
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (!usedPermit) {
|
|
368
|
+
// Standard path: check allowance, approve if needed, then relatedWrite
|
|
369
|
+
const tokenContract = new ethers.Contract(token!, ERC20_ABI, this.signer);
|
|
370
|
+
const allowance = BigInt(await (tokenContract as any)['allowance'](signerAddr, wire));
|
|
371
|
+
if (allowance < amount) {
|
|
372
|
+
const approveTx = await (tokenContract as any)['approve'](wire, ethers.MaxUint256);
|
|
373
|
+
await approveTx.wait();
|
|
374
|
+
}
|
|
375
|
+
const tx = await (wireContract as any)['relatedWrite'](
|
|
376
|
+
sourceKey, ciphertextBytes, encryptedKeyBytes, targetKey,
|
|
377
|
+
token!, amount,
|
|
378
|
+
);
|
|
379
|
+
return tx.wait();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// unreachable but satisfies TypeScript
|
|
383
|
+
throw new Error('relatedCreate: unexpected state');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Approve a RelationWire to spend your ERC-20 tokens.
|
|
388
|
+
* Call this once before using `relatedCreate` with an ERC-20 wire.
|
|
389
|
+
* You can approve `MaxUint256` for unlimited allowance.
|
|
390
|
+
*/
|
|
391
|
+
async approveWire(
|
|
392
|
+
wire : string,
|
|
393
|
+
token : string,
|
|
394
|
+
amount : bigint = ethers.MaxUint256,
|
|
395
|
+
): Promise<ethers.TransactionReceipt> {
|
|
396
|
+
const tokenContract = new ethers.Contract(token, ERC20_ABI, this.signer);
|
|
397
|
+
const tx = await (tokenContract as any)['approve'](wire, amount);
|
|
398
|
+
return tx.wait();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Withdraw a single token's accumulated payments for a project record.
|
|
403
|
+
* Caller must be the record owner of `id` on this table.
|
|
404
|
+
*
|
|
405
|
+
* @param wire Address of the RelationWire contract
|
|
406
|
+
* @param id Primary key of THIS table's record (the project)
|
|
407
|
+
* @param token Token to withdraw (ethers.ZeroAddress = native CELO)
|
|
408
|
+
* @param to Recipient address (defaults to signer)
|
|
409
|
+
*/
|
|
410
|
+
async withdrawFunds(
|
|
411
|
+
wire : string,
|
|
412
|
+
id : bigint,
|
|
413
|
+
token : string = ethers.ZeroAddress,
|
|
414
|
+
to? : string,
|
|
415
|
+
): Promise<ethers.TransactionReceipt> {
|
|
416
|
+
const recipient = to ?? await this.signer.getAddress();
|
|
417
|
+
const wireContract = new ethers.Contract(wire, WIRE_ABI, this.signer);
|
|
418
|
+
const tx = await (wireContract as any)['withdrawProjectFunds'](
|
|
419
|
+
this.key(id), token, recipient
|
|
420
|
+
);
|
|
421
|
+
return tx.wait();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Withdraw ALL token balances for a project in one transaction.
|
|
426
|
+
* Caller must be the record owner of `id` on this table.
|
|
427
|
+
*
|
|
428
|
+
* @param wire Address of the RelationWire contract
|
|
429
|
+
* @param id Primary key of the project record
|
|
430
|
+
* @param to Recipient address (defaults to signer)
|
|
431
|
+
*/
|
|
432
|
+
async withdrawAllFunds(
|
|
433
|
+
wire : string,
|
|
434
|
+
id : bigint,
|
|
435
|
+
to? : string,
|
|
436
|
+
): Promise<ethers.TransactionReceipt> {
|
|
437
|
+
const recipient = to ?? await this.signer.getAddress();
|
|
438
|
+
const wireContract = new ethers.Contract(wire, WIRE_ABI, this.signer);
|
|
439
|
+
const tx = await (wireContract as any)['withdrawAllProjectFunds'](
|
|
440
|
+
this.key(id), recipient
|
|
441
|
+
);
|
|
442
|
+
return tx.wait();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Check pending balances for a project across all tokens accepted by the wire.
|
|
447
|
+
* Returns a plain object: { tokenAddress: bigintBalance, ... }
|
|
448
|
+
*
|
|
449
|
+
* @param wire Address of the RelationWire contract
|
|
450
|
+
* @param id Primary key of the project record
|
|
451
|
+
*/
|
|
452
|
+
async projectBalances(
|
|
453
|
+
wire : string,
|
|
454
|
+
id : bigint,
|
|
455
|
+
): Promise<Record<string, bigint>> {
|
|
456
|
+
const wireContract = new ethers.Contract(wire, WIRE_ABI, this.provider);
|
|
457
|
+
const { tokens, balances } = await (wireContract as any)['projectBalances'](this.key(id));
|
|
458
|
+
const result: Record<string, bigint> = {};
|
|
459
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
460
|
+
result[tokens[i]] = BigInt(balances[i]);
|
|
461
|
+
}
|
|
462
|
+
return result;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ─────────────────────────────────────────────────────────────
|
|
466
|
+
// Private helpers
|
|
467
|
+
// ─────────────────────────────────────────────────────────────
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Build and sign an EIP-2612 permit signature.
|
|
471
|
+
* Works for cUSD, cEUR, cREAL, USDC and any token that implements ERC-2612.
|
|
472
|
+
* Throws if the token doesn't expose `nonces()` or `DOMAIN_SEPARATOR()`.
|
|
473
|
+
*/
|
|
474
|
+
private async _signPermit(
|
|
475
|
+
token : string,
|
|
476
|
+
spender : string,
|
|
477
|
+
value : bigint,
|
|
478
|
+
owner : string,
|
|
479
|
+
ttl : number = 20 * 60, // 20 minutes
|
|
480
|
+
): Promise<{ deadline: bigint; v: number; r: string; s: string }> {
|
|
481
|
+
const tokenContract = new ethers.Contract(token, ERC20_ABI, this.provider);
|
|
482
|
+
|
|
483
|
+
const [nonce, name, deadline] = await Promise.all([
|
|
484
|
+
(tokenContract as any)['nonces'](owner).then(BigInt),
|
|
485
|
+
(tokenContract as any)['name'](),
|
|
486
|
+
Promise.resolve(BigInt(Math.floor(Date.now() / 1000) + ttl)),
|
|
487
|
+
]);
|
|
488
|
+
|
|
489
|
+
// Try ERC-2612 version(); many tokens omit it and default to "1"
|
|
490
|
+
let version = '1';
|
|
491
|
+
try { version = await (tokenContract as any)['version'](); } catch { /* default "1" */ }
|
|
492
|
+
|
|
493
|
+
const network = await this.provider.getNetwork();
|
|
494
|
+
const domain = { name, version, chainId: Number(network.chainId), verifyingContract: token };
|
|
495
|
+
const types = {
|
|
496
|
+
Permit: [
|
|
497
|
+
{ name: 'owner', type: 'address' },
|
|
498
|
+
{ name: 'spender', type: 'address' },
|
|
499
|
+
{ name: 'value', type: 'uint256' },
|
|
500
|
+
{ name: 'nonce', type: 'uint256' },
|
|
501
|
+
{ name: 'deadline', type: 'uint256' },
|
|
502
|
+
],
|
|
503
|
+
};
|
|
504
|
+
const message = { owner, spender, value, nonce, deadline };
|
|
505
|
+
|
|
506
|
+
const sig = await (this.signer as ethers.Signer & {
|
|
507
|
+
signTypedData(domain: object, types: object, value: object): Promise<string>;
|
|
508
|
+
}).signTypedData(domain, types, message);
|
|
509
|
+
|
|
510
|
+
const { v, r, s } = ethers.Signature.from(sig);
|
|
511
|
+
return { deadline, v, r, s };
|
|
512
|
+
}
|
|
513
|
+
private async _countersFromBytes32Key(recordKey: string): Promise<Record<string, bigint>> {
|
|
514
|
+
if (this.counterFields.length === 0) return {};
|
|
515
|
+
const contract = new ethers.Contract(this.tableAddress, COUNTER_ABI, this.provider);
|
|
516
|
+
const values = await Promise.all(
|
|
517
|
+
this.counterFields.map(async (name) => {
|
|
518
|
+
const hash = this.counterFieldHashes.get(name)!;
|
|
519
|
+
const raw = await (contract as any)['counterValue'](recordKey, hash);
|
|
520
|
+
return [name, BigInt(raw)] as [string, bigint];
|
|
521
|
+
})
|
|
522
|
+
);
|
|
523
|
+
return Object.fromEntries(values);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Encrypt a payload for self, returning raw bytes for the wire contract.
|
|
528
|
+
*/
|
|
529
|
+
private async _encryptPayload(
|
|
530
|
+
_id : bigint,
|
|
531
|
+
data : T,
|
|
532
|
+
): Promise<{ ciphertextBytes: Uint8Array; encryptedKeyBytes: Uint8Array }> {
|
|
533
|
+
const { ciphertext, encryptedKey } = await this.rawClient.encryptForSelf(
|
|
534
|
+
JSON.stringify(data)
|
|
535
|
+
);
|
|
536
|
+
return { ciphertextBytes: ciphertext, encryptedKeyBytes: encryptedKey };
|
|
537
|
+
}
|
|
538
|
+
}
|