@show-karma/karma-gap-sdk 0.4.22 → 0.4.24
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/__tests__/Milestone.edit.test.d.ts +20 -0
- package/core/__tests__/Milestone.edit.test.js +328 -0
- package/core/class/entities/Milestone.d.ts +21 -0
- package/core/class/entities/Milestone.js +47 -0
- package/core/class/entities/Project.js +2 -2
- package/package.json +2 -2
- 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
|
@@ -0,0 +1,20 @@
|
|
|
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 {};
|
|
@@ -0,0 +1,328 @@
|
|
|
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
|
+
});
|
|
@@ -33,6 +33,27 @@ export declare class Milestone extends Attestation<IMilestone> implements IMiles
|
|
|
33
33
|
verified: MilestoneCompleted[];
|
|
34
34
|
type: string;
|
|
35
35
|
priority?: number;
|
|
36
|
+
/**
|
|
37
|
+
* Edits this milestone by revoking the current attestation and re-attesting with updated data.
|
|
38
|
+
* Only milestones in PENDING state (not completed, approved, or verified) can be edited.
|
|
39
|
+
* @param signer - The signer to use for revocation and re-attestation
|
|
40
|
+
* @param newData - Partial milestone data to update (only provided fields are overridden)
|
|
41
|
+
* @param callback - Optional callback function for status updates
|
|
42
|
+
*/
|
|
43
|
+
edit(signer: SignerOrProvider, newData: Partial<IMilestone>, callback?: Function): Promise<AttestationWithTx>;
|
|
44
|
+
/**
|
|
45
|
+
* Edits the completion of this milestone by revoking the current completion
|
|
46
|
+
* attestation and re-completing with updated data.
|
|
47
|
+
* Only milestones in COMPLETED state (not yet approved or verified) can have
|
|
48
|
+
* their completion edited.
|
|
49
|
+
* @param signer - The signer to use for revocation and re-attestation
|
|
50
|
+
* @param newData - Updated completion data (reason and/or proofOfWork)
|
|
51
|
+
* @param callback - Optional callback function for status updates
|
|
52
|
+
*/
|
|
53
|
+
editCompletion(signer: SignerOrProvider, newData: {
|
|
54
|
+
reason?: string;
|
|
55
|
+
proofOfWork?: string;
|
|
56
|
+
}, callback?: Function): Promise<AttestationWithTx>;
|
|
36
57
|
/**
|
|
37
58
|
* Approves this milestone. If the milestone is not completed or already approved,
|
|
38
59
|
* it will throw an error.
|
|
@@ -23,6 +23,53 @@ class Milestone extends Attestation_1.Attestation {
|
|
|
23
23
|
this.verified = [];
|
|
24
24
|
this.type = "milestone";
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Edits this milestone by revoking the current attestation and re-attesting with updated data.
|
|
28
|
+
* Only milestones in PENDING state (not completed, approved, or verified) can be edited.
|
|
29
|
+
* @param signer - The signer to use for revocation and re-attestation
|
|
30
|
+
* @param newData - Partial milestone data to update (only provided fields are overridden)
|
|
31
|
+
* @param callback - Optional callback function for status updates
|
|
32
|
+
*/
|
|
33
|
+
async edit(signer, newData, callback) {
|
|
34
|
+
if (this.completed || this.approved || this.verified?.length) {
|
|
35
|
+
throw new SchemaError_1.AttestationError("ATTEST_ERROR", "Cannot edit milestone that is not in PENDING state");
|
|
36
|
+
}
|
|
37
|
+
// Step 1: Revoke current attestation
|
|
38
|
+
await this.revoke(signer, callback);
|
|
39
|
+
// Step 2: Update data with new fields
|
|
40
|
+
const updatedData = { ...this.data, ...newData };
|
|
41
|
+
this.setValues(updatedData);
|
|
42
|
+
// Step 3: Re-attest with updated data
|
|
43
|
+
return this.attest(signer, callback);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Edits the completion of this milestone by revoking the current completion
|
|
47
|
+
* attestation and re-completing with updated data.
|
|
48
|
+
* Only milestones in COMPLETED state (not yet approved or verified) can have
|
|
49
|
+
* their completion edited.
|
|
50
|
+
* @param signer - The signer to use for revocation and re-attestation
|
|
51
|
+
* @param newData - Updated completion data (reason and/or proofOfWork)
|
|
52
|
+
* @param callback - Optional callback function for status updates
|
|
53
|
+
*/
|
|
54
|
+
async editCompletion(signer, newData, callback) {
|
|
55
|
+
if (!this.completed) {
|
|
56
|
+
throw new SchemaError_1.AttestationError("ATTEST_ERROR", "Milestone is not completed");
|
|
57
|
+
}
|
|
58
|
+
if (this.approved) {
|
|
59
|
+
throw new SchemaError_1.AttestationError("ATTEST_ERROR", "Cannot edit completion of an approved milestone");
|
|
60
|
+
}
|
|
61
|
+
if (this.verified?.length) {
|
|
62
|
+
throw new SchemaError_1.AttestationError("ATTEST_ERROR", "Cannot edit completion of a verified milestone");
|
|
63
|
+
}
|
|
64
|
+
// Step 1: Revoke current completion attestation
|
|
65
|
+
await this.revokeCompletion(signer, callback);
|
|
66
|
+
// Step 2: Re-complete with updated data
|
|
67
|
+
const completionData = {
|
|
68
|
+
reason: newData.reason ?? this.completed?.data?.reason ?? "",
|
|
69
|
+
proofOfWork: newData.proofOfWork ?? this.completed?.data?.proofOfWork ?? "",
|
|
70
|
+
};
|
|
71
|
+
return this.complete(signer, completionData, callback);
|
|
72
|
+
}
|
|
26
73
|
/**
|
|
27
74
|
* Approves this milestone. If the milestone is not completed or already approved,
|
|
28
75
|
* it will throw an error.
|
|
@@ -67,10 +67,10 @@ class Project extends Attestation_1.Attestation {
|
|
|
67
67
|
return { tx: txArray, uids: [this.uid] };
|
|
68
68
|
}
|
|
69
69
|
isOwner(signer, publicAddress) {
|
|
70
|
-
return GapContract_1.GapContract.isProjectOwner(signer, this.uid, this.chainID, publicAddress);
|
|
70
|
+
return GapContract_1.GapContract.isProjectOwner(signer, this.uid, this.chainID, publicAddress, this.schema.gap.rpcConfig);
|
|
71
71
|
}
|
|
72
72
|
isAdmin(signer, publicAddress) {
|
|
73
|
-
return GapContract_1.GapContract.isProjectAdmin(signer, this.uid, this.chainID, publicAddress);
|
|
73
|
+
return GapContract_1.GapContract.isProjectAdmin(signer, this.uid, this.chainID, publicAddress, this.schema.gap.rpcConfig);
|
|
74
74
|
}
|
|
75
75
|
/**
|
|
76
76
|
* Add new members to the project.
|
package/package.json
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.4.
|
|
7
|
-
"description": "Simple and easy interface between EAS and Karma
|
|
6
|
+
"version": "0.4.24",
|
|
7
|
+
"description": "Simple and easy interface between EAS and Karma.",
|
|
8
8
|
"main": "./index.js",
|
|
9
9
|
"author": "KarmaHQ",
|
|
10
10
|
"license": "MIT",
|
|
@@ -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
|
-
}
|