Skip to content

Commit caaada5

Browse files
committed
feat: charter signature threshold enforced
1 parent 37199c4 commit caaada5

File tree

21 files changed

+1042
-68
lines changed

21 files changed

+1042
-68
lines changed
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
# Web3-Adapter Mapping Rules
2+
3+
This document explains how to create mappings for the web3-adapter system, which enables data exchange between different platforms using a universal ontology.
4+
5+
## Basic Structure
6+
7+
A mapping file defines how local database fields map to global ontology fields. The structure is:
8+
9+
```json
10+
{
11+
"tableName": "local_table_name",
12+
"schemaId": "global_schema_uuid",
13+
"ownerEnamePath": "path_to_owner_ename",
14+
"ownedJunctionTables": ["junction_table1", "junction_table2"],
15+
"localToUniversalMap": {
16+
"localField": "globalField",
17+
"localRelation": "tableName(relationPath),globalAlias"
18+
}
19+
}
20+
```
21+
22+
## Field Mapping
23+
24+
### Direct Field Mapping
25+
26+
```json
27+
"localField": "globalField"
28+
```
29+
30+
Maps a local field directly to a global field with the same name.
31+
32+
### Relation Mapping
33+
34+
```json
35+
"localRelation": "tableName(relationPath),globalAlias"
36+
```
37+
38+
Maps a local relation to a global field, where:
39+
40+
- `tableName` is the referenced table name
41+
- `relationPath` is the path to the relation data
42+
- `globalAlias` is the target global field name
43+
44+
### Array Relation Mapping
45+
46+
```json
47+
"participants": "users(participants[].id),participantIds"
48+
```
49+
50+
Maps an array of relations:
51+
52+
- `participants[].id` extracts the `id` field from each item in the `participants` array
53+
- `users()` resolves each ID to a global user reference
54+
- `participantIds` is the target global field name
55+
56+
## Special Functions
57+
58+
### Date Conversion (`__date`)
59+
60+
Converts various timestamp formats to ISO string format.
61+
62+
```json
63+
"createdAt": "__date(createdAt)"
64+
"timestamp": "__date(calc(timestamp * 1000))"
65+
```
66+
67+
**Supported input formats:**
68+
69+
- Unix timestamp (number)
70+
- Firebase v8 timestamp (`{_seconds: number}`)
71+
- Firebase v9+ timestamp (`{seconds: number}`)
72+
- Firebase Timestamp objects
73+
- Date objects
74+
- UTC strings
75+
76+
### Calculation (`__calc`)
77+
78+
Performs mathematical calculations using field values.
79+
80+
```json
81+
"total": "__calc(quantity * price)"
82+
"average": "__calc((score1 + score2 + score3) / 3)"
83+
```
84+
85+
**Features:**
86+
87+
- Supports basic arithmetic operations (+, -, \*, /, etc.)
88+
- Can reference other fields in the same entity
89+
- Automatically resolves field values before calculation
90+
91+
## Owner Path
92+
93+
The `ownerEnamePath` defines how to determine which eVault owns the data:
94+
95+
```json
96+
"ownerEnamePath": "ename" // Direct field
97+
"ownerEnamePath": "users(createdBy.ename)" // Nested via relation
98+
"ownerEnamePath": "users(participants[].ename)" // Array relation
99+
```
100+
101+
## Junction Tables
102+
103+
Junction tables (many-to-many relationships) can be marked as owned:
104+
105+
```json
106+
"ownedJunctionTables": [
107+
"user_followers",
108+
"user_following"
109+
]
110+
```
111+
112+
When junction table data changes, it triggers updates to the parent entity.
113+
114+
## Examples
115+
116+
### User Mapping
117+
118+
```json
119+
{
120+
"tableName": "users",
121+
"schemaId": "550e8400-e29b-41d4-a716-446655440000",
122+
"ownerEnamePath": "ename",
123+
"ownedJunctionTables": ["user_followers", "user_following"],
124+
"localToUniversalMap": {
125+
"handle": "username",
126+
"name": "displayName",
127+
"description": "bio",
128+
"avatarUrl": "avatarUrl",
129+
"ename": "ename",
130+
"followers": "followers",
131+
"following": "following"
132+
}
133+
}
134+
```
135+
136+
### Group with Relations
137+
138+
```json
139+
{
140+
"tableName": "groups",
141+
"schemaId": "550e8400-e29b-41d4-a716-446655440003",
142+
"ownerEnamePath": "users(participants[].ename)",
143+
"localToUniversalMap": {
144+
"name": "name",
145+
"description": "description",
146+
"owner": "owner",
147+
"admins": "users(admins),admins",
148+
"participants": "users(participants[].id),participantIds",
149+
"createdAt": "__date(createdAt)",
150+
"updatedAt": "__date(updatedAt)"
151+
}
152+
}
153+
```
154+
155+
## Best Practices
156+
157+
1. **Use descriptive global field names** that match the ontology schema
158+
2. **Handle timestamps consistently** using `__date()` function
159+
3. **Map relations properly** using the `tableName(relationPath)` syntax
160+
4. **Use aliases** when the global field name differs from the local field
161+
5. **Test mappings** with sample data to ensure proper conversion
162+
6. **Document complex mappings** with comments explaining the logic
163+
164+
## Troubleshooting
165+
166+
### Common Issues
167+
168+
1. **Missing relations**: Ensure the referenced table has a mapping
169+
2. **Invalid paths**: Check that the relation path matches your entity structure
170+
3. **Type mismatches**: Use `__date()` for timestamps, `__calc()` for calculations
171+
4. **Circular references**: Avoid mapping entities that reference each other infinitely
172+
173+
### Debug Tips
174+
175+
- Check the console for mapping errors
176+
- Verify that all referenced tables have mappings
177+
- Test with simple data first, then add complexity
178+
- Use the `__calc()` function to debug field values

platforms/cerberus/src/controllers/WebhookController.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { UserService } from "../services/UserService";
33
import { GroupService } from "../services/GroupService";
44
import { MessageService } from "../services/MessageService";
55
import { CerberusTriggerService } from "../services/CerberusTriggerService";
6+
import { CharterSignatureService } from "../services/CharterSignatureService";
67
import { Web3Adapter } from "../../../../infrastructure/web3-adapter/src";
78
import { User } from "../database/entities/User";
89
import { Group } from "../database/entities/Group";
@@ -14,13 +15,15 @@ export class WebhookController {
1415
groupService: GroupService;
1516
messageService: MessageService;
1617
cerberusTriggerService: CerberusTriggerService;
18+
charterSignatureService: CharterSignatureService;
1719
adapter: Web3Adapter;
1820

1921
constructor(adapter: Web3Adapter) {
2022
this.userService = new UserService();
2123
this.groupService = new GroupService();
2224
this.messageService = new MessageService();
2325
this.cerberusTriggerService = new CerberusTriggerService();
26+
this.charterSignatureService = new CharterSignatureService();
2427
this.adapter = adapter;
2528
}
2629

@@ -298,6 +301,81 @@ export class WebhookController {
298301
});
299302
}
300303
}
304+
} else if (mapping.tableName === "charter_signatures") {
305+
console.log("Processing charter signature with data:", local.data);
306+
307+
// Extract group and user from the signature data
308+
let group: Group | null = null;
309+
let user: User | null = null;
310+
311+
// Parse groupId from relation string like "groups(cd8e7ce1-ca76-4564-8fb8-1cbb5c3d1917)"
312+
if (local.data.groupId && typeof local.data.groupId === "string") {
313+
const groupId = local.data.groupId.split("(")[1].split(")")[0];
314+
console.log("Extracted groupId:", groupId);
315+
group = await this.groupService.getGroupById(groupId);
316+
}
317+
318+
// Parse userId from relation string like "users(userId)" or handle null case
319+
if (local.data.userId && typeof local.data.userId === "string") {
320+
const userId = local.data.userId.split("(")[1].split(")")[0];
321+
console.log("Extracted userId:", userId);
322+
user = await this.userService.getUserById(userId);
323+
} else if (local.data.userId === null) {
324+
console.log("userId is null, skipping user lookup");
325+
// For now, we'll create the signature without a user - you might want to handle this differently
326+
}
327+
328+
if (!group) {
329+
console.error("Group not found for charter signature");
330+
return res.status(500).send();
331+
}
332+
333+
if (!user) {
334+
console.error("User not found for charter signature - userId was null or invalid");
335+
return res.status(500).send();
336+
}
337+
338+
if (localId) {
339+
console.log("Updating existing charter signature with localId:", localId);
340+
// For now, we'll just log that we're updating
341+
// You might want to add update logic here if needed
342+
console.log("Charter signature update not yet implemented");
343+
} else {
344+
console.log("Creating new charter signature");
345+
346+
// Create the charter signature using the service
347+
const charterSignature = await this.charterSignatureService.createCharterSignature({
348+
data: {
349+
id: req.body.id,
350+
group: group.id,
351+
user: user.id,
352+
charterHash: local.data.charterHash,
353+
signature: local.data.signature,
354+
publicKey: local.data.publicKey,
355+
message: local.data.message,
356+
createdAt: local.data.createdAt,
357+
updatedAt: local.data.updatedAt,
358+
}
359+
});
360+
361+
console.log("Created charter signature with ID:", charterSignature.id);
362+
this.adapter.addToLockedIds(charterSignature.id);
363+
await this.adapter.mappingDb.storeMapping({
364+
localId: charterSignature.id,
365+
globalId: req.body.id,
366+
});
367+
console.log("Stored mapping for charter signature:", charterSignature.id, "->", req.body.id);
368+
369+
// Analyze charter activation after new signature
370+
try {
371+
await this.charterSignatureService.analyzeCharterActivation(
372+
group.id,
373+
this.messageService
374+
);
375+
} catch (error) {
376+
console.error("Error analyzing charter activation:", error);
377+
}
378+
}
301379
}
302380
res.status(200).send();
303381
} catch (e) {

platforms/cerberus/src/database/data-source.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { PostgresSubscriber } from "../web3adapter/watchers/subscriber";
88
import path from "path";
99
import { UserEVaultMapping } from "./entities/UserEVaultMapping";
1010
import { VotingObservation } from "./entities/VotingObservation";
11+
import { CharterSignature } from "./entities/CharterSignature";
1112

1213
config({ path: path.resolve(__dirname, "../../../../.env") });
1314

@@ -16,7 +17,16 @@ export const AppDataSource = new DataSource({
1617
url: process.env.CERBERUS_DATABASE_URL,
1718
synchronize: true, // Temporarily enabled to create voting_observations table
1819
logging: process.env.NODE_ENV === "development",
19-
entities: [User, Group, Message, MetaEnvelopeMap, UserEVaultMapping, VotingObservation],
20+
entities: [
21+
User,
22+
Group,
23+
Message,
24+
MetaEnvelopeMap,
25+
UserEVaultMapping,
26+
VotingObservation,
27+
CharterSignature,
28+
],
2029
migrations: ["src/database/migrations/*.ts"],
2130
subscribers: [PostgresSubscriber],
22-
});
31+
});
32+
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {
2+
Entity,
3+
CreateDateColumn,
4+
UpdateDateColumn,
5+
PrimaryGeneratedColumn,
6+
Column,
7+
ManyToOne,
8+
JoinColumn,
9+
} from "typeorm";
10+
import { Group } from "./Group";
11+
import { User } from "./User";
12+
13+
@Entity()
14+
export class CharterSignature {
15+
@PrimaryGeneratedColumn("uuid")
16+
id!: string;
17+
18+
@Column()
19+
groupId!: string;
20+
21+
@Column()
22+
userId!: string;
23+
24+
@Column({ type: "text" })
25+
charterHash!: string; // Hash of the charter content to track versions
26+
27+
@Column({ type: "text" })
28+
signature!: string; // Cryptographic signature
29+
30+
@Column({ type: "text" })
31+
publicKey!: string; // User's public key
32+
33+
@Column({ type: "text" })
34+
message!: string; // Original message that was signed
35+
36+
@ManyToOne(() => Group)
37+
@JoinColumn({ name: "groupId" })
38+
group!: Group;
39+
40+
@ManyToOne(() => User)
41+
@JoinColumn({ name: "userId" })
42+
user!: User;
43+
44+
@CreateDateColumn()
45+
createdAt!: Date;
46+
47+
@UpdateDateColumn()
48+
updatedAt!: Date;
49+
}
50+

platforms/cerberus/src/database/entities/Group.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
OneToMany,
1010
} from "typeorm";
1111
import { Message } from "./Message";
12+
import { CharterSignature } from "./CharterSignature";
1213

1314
@Entity()
1415
export class Group {
@@ -30,6 +31,9 @@ export class Group {
3031
@Column({ type: "text", nullable: true })
3132
charter!: string; // Markdown content for the group charter
3233

34+
@Column({ default: true })
35+
isCharterActive!: boolean; // Whether the charter is currently active and monitoring violations
36+
3337
@ManyToMany("User")
3438
@JoinTable({
3539
name: "group_participants",
@@ -41,6 +45,9 @@ export class Group {
4145
@OneToMany(() => Message, (message) => message.group)
4246
messages!: Message[];
4347

48+
@OneToMany(() => CharterSignature, (signature) => signature.group)
49+
charterSignatures!: CharterSignature[];
50+
4451
@CreateDateColumn()
4552
createdAt!: Date;
4653

0 commit comments

Comments
 (0)