@show-karma/karma-gap-sdk 0.4.27 → 0.4.29
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/core/class/entities/Milestone.d.ts +18 -0
- package/core/class/entities/Milestone.js +60 -1
- package/core/class/karma-indexer/api/types.d.ts +1 -0
- package/core/class/types/attestations.d.ts +2 -2
- package/package.json +1 -1
- package/core/__tests__/Milestone.edit.test.d.ts +0 -20
- package/core/__tests__/Milestone.edit.test.js +0 -328
- package/core/__tests__/received-date.types.test.d.ts +0 -1
- package/core/__tests__/received-date.types.test.js +0 -39
- package/core/class/remote-storage/IpfsStorage.d.ts +0 -23
- package/core/class/remote-storage/IpfsStorage.js +0 -56
- package/core/class/remote-storage/RemoteStorage.d.ts +0 -41
- package/core/class/remote-storage/RemoteStorage.js +0 -38
- package/core/utils/get-ipfs-data.d.ts +0 -1
- package/core/utils/get-ipfs-data.js +0 -19
|
@@ -30,6 +30,7 @@ export declare class Milestone extends Attestation<IMilestone> implements IMiles
|
|
|
30
30
|
completed: MilestoneCompleted;
|
|
31
31
|
approved: MilestoneCompleted;
|
|
32
32
|
rejected: MilestoneCompleted;
|
|
33
|
+
cancelled: MilestoneCompleted;
|
|
33
34
|
verified: MilestoneCompleted[];
|
|
34
35
|
type: string;
|
|
35
36
|
priority?: number;
|
|
@@ -93,6 +94,23 @@ export declare class Milestone extends Attestation<IMilestone> implements IMiles
|
|
|
93
94
|
tx: Transaction[];
|
|
94
95
|
uids: `0x${string}`[];
|
|
95
96
|
}>;
|
|
97
|
+
/**
|
|
98
|
+
* Cancels this milestone. A cancelled milestone will not be delivered and is
|
|
99
|
+
* kept, badged "cancelled", for the program's records. Only milestones that
|
|
100
|
+
* are not yet completed or verified can be cancelled.
|
|
101
|
+
* @param signer
|
|
102
|
+
* @param reason
|
|
103
|
+
*/
|
|
104
|
+
cancel(signer: SignerOrProvider, reason?: string, callback?: Function): Promise<AttestationWithTx>;
|
|
105
|
+
/**
|
|
106
|
+
* Revokes the cancelled status of the milestone (un-cancel). If the milestone
|
|
107
|
+
* is not cancelled, it will throw an error.
|
|
108
|
+
* @param signer
|
|
109
|
+
*/
|
|
110
|
+
revokeCancel(signer: SignerOrProvider): Promise<{
|
|
111
|
+
tx: Transaction[];
|
|
112
|
+
uids: `0x${string}`[];
|
|
113
|
+
}>;
|
|
96
114
|
/**
|
|
97
115
|
* Revokes multiple milestone attestations at once.
|
|
98
116
|
* This method can be used to revoke multiple milestone attestations in a single transaction.
|
|
@@ -31,7 +31,7 @@ class Milestone extends Attestation_1.Attestation {
|
|
|
31
31
|
* @param callback - Optional callback function for status updates
|
|
32
32
|
*/
|
|
33
33
|
async edit(signer, newData, callback) {
|
|
34
|
-
if (this.completed || this.approved || this.verified?.length) {
|
|
34
|
+
if (this.completed || this.approved || this.verified?.length || this.cancelled) {
|
|
35
35
|
throw new SchemaError_1.AttestationError("ATTEST_ERROR", "Cannot edit milestone that is not in PENDING state");
|
|
36
36
|
}
|
|
37
37
|
// Step 1: Revoke current attestation
|
|
@@ -216,6 +216,55 @@ class Milestone extends Attestation_1.Attestation {
|
|
|
216
216
|
]);
|
|
217
217
|
return { tx, uids };
|
|
218
218
|
}
|
|
219
|
+
/**
|
|
220
|
+
* Cancels this milestone. A cancelled milestone will not be delivered and is
|
|
221
|
+
* kept, badged "cancelled", for the program's records. Only milestones that
|
|
222
|
+
* are not yet completed or verified can be cancelled.
|
|
223
|
+
* @param signer
|
|
224
|
+
* @param reason
|
|
225
|
+
*/
|
|
226
|
+
async cancel(signer, reason = "", callback) {
|
|
227
|
+
if (this.completed || this.verified?.length)
|
|
228
|
+
throw new SchemaError_1.AttestationError("ATTEST_ERROR", "Cannot cancel a completed or verified milestone");
|
|
229
|
+
if (this.cancelled)
|
|
230
|
+
throw new SchemaError_1.AttestationError("ATTEST_ERROR", "Milestone is already cancelled");
|
|
231
|
+
const schema = this.schema.gap.findSchema("MilestoneCompleted");
|
|
232
|
+
if (this.schema.isJsonSchema()) {
|
|
233
|
+
schema.setValue("json", JSON.stringify({ type: "cancelled", reason }));
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
schema.setValue("type", "cancelled");
|
|
237
|
+
schema.setValue("reason", reason);
|
|
238
|
+
}
|
|
239
|
+
const { tx, uids } = await this.attestStatus(signer, schema, callback);
|
|
240
|
+
this.cancelled = new attestations_1.MilestoneCompleted({
|
|
241
|
+
data: {
|
|
242
|
+
type: "cancelled",
|
|
243
|
+
reason,
|
|
244
|
+
},
|
|
245
|
+
refUID: this.uid,
|
|
246
|
+
uid: uids?.[0],
|
|
247
|
+
schema: schema,
|
|
248
|
+
recipient: this.recipient,
|
|
249
|
+
});
|
|
250
|
+
return { tx, uids };
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Revokes the cancelled status of the milestone (un-cancel). If the milestone
|
|
254
|
+
* is not cancelled, it will throw an error.
|
|
255
|
+
* @param signer
|
|
256
|
+
*/
|
|
257
|
+
async revokeCancel(signer) {
|
|
258
|
+
if (!this.cancelled)
|
|
259
|
+
throw new SchemaError_1.AttestationError("ATTEST_ERROR", "Milestone is not cancelled");
|
|
260
|
+
const { tx, uids } = await this.cancelled.schema.multiRevoke(signer, [
|
|
261
|
+
{
|
|
262
|
+
schemaId: this.cancelled.schema.uid,
|
|
263
|
+
uid: this.cancelled.uid,
|
|
264
|
+
},
|
|
265
|
+
]);
|
|
266
|
+
return { tx, uids };
|
|
267
|
+
}
|
|
219
268
|
/**
|
|
220
269
|
* Revokes multiple milestone attestations at once.
|
|
221
270
|
* This method can be used to revoke multiple milestone attestations in a single transaction.
|
|
@@ -589,6 +638,16 @@ class Milestone extends Attestation_1.Attestation {
|
|
|
589
638
|
chainID: attestation.chainID,
|
|
590
639
|
});
|
|
591
640
|
}
|
|
641
|
+
if (attestation.cancelled) {
|
|
642
|
+
milestone.cancelled = new attestations_1.MilestoneCompleted({
|
|
643
|
+
...attestation.cancelled,
|
|
644
|
+
data: {
|
|
645
|
+
...attestation.cancelled.data,
|
|
646
|
+
},
|
|
647
|
+
schema: new AllGapSchemas_1.AllGapSchemas().findSchema("MilestoneCompleted", consts_1.chainIdToNetwork[attestation.chainID]),
|
|
648
|
+
chainID: attestation.chainID,
|
|
649
|
+
});
|
|
650
|
+
}
|
|
592
651
|
if (attestation.verified?.length > 0) {
|
|
593
652
|
milestone.verified = attestation.verified.map((m) => new attestations_1.MilestoneCompleted({
|
|
594
653
|
...m,
|
|
@@ -50,6 +50,7 @@ export interface IMilestoneResponse extends IAttestationResponse {
|
|
|
50
50
|
completed?: IMilestoneCompleted;
|
|
51
51
|
approved?: IMilestoneCompleted;
|
|
52
52
|
rejected?: IMilestoneCompleted;
|
|
53
|
+
cancelled?: IMilestoneCompleted;
|
|
53
54
|
verified?: IMilestoneCompleted[];
|
|
54
55
|
data: {
|
|
55
56
|
title: string;
|
|
@@ -91,12 +91,12 @@ export declare class MemberDetails extends Attestation<IMemberDetails> implement
|
|
|
91
91
|
profilePictureURL: string;
|
|
92
92
|
}
|
|
93
93
|
export interface IMilestoneCompleted {
|
|
94
|
-
type?: "approved" | "rejected" | "completed" | "verified";
|
|
94
|
+
type?: "approved" | "rejected" | "completed" | "verified" | "cancelled";
|
|
95
95
|
reason?: string;
|
|
96
96
|
proofOfWork?: string;
|
|
97
97
|
}
|
|
98
98
|
export declare class MilestoneCompleted extends Attestation<IMilestoneCompleted> implements IMilestoneCompleted {
|
|
99
|
-
type: "approved" | "rejected" | "completed" | "verified";
|
|
99
|
+
type: "approved" | "rejected" | "completed" | "verified" | "cancelled";
|
|
100
100
|
reason?: string;
|
|
101
101
|
proofOfWork?: string;
|
|
102
102
|
}
|
package/package.json
CHANGED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for Milestone.edit() and Milestone.editCompletion() convenience methods.
|
|
3
|
-
*
|
|
4
|
-
* Phase 3: edit() performs revoke-and-re-attest for milestone definitions:
|
|
5
|
-
* 1. Guard: only PENDING milestones (no completed, approved, or verified)
|
|
6
|
-
* 2. Revoke the current attestation via this.revoke()
|
|
7
|
-
* 3. Update data via this.setValues() (merges old + new fields)
|
|
8
|
-
* 4. Re-attest via this.attest()
|
|
9
|
-
*
|
|
10
|
-
* Phase 4: editCompletion() performs revoke-and-re-complete for completed milestones:
|
|
11
|
-
* 1. Guard: must be COMPLETED but not APPROVED or VERIFIED
|
|
12
|
-
* 2. Revoke current completion via this.revokeCompletion()
|
|
13
|
-
* 3. Re-complete with updated data via this.complete()
|
|
14
|
-
*
|
|
15
|
-
* Since the SDK has no Jest config, these tests define the behavioral contract.
|
|
16
|
-
* They simulate the methods' behavior to validate the expected flow without
|
|
17
|
-
* requiring the full EAS/blockchain dependency chain. When a Jest config is added,
|
|
18
|
-
* they will become directly runnable.
|
|
19
|
-
*/
|
|
20
|
-
export {};
|
|
@@ -1,328 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Tests for Milestone.edit() and Milestone.editCompletion() convenience methods.
|
|
4
|
-
*
|
|
5
|
-
* Phase 3: edit() performs revoke-and-re-attest for milestone definitions:
|
|
6
|
-
* 1. Guard: only PENDING milestones (no completed, approved, or verified)
|
|
7
|
-
* 2. Revoke the current attestation via this.revoke()
|
|
8
|
-
* 3. Update data via this.setValues() (merges old + new fields)
|
|
9
|
-
* 4. Re-attest via this.attest()
|
|
10
|
-
*
|
|
11
|
-
* Phase 4: editCompletion() performs revoke-and-re-complete for completed milestones:
|
|
12
|
-
* 1. Guard: must be COMPLETED but not APPROVED or VERIFIED
|
|
13
|
-
* 2. Revoke current completion via this.revokeCompletion()
|
|
14
|
-
* 3. Re-complete with updated data via this.complete()
|
|
15
|
-
*
|
|
16
|
-
* Since the SDK has no Jest config, these tests define the behavioral contract.
|
|
17
|
-
* They simulate the methods' behavior to validate the expected flow without
|
|
18
|
-
* requiring the full EAS/blockchain dependency chain. When a Jest config is added,
|
|
19
|
-
* they will become directly runnable.
|
|
20
|
-
*/
|
|
21
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
// Phase 3: Milestone.edit()
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
describe("Milestone.edit()", () => {
|
|
26
|
-
let mockSigner;
|
|
27
|
-
beforeEach(() => {
|
|
28
|
-
mockSigner = {};
|
|
29
|
-
});
|
|
30
|
-
/**
|
|
31
|
-
* Creates a test milestone simulating the real Milestone class behavior.
|
|
32
|
-
* Uses revoke() + setValues() + attest() internally, matching the actual
|
|
33
|
-
* implementation in core/class/entities/Milestone.ts.
|
|
34
|
-
*/
|
|
35
|
-
function createTestMilestone(overrides) {
|
|
36
|
-
const revokeMock = jest.fn().mockResolvedValue({ tx: {}, uids: [] });
|
|
37
|
-
const attestMock = jest.fn().mockResolvedValue({
|
|
38
|
-
tx: { hash: "0xTxHash" },
|
|
39
|
-
uids: ["0xNewUID"]
|
|
40
|
-
});
|
|
41
|
-
const setValuesMock = jest.fn();
|
|
42
|
-
const milestone = {
|
|
43
|
-
uid: "0xOldMilestoneUID",
|
|
44
|
-
title: "Original Title",
|
|
45
|
-
description: "Original Description",
|
|
46
|
-
endsAt: 1735689600,
|
|
47
|
-
startsAt: 1704067200,
|
|
48
|
-
type: "milestone",
|
|
49
|
-
completed: overrides?.completed || undefined,
|
|
50
|
-
approved: overrides?.approved || undefined,
|
|
51
|
-
verified: overrides?.verified || [],
|
|
52
|
-
recipient: "0xRecipient",
|
|
53
|
-
data: {
|
|
54
|
-
title: "Original Title",
|
|
55
|
-
description: "Original Description",
|
|
56
|
-
endsAt: 1735689600,
|
|
57
|
-
startsAt: 1704067200
|
|
58
|
-
},
|
|
59
|
-
// Mock methods matching real Attestation class
|
|
60
|
-
revoke: revokeMock,
|
|
61
|
-
attest: attestMock,
|
|
62
|
-
setValues: setValuesMock,
|
|
63
|
-
// Expose mocks for assertions
|
|
64
|
-
_mocks: { revoke: revokeMock, attest: attestMock, setValues: setValuesMock }
|
|
65
|
-
};
|
|
66
|
-
// Simulate the edit method matching the real implementation
|
|
67
|
-
milestone.edit = async function (signer, newData, callback) {
|
|
68
|
-
// Guard: only PENDING milestones
|
|
69
|
-
if (this.completed || this.approved || (this.verified && this.verified.length > 0)) {
|
|
70
|
-
throw new Error("Cannot edit milestone that is not in PENDING state");
|
|
71
|
-
}
|
|
72
|
-
// Step 1: Revoke current attestation
|
|
73
|
-
await this.revoke(signer, callback);
|
|
74
|
-
// Step 2: Update data with new fields
|
|
75
|
-
const updatedData = { ...this.data, ...newData };
|
|
76
|
-
this.setValues(updatedData);
|
|
77
|
-
// Step 3: Re-attest with updated data
|
|
78
|
-
return this.attest(signer, callback);
|
|
79
|
-
};
|
|
80
|
-
return milestone;
|
|
81
|
-
}
|
|
82
|
-
describe("successful edit (PENDING milestone)", () => {
|
|
83
|
-
it("should call revoke on current attestation, then re-attest", async () => {
|
|
84
|
-
const milestone = createTestMilestone();
|
|
85
|
-
await milestone.edit(mockSigner, {
|
|
86
|
-
title: "Updated Title",
|
|
87
|
-
description: "Updated Description"
|
|
88
|
-
});
|
|
89
|
-
expect(milestone._mocks.revoke).toHaveBeenCalledTimes(1);
|
|
90
|
-
expect(milestone._mocks.revoke).toHaveBeenCalledWith(mockSigner, undefined);
|
|
91
|
-
expect(milestone._mocks.attest).toHaveBeenCalledTimes(1);
|
|
92
|
-
expect(milestone._mocks.attest).toHaveBeenCalledWith(mockSigner, undefined);
|
|
93
|
-
});
|
|
94
|
-
it("should call setValues with merged data (old + new)", async () => {
|
|
95
|
-
const milestone = createTestMilestone();
|
|
96
|
-
await milestone.edit(mockSigner, {
|
|
97
|
-
title: "New Title"
|
|
98
|
-
});
|
|
99
|
-
expect(milestone._mocks.setValues).toHaveBeenCalledTimes(1);
|
|
100
|
-
expect(milestone._mocks.setValues).toHaveBeenCalledWith(expect.objectContaining({
|
|
101
|
-
title: "New Title",
|
|
102
|
-
description: "Original Description",
|
|
103
|
-
endsAt: 1735689600,
|
|
104
|
-
startsAt: 1704067200
|
|
105
|
-
}));
|
|
106
|
-
});
|
|
107
|
-
it("should preserve unchanged fields during partial update", async () => {
|
|
108
|
-
const milestone = createTestMilestone();
|
|
109
|
-
await milestone.edit(mockSigner, {
|
|
110
|
-
title: "New Title Only"
|
|
111
|
-
});
|
|
112
|
-
const setValuesArg = milestone._mocks.setValues.mock.calls[0][0];
|
|
113
|
-
expect(setValuesArg.description).toBe("Original Description");
|
|
114
|
-
expect(setValuesArg.endsAt).toBe(1735689600);
|
|
115
|
-
expect(setValuesArg.startsAt).toBe(1704067200);
|
|
116
|
-
});
|
|
117
|
-
it("should update all supported fields when fully provided", async () => {
|
|
118
|
-
const milestone = createTestMilestone();
|
|
119
|
-
await milestone.edit(mockSigner, {
|
|
120
|
-
title: "New Title",
|
|
121
|
-
description: "New Description",
|
|
122
|
-
endsAt: 1767225600,
|
|
123
|
-
startsAt: 1735689600,
|
|
124
|
-
priority: 1
|
|
125
|
-
});
|
|
126
|
-
const setValuesArg = milestone._mocks.setValues.mock.calls[0][0];
|
|
127
|
-
expect(setValuesArg.title).toBe("New Title");
|
|
128
|
-
expect(setValuesArg.description).toBe("New Description");
|
|
129
|
-
expect(setValuesArg.endsAt).toBe(1767225600);
|
|
130
|
-
expect(setValuesArg.startsAt).toBe(1735689600);
|
|
131
|
-
expect(setValuesArg.priority).toBe(1);
|
|
132
|
-
});
|
|
133
|
-
it("should return the result from attest()", async () => {
|
|
134
|
-
const milestone = createTestMilestone();
|
|
135
|
-
const result = await milestone.edit(mockSigner, { title: "New" });
|
|
136
|
-
expect(result).toEqual({
|
|
137
|
-
tx: { hash: "0xTxHash" },
|
|
138
|
-
uids: ["0xNewUID"]
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
describe("state guards", () => {
|
|
143
|
-
it("should throw for COMPLETED milestones", async () => {
|
|
144
|
-
const milestone = createTestMilestone({
|
|
145
|
-
completed: {
|
|
146
|
-
uid: "0xCompletedUID",
|
|
147
|
-
data: { type: "completed", reason: "Done" }
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
await expect(milestone.edit(mockSigner, { title: "Nope" })).rejects.toThrow("Cannot edit milestone that is not in PENDING state");
|
|
151
|
-
expect(milestone._mocks.revoke).not.toHaveBeenCalled();
|
|
152
|
-
});
|
|
153
|
-
it("should throw for APPROVED milestones", async () => {
|
|
154
|
-
const milestone = createTestMilestone({
|
|
155
|
-
approved: {
|
|
156
|
-
uid: "0xApprovedUID",
|
|
157
|
-
data: { type: "approved", reason: "Good" }
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
await expect(milestone.edit(mockSigner, { title: "Nope" })).rejects.toThrow("Cannot edit milestone that is not in PENDING state");
|
|
161
|
-
});
|
|
162
|
-
it("should throw for VERIFIED milestones (verified.length > 0)", async () => {
|
|
163
|
-
const milestone = createTestMilestone({
|
|
164
|
-
verified: [
|
|
165
|
-
{
|
|
166
|
-
uid: "0xVerifiedUID",
|
|
167
|
-
data: { type: "verified", reason: "Verified" }
|
|
168
|
-
}
|
|
169
|
-
]
|
|
170
|
-
});
|
|
171
|
-
await expect(milestone.edit(mockSigner, { title: "Nope" })).rejects.toThrow("Cannot edit milestone that is not in PENDING state");
|
|
172
|
-
});
|
|
173
|
-
it("should allow edit when completed/approved/verified are all falsy/empty", async () => {
|
|
174
|
-
const milestone = createTestMilestone({
|
|
175
|
-
completed: undefined,
|
|
176
|
-
approved: undefined,
|
|
177
|
-
verified: []
|
|
178
|
-
});
|
|
179
|
-
await expect(milestone.edit(mockSigner, { title: "Allowed" })).resolves.not.toThrow();
|
|
180
|
-
expect(milestone._mocks.revoke).toHaveBeenCalledTimes(1);
|
|
181
|
-
expect(milestone._mocks.attest).toHaveBeenCalledTimes(1);
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
describe("execution order", () => {
|
|
185
|
-
it("should call revoke BEFORE setValues and attest (correct ordering)", async () => {
|
|
186
|
-
const callOrder = [];
|
|
187
|
-
const milestone = createTestMilestone();
|
|
188
|
-
milestone._mocks.revoke.mockImplementation(async () => {
|
|
189
|
-
callOrder.push("revoke");
|
|
190
|
-
return { tx: {}, uids: [] };
|
|
191
|
-
});
|
|
192
|
-
milestone._mocks.setValues.mockImplementation(() => {
|
|
193
|
-
callOrder.push("setValues");
|
|
194
|
-
});
|
|
195
|
-
milestone._mocks.attest.mockImplementation(async () => {
|
|
196
|
-
callOrder.push("attest");
|
|
197
|
-
return { tx: {}, uids: ["0xNew"] };
|
|
198
|
-
});
|
|
199
|
-
await milestone.edit(mockSigner, {
|
|
200
|
-
title: "New"
|
|
201
|
-
});
|
|
202
|
-
expect(callOrder).toEqual(["revoke", "setValues", "attest"]);
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
// ---------------------------------------------------------------------------
|
|
207
|
-
// Phase 4: Milestone.editCompletion()
|
|
208
|
-
// ---------------------------------------------------------------------------
|
|
209
|
-
describe("Milestone.editCompletion()", () => {
|
|
210
|
-
let mockSigner;
|
|
211
|
-
beforeEach(() => {
|
|
212
|
-
mockSigner = {};
|
|
213
|
-
});
|
|
214
|
-
/**
|
|
215
|
-
* Creates a test milestone with completion data for testing editCompletion().
|
|
216
|
-
*/
|
|
217
|
-
function createCompletedMilestone(overrides) {
|
|
218
|
-
const revokeCompletionMock = jest
|
|
219
|
-
.fn()
|
|
220
|
-
.mockResolvedValue({ tx: {}, uids: [] });
|
|
221
|
-
const completeMock = jest.fn().mockResolvedValue({
|
|
222
|
-
tx: { hash: "0xCompletionTxHash" },
|
|
223
|
-
uids: ["0xNewCompletionUID"]
|
|
224
|
-
});
|
|
225
|
-
const milestone = {
|
|
226
|
-
uid: "0xMilestoneUID",
|
|
227
|
-
completed: {
|
|
228
|
-
uid: "0xOldCompletionUID",
|
|
229
|
-
data: {
|
|
230
|
-
reason: "Original completion reason",
|
|
231
|
-
proofOfWork: "https://example.com/original-proof"
|
|
232
|
-
}
|
|
233
|
-
},
|
|
234
|
-
approved: overrides?.approved || undefined,
|
|
235
|
-
verified: overrides?.verified || [],
|
|
236
|
-
revokeCompletion: revokeCompletionMock,
|
|
237
|
-
complete: completeMock,
|
|
238
|
-
_mocks: { revokeCompletion: revokeCompletionMock, complete: completeMock }
|
|
239
|
-
};
|
|
240
|
-
// Simulate editCompletion matching real implementation
|
|
241
|
-
milestone.editCompletion = async function (signer, newData, callback) {
|
|
242
|
-
if (!this.completed) {
|
|
243
|
-
throw new Error("Milestone is not completed");
|
|
244
|
-
}
|
|
245
|
-
if (this.approved) {
|
|
246
|
-
throw new Error("Cannot edit completion of an approved milestone");
|
|
247
|
-
}
|
|
248
|
-
if (this.verified && this.verified.length > 0) {
|
|
249
|
-
throw new Error("Cannot edit completion of a verified milestone");
|
|
250
|
-
}
|
|
251
|
-
await this.revokeCompletion(signer, callback);
|
|
252
|
-
const completionData = {
|
|
253
|
-
reason: newData.reason ?? this.completed?.data?.reason ?? "",
|
|
254
|
-
proofOfWork: newData.proofOfWork ?? this.completed?.data?.proofOfWork ?? ""
|
|
255
|
-
};
|
|
256
|
-
return this.complete(signer, completionData, callback);
|
|
257
|
-
};
|
|
258
|
-
return milestone;
|
|
259
|
-
}
|
|
260
|
-
describe("successful editCompletion", () => {
|
|
261
|
-
it("should revoke old completion and re-complete with updated data", async () => {
|
|
262
|
-
const milestone = createCompletedMilestone();
|
|
263
|
-
await milestone.editCompletion(mockSigner, {
|
|
264
|
-
reason: "Updated completion reason",
|
|
265
|
-
proofOfWork: "https://example.com/updated-proof"
|
|
266
|
-
});
|
|
267
|
-
expect(milestone._mocks.revokeCompletion).toHaveBeenCalledTimes(1);
|
|
268
|
-
expect(milestone._mocks.complete).toHaveBeenCalledTimes(1);
|
|
269
|
-
expect(milestone._mocks.complete).toHaveBeenCalledWith(mockSigner, {
|
|
270
|
-
reason: "Updated completion reason",
|
|
271
|
-
proofOfWork: "https://example.com/updated-proof"
|
|
272
|
-
}, undefined);
|
|
273
|
-
});
|
|
274
|
-
it("should preserve unchanged fields from original completion", async () => {
|
|
275
|
-
const milestone = createCompletedMilestone();
|
|
276
|
-
// Only update reason, proofOfWork should be preserved from original
|
|
277
|
-
await milestone.editCompletion(mockSigner, {
|
|
278
|
-
reason: "New reason only"
|
|
279
|
-
});
|
|
280
|
-
expect(milestone._mocks.complete).toHaveBeenCalledWith(mockSigner, {
|
|
281
|
-
reason: "New reason only",
|
|
282
|
-
proofOfWork: "https://example.com/original-proof"
|
|
283
|
-
}, undefined);
|
|
284
|
-
});
|
|
285
|
-
it("should return the result from complete()", async () => {
|
|
286
|
-
const milestone = createCompletedMilestone();
|
|
287
|
-
const result = await milestone.editCompletion(mockSigner, { reason: "Updated" });
|
|
288
|
-
expect(result).toEqual({
|
|
289
|
-
tx: { hash: "0xCompletionTxHash" },
|
|
290
|
-
uids: ["0xNewCompletionUID"]
|
|
291
|
-
});
|
|
292
|
-
});
|
|
293
|
-
});
|
|
294
|
-
describe("state guards", () => {
|
|
295
|
-
it("should throw if milestone is not completed", async () => {
|
|
296
|
-
const milestone = createCompletedMilestone();
|
|
297
|
-
milestone.completed = undefined;
|
|
298
|
-
await expect(milestone.editCompletion(mockSigner, {
|
|
299
|
-
reason: "Nope"
|
|
300
|
-
})).rejects.toThrow("Milestone is not completed");
|
|
301
|
-
expect(milestone._mocks.revokeCompletion).not.toHaveBeenCalled();
|
|
302
|
-
});
|
|
303
|
-
it("should throw if milestone is approved", async () => {
|
|
304
|
-
const milestone = createCompletedMilestone({
|
|
305
|
-
approved: {
|
|
306
|
-
uid: "0xApprovedUID",
|
|
307
|
-
data: { type: "approved", reason: "Approved" }
|
|
308
|
-
}
|
|
309
|
-
});
|
|
310
|
-
await expect(milestone.editCompletion(mockSigner, {
|
|
311
|
-
reason: "Nope"
|
|
312
|
-
})).rejects.toThrow("Cannot edit completion of an approved milestone");
|
|
313
|
-
});
|
|
314
|
-
it("should throw if milestone is verified", async () => {
|
|
315
|
-
const milestone = createCompletedMilestone({
|
|
316
|
-
verified: [
|
|
317
|
-
{
|
|
318
|
-
uid: "0xVerifiedUID",
|
|
319
|
-
data: { type: "verified" }
|
|
320
|
-
}
|
|
321
|
-
]
|
|
322
|
-
});
|
|
323
|
-
await expect(milestone.editCompletion(mockSigner, {
|
|
324
|
-
reason: "Nope"
|
|
325
|
-
})).rejects.toThrow("Cannot edit completion of a verified milestone");
|
|
326
|
-
});
|
|
327
|
-
});
|
|
328
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const attestationGrantDetails = {
|
|
4
|
-
title: "Grant",
|
|
5
|
-
proposalURL: "https://example.com",
|
|
6
|
-
type: "grant-details",
|
|
7
|
-
receivedDate: 1704844800
|
|
8
|
-
};
|
|
9
|
-
const indexerGrantDetails = {
|
|
10
|
-
id: "attestation-id",
|
|
11
|
-
uid: "0x1111111111111111111111111111111111111111111111111111111111111111",
|
|
12
|
-
schemaUID: "0x2222222222222222222222222222222222222222222222222222222222222222",
|
|
13
|
-
refUID: "0x0000000000000000000000000000000000000000000000000000000000000000",
|
|
14
|
-
attester: "0x3333333333333333333333333333333333333333333333333333333333333333",
|
|
15
|
-
recipient: "0x4444444444444444444444444444444444444444444444444444444444444444",
|
|
16
|
-
revoked: false,
|
|
17
|
-
createdAt: new Date().toISOString(),
|
|
18
|
-
updatedAt: new Date().toISOString(),
|
|
19
|
-
chainID: 10,
|
|
20
|
-
type: "GrantDetails",
|
|
21
|
-
decodedDataJson: "{}",
|
|
22
|
-
isOffchain: false,
|
|
23
|
-
revocable: true,
|
|
24
|
-
schemaId: "0x5555555555555555555555555555555555555555555555555555555555555555",
|
|
25
|
-
data: {
|
|
26
|
-
title: "Grant",
|
|
27
|
-
amount: "1000",
|
|
28
|
-
description: "Description",
|
|
29
|
-
proposalURL: "https://example.com",
|
|
30
|
-
payoutAddress: "0x6666666666666666666666666666666666666666666666666666666666666666",
|
|
31
|
-
questions: [],
|
|
32
|
-
type: "grant-details",
|
|
33
|
-
receivedDate: 1704844800
|
|
34
|
-
}
|
|
35
|
-
};
|
|
36
|
-
const attestationReceivedDate = attestationGrantDetails.receivedDate;
|
|
37
|
-
const indexerReceivedDate = indexerGrantDetails.data.receivedDate;
|
|
38
|
-
void attestationReceivedDate;
|
|
39
|
-
void indexerReceivedDate;
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { RemoteStorage } from "./RemoteStorage";
|
|
2
|
-
import { TRemoteStorageOutput } from "core/types";
|
|
3
|
-
export interface IpfsStorageOptions {
|
|
4
|
-
token: string;
|
|
5
|
-
}
|
|
6
|
-
export declare class IpfsStorage extends RemoteStorage {
|
|
7
|
-
private pinataJWTToken;
|
|
8
|
-
constructor(opts: IpfsStorageOptions,
|
|
9
|
-
/**
|
|
10
|
-
* If set, will send request to another server instead of
|
|
11
|
-
* using the local instance
|
|
12
|
-
*/
|
|
13
|
-
sponsor?: RemoteStorage["sponsor"]);
|
|
14
|
-
private assert;
|
|
15
|
-
save<T = unknown>(data: T): Promise<string>;
|
|
16
|
-
encode(data: string): TRemoteStorageOutput<string>;
|
|
17
|
-
get<T = unknown>(args: {
|
|
18
|
-
cid: string;
|
|
19
|
-
}): Promise<T>;
|
|
20
|
-
saveAndGetCID(data: any, pinataMetadata?: {
|
|
21
|
-
name: string;
|
|
22
|
-
}): Promise<any>;
|
|
23
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.IpfsStorage = void 0;
|
|
7
|
-
const RemoteStorage_1 = require("./RemoteStorage");
|
|
8
|
-
const SchemaError_1 = require("../SchemaError");
|
|
9
|
-
const utils_1 = require("../../utils");
|
|
10
|
-
const axios_1 = __importDefault(require("axios"));
|
|
11
|
-
class IpfsStorage extends RemoteStorage_1.RemoteStorage {
|
|
12
|
-
constructor(opts,
|
|
13
|
-
/**
|
|
14
|
-
* If set, will send request to another server instead of
|
|
15
|
-
* using the local instance
|
|
16
|
-
*/
|
|
17
|
-
sponsor) {
|
|
18
|
-
super(0 /* STORAGE_TYPE.IPFS */, sponsor);
|
|
19
|
-
this.assert(opts);
|
|
20
|
-
this.pinataJWTToken = opts.token;
|
|
21
|
-
}
|
|
22
|
-
assert(opts) { }
|
|
23
|
-
async save(data) {
|
|
24
|
-
try {
|
|
25
|
-
const cid = await this.saveAndGetCID(data);
|
|
26
|
-
return cid;
|
|
27
|
-
}
|
|
28
|
-
catch (error) {
|
|
29
|
-
throw new SchemaError_1.RemoteStorageError("REMOTE_STORAGE_UPLOAD", `Error adding data to IPFS`);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
encode(data) {
|
|
33
|
-
return { hash: data, storageType: this.storageType };
|
|
34
|
-
}
|
|
35
|
-
async get(args) {
|
|
36
|
-
return (0, utils_1.getIPFSData)(args.cid);
|
|
37
|
-
}
|
|
38
|
-
async saveAndGetCID(data, pinataMetadata = { name: "via karma-gap-sdk" }) {
|
|
39
|
-
try {
|
|
40
|
-
const res = await axios_1.default.post("https://api.pinata.cloud/pinning/pinJSONToIPFS", {
|
|
41
|
-
pinataContent: data,
|
|
42
|
-
pinataMetadata: pinataMetadata,
|
|
43
|
-
}, {
|
|
44
|
-
headers: {
|
|
45
|
-
"Content-Type": "application/json",
|
|
46
|
-
Authorization: `Bearer ${this.pinataJWTToken}`,
|
|
47
|
-
},
|
|
48
|
-
});
|
|
49
|
-
return res.data.IpfsHash;
|
|
50
|
-
}
|
|
51
|
-
catch (error) {
|
|
52
|
-
throw new SchemaError_1.RemoteStorageError("REMOTE_STORAGE_UPLOAD", `Error adding data to IPFS`);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
exports.IpfsStorage = IpfsStorage;
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { STORAGE_TYPE, TRemoteStorageOutput } from 'core/types';
|
|
2
|
-
interface SponsoredRemote {
|
|
3
|
-
url: string;
|
|
4
|
-
responseParser: (response: any) => string;
|
|
5
|
-
}
|
|
6
|
-
export declare abstract class RemoteStorage<C = unknown> {
|
|
7
|
-
protected client: C;
|
|
8
|
-
readonly storageType: number;
|
|
9
|
-
readonly sponsor?: SponsoredRemote;
|
|
10
|
-
constructor(storageType: STORAGE_TYPE,
|
|
11
|
-
/**
|
|
12
|
-
* If set, will try to POST request to another server instead of
|
|
13
|
-
* using the local instance.
|
|
14
|
-
*
|
|
15
|
-
* > If a response parser is not set, it will try to get { cid: string }.
|
|
16
|
-
*/
|
|
17
|
-
sponsor: SponsoredRemote);
|
|
18
|
-
/**
|
|
19
|
-
* Try to save data to remote storage and return the CID.
|
|
20
|
-
* IF sponsorUrl is set, this method will be automatically
|
|
21
|
-
* intercepted and will send a POST request to the sponsorUrl
|
|
22
|
-
* with the contents: `{ data: T, type: "<AttestationType>" }`
|
|
23
|
-
*/
|
|
24
|
-
abstract save<T = unknown>(data: T, schemaName: string): Promise<string>;
|
|
25
|
-
/**
|
|
26
|
-
* Encodes the data according to the remote storage type parameters
|
|
27
|
-
* OR returns the data as is if no encoding is required
|
|
28
|
-
*/
|
|
29
|
-
abstract encode(data: unknown): TRemoteStorageOutput;
|
|
30
|
-
/**
|
|
31
|
-
* Get data from Remote Storage
|
|
32
|
-
*/
|
|
33
|
-
abstract get<T = unknown>(args: unknown): Promise<T>;
|
|
34
|
-
/**
|
|
35
|
-
* If sponsorUrl is set, intercept the save method and send a POST request
|
|
36
|
-
* to the sponsorUrl instead of using the local instance.
|
|
37
|
-
* @returns
|
|
38
|
-
*/
|
|
39
|
-
private interceptRemoteStorage;
|
|
40
|
-
}
|
|
41
|
-
export {};
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.RemoteStorage = void 0;
|
|
7
|
-
const axios_1 = __importDefault(require("axios"));
|
|
8
|
-
class RemoteStorage {
|
|
9
|
-
constructor(storageType,
|
|
10
|
-
/**
|
|
11
|
-
* If set, will try to POST request to another server instead of
|
|
12
|
-
* using the local instance.
|
|
13
|
-
*
|
|
14
|
-
* > If a response parser is not set, it will try to get { cid: string }.
|
|
15
|
-
*/
|
|
16
|
-
sponsor) {
|
|
17
|
-
this.storageType = storageType;
|
|
18
|
-
this.sponsor = sponsor;
|
|
19
|
-
this.interceptRemoteStorage();
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* If sponsorUrl is set, intercept the save method and send a POST request
|
|
23
|
-
* to the sponsorUrl instead of using the local instance.
|
|
24
|
-
* @returns
|
|
25
|
-
*/
|
|
26
|
-
interceptRemoteStorage() {
|
|
27
|
-
if (!this.sponsor?.url)
|
|
28
|
-
return;
|
|
29
|
-
this.save = async (data, schemaName) => {
|
|
30
|
-
const { data: response } = await axios_1.default.post(this.sponsor.url, {
|
|
31
|
-
data: data,
|
|
32
|
-
type: schemaName,
|
|
33
|
-
});
|
|
34
|
-
return this.sponsor.responseParser?.(response) || response.cid;
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
exports.RemoteStorage = RemoteStorage;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function getIPFSData<T>(cid: string): Promise<T>;
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.getIPFSData = getIPFSData;
|
|
7
|
-
const axios_1 = __importDefault(require("axios"));
|
|
8
|
-
async function getIPFSData(cid) {
|
|
9
|
-
try {
|
|
10
|
-
const { data } = await axios_1.default.get(`https://ipfs.io/ipfs/${cid}`, {
|
|
11
|
-
timeout: 5000,
|
|
12
|
-
});
|
|
13
|
-
return data;
|
|
14
|
-
}
|
|
15
|
-
catch (err) {
|
|
16
|
-
console.error(err);
|
|
17
|
-
throw new Error(`Error to retrive data for CID: ${cid}`);
|
|
18
|
-
}
|
|
19
|
-
}
|