@show-karma/karma-gap-sdk 0.4.15 → 0.4.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/.cursorrules +43 -0
  2. package/core/abi/AirdropNFT.json +1 -1
  3. package/core/abi/Allo.json +860 -860
  4. package/core/abi/AlloRegistry.json +578 -578
  5. package/core/abi/CommunityResolverABI.json +506 -506
  6. package/core/abi/Donations.json +251 -251
  7. package/core/abi/EAS.json +1 -1
  8. package/core/abi/MultiAttester.json +746 -746
  9. package/core/abi/ProjectResolver.json +574 -574
  10. package/core/abi/SchemaRegistry.json +1 -1
  11. package/core/abi/index.ts +21 -0
  12. package/core/class/AllGapSchemas.ts +21 -0
  13. package/core/class/Attestation.ts +429 -0
  14. package/core/class/Fetcher.ts +224 -0
  15. package/core/class/GAP.ts +481 -0
  16. package/core/class/GapSchema.ts +93 -0
  17. package/core/class/Gelato/{Gelato.js → Gelato.ts} +23 -0
  18. package/core/class/GrantProgramRegistry/Allo.ts +188 -0
  19. package/core/class/GrantProgramRegistry/AlloRegistry.ts +101 -0
  20. package/core/class/GraphQL/AxiosGQL.ts +29 -0
  21. package/core/class/GraphQL/EASClient.ts +34 -0
  22. package/core/class/GraphQL/GapEasClient.ts +869 -0
  23. package/core/class/Schema.ts +659 -0
  24. package/core/class/SchemaError.ts +42 -0
  25. package/core/class/contract/GapContract.ts +457 -0
  26. package/core/class/entities/Community.ts +148 -0
  27. package/core/class/entities/ContributorProfile.ts +108 -0
  28. package/core/class/entities/Grant.ts +321 -0
  29. package/core/class/entities/GrantUpdate.ts +187 -0
  30. package/core/class/entities/MemberOf.ts +52 -0
  31. package/core/class/entities/Milestone.ts +898 -0
  32. package/core/class/entities/Project.ts +672 -0
  33. package/core/class/entities/ProjectImpact.ts +170 -0
  34. package/core/class/entities/ProjectMilestone.ts +254 -0
  35. package/core/class/entities/ProjectPointer.ts +39 -0
  36. package/core/class/entities/ProjectUpdate.ts +176 -0
  37. package/core/class/entities/Track.ts +32 -0
  38. package/core/class/karma-indexer/GapIndexerClient.ts +383 -0
  39. package/core/class/karma-indexer/api/GapIndexerApi.ts +446 -0
  40. package/core/class/karma-indexer/api/types.ts +313 -0
  41. package/core/class/remote-storage/IpfsStorage.ts +76 -0
  42. package/core/class/remote-storage/RemoteStorage.ts +65 -0
  43. package/core/class/types/allo.ts +93 -0
  44. package/core/class/types/attestations.ts +223 -0
  45. package/core/consts.ts +775 -0
  46. package/core/scripts/create-grant.ts +102 -0
  47. package/core/scripts/create-program.ts +43 -0
  48. package/core/scripts/create-schemas.ts +65 -0
  49. package/core/scripts/deploy.ts +65 -0
  50. package/core/scripts/index.ts +1 -0
  51. package/core/scripts/milestone-multi-grants.ts +125 -0
  52. package/core/shared/types.ts +13 -0
  53. package/core/types.ts +224 -0
  54. package/core/utils/gelato/send-gelato-txn.ts +114 -0
  55. package/core/utils/gelato/sponsor-handler.ts +77 -0
  56. package/core/utils/gelato/watch-gelato-txn.ts +67 -0
  57. package/core/utils/get-date.ts +3 -0
  58. package/core/utils/get-ipfs-data.ts +13 -0
  59. package/core/utils/get-web3-provider.ts +18 -0
  60. package/core/utils/gql-queries.ts +133 -0
  61. package/core/utils/map-filter.ts +21 -0
  62. package/core/utils/serialize-bigint.ts +7 -0
  63. package/core/utils/to-unix.ts +18 -0
  64. package/create-community-example.ts +119 -0
  65. package/csv-upload/README.md +74 -0
  66. package/csv-upload/config.ts +41 -0
  67. package/csv-upload/example.csv +2 -0
  68. package/csv-upload/keys.example.json +8 -0
  69. package/csv-upload/scripts/run.ts +417 -0
  70. package/csv-upload/types.ts +39 -0
  71. package/docs/.gitkeep +0 -0
  72. package/docs/images/attestation-architecture.png +0 -0
  73. package/docs/images/dfd-get-projects.png +0 -0
  74. package/gap-schema.yaml +155 -0
  75. package/milestone-workflow-example.ts +353 -0
  76. package/package.json +45 -39
  77. package/readme.md +872 -0
  78. package/schemas/.gitkeep +0 -0
  79. package/schemas/GAP-schemas-1692135812877.json +33 -0
  80. package/test-file-indexer-api.ts +25 -0
  81. package/tsconfig.json +26 -0
  82. package/core/abi/index.d.ts +0 -1114
  83. package/core/abi/index.js +0 -26
  84. package/core/class/AllGapSchemas.d.ts +0 -9
  85. package/core/class/AllGapSchemas.js +0 -19
  86. package/core/class/Attestation.d.ts +0 -173
  87. package/core/class/Attestation.js +0 -333
  88. package/core/class/Fetcher.d.ts +0 -175
  89. package/core/class/Fetcher.js +0 -13
  90. package/core/class/GAP.d.ts +0 -254
  91. package/core/class/GAP.js +0 -289
  92. package/core/class/GapSchema.d.ts +0 -34
  93. package/core/class/GapSchema.js +0 -62
  94. package/core/class/GrantProgramRegistry/Allo.d.ts +0 -17
  95. package/core/class/GrantProgramRegistry/Allo.js +0 -137
  96. package/core/class/GrantProgramRegistry/AlloRegistry.d.ts +0 -15
  97. package/core/class/GrantProgramRegistry/AlloRegistry.js +0 -70
  98. package/core/class/GraphQL/AxiosGQL.d.ts +0 -6
  99. package/core/class/GraphQL/AxiosGQL.js +0 -25
  100. package/core/class/GraphQL/EASClient.d.ts +0 -16
  101. package/core/class/GraphQL/EASClient.js +0 -26
  102. package/core/class/GraphQL/GapEasClient.d.ts +0 -71
  103. package/core/class/GraphQL/GapEasClient.js +0 -451
  104. package/core/class/GraphQL/index.js +0 -19
  105. package/core/class/Schema.d.ts +0 -233
  106. package/core/class/Schema.js +0 -488
  107. package/core/class/SchemaError.d.ts +0 -30
  108. package/core/class/SchemaError.js +0 -39
  109. package/core/class/contract/GapContract.d.ts +0 -102
  110. package/core/class/contract/GapContract.js +0 -285
  111. package/core/class/entities/Community.d.ts +0 -34
  112. package/core/class/entities/Community.js +0 -109
  113. package/core/class/entities/ContributorProfile.d.ts +0 -41
  114. package/core/class/entities/ContributorProfile.js +0 -69
  115. package/core/class/entities/Grant.d.ts +0 -54
  116. package/core/class/entities/Grant.js +0 -223
  117. package/core/class/entities/GrantUpdate.d.ts +0 -40
  118. package/core/class/entities/GrantUpdate.js +0 -114
  119. package/core/class/entities/MemberOf.d.ts +0 -11
  120. package/core/class/entities/MemberOf.js +0 -33
  121. package/core/class/entities/Milestone.d.ts +0 -168
  122. package/core/class/entities/Milestone.js +0 -657
  123. package/core/class/entities/Project.d.ts +0 -92
  124. package/core/class/entities/Project.js +0 -418
  125. package/core/class/entities/ProjectImpact.d.ts +0 -50
  126. package/core/class/entities/ProjectImpact.js +0 -112
  127. package/core/class/entities/ProjectMilestone.d.ts +0 -60
  128. package/core/class/entities/ProjectMilestone.js +0 -174
  129. package/core/class/entities/ProjectPointer.d.ts +0 -12
  130. package/core/class/entities/ProjectPointer.js +0 -22
  131. package/core/class/entities/ProjectUpdate.d.ts +0 -50
  132. package/core/class/entities/ProjectUpdate.js +0 -110
  133. package/core/class/entities/Track.d.ts +0 -16
  134. package/core/class/entities/Track.js +0 -21
  135. package/core/class/entities/index.js +0 -26
  136. package/core/class/index.js +0 -26
  137. package/core/class/karma-indexer/GapIndexerClient.d.ts +0 -66
  138. package/core/class/karma-indexer/GapIndexerClient.js +0 -207
  139. package/core/class/karma-indexer/api/GapIndexerApi.d.ts +0 -73
  140. package/core/class/karma-indexer/api/GapIndexerApi.js +0 -256
  141. package/core/class/karma-indexer/api/types.d.ts +0 -295
  142. package/core/class/karma-indexer/api/types.js +0 -2
  143. package/core/class/remote-storage/IpfsStorage.d.ts +0 -23
  144. package/core/class/remote-storage/IpfsStorage.js +0 -56
  145. package/core/class/remote-storage/RemoteStorage.d.ts +0 -41
  146. package/core/class/remote-storage/RemoteStorage.js +0 -38
  147. package/core/class/types/allo.d.ts +0 -78
  148. package/core/class/types/allo.js +0 -2
  149. package/core/class/types/attestations.d.ts +0 -168
  150. package/core/class/types/attestations.js +0 -66
  151. package/core/consts.d.ts +0 -48
  152. package/core/consts.js +0 -641
  153. package/core/index.js +0 -24
  154. package/core/shared/types.d.ts +0 -6
  155. package/core/shared/types.js +0 -2
  156. package/core/types.d.ts +0 -131
  157. package/core/types.js +0 -13
  158. package/core/utils/gelato/index.js +0 -19
  159. package/core/utils/gelato/send-gelato-txn.d.ts +0 -55
  160. package/core/utils/gelato/send-gelato-txn.js +0 -100
  161. package/core/utils/gelato/sponsor-handler.d.ts +0 -9
  162. package/core/utils/gelato/sponsor-handler.js +0 -60
  163. package/core/utils/gelato/watch-gelato-txn.d.ts +0 -7
  164. package/core/utils/gelato/watch-gelato-txn.js +0 -63
  165. package/core/utils/get-date.d.ts +0 -1
  166. package/core/utils/get-date.js +0 -7
  167. package/core/utils/get-ipfs-data.d.ts +0 -1
  168. package/core/utils/get-ipfs-data.js +0 -20
  169. package/core/utils/get-web3-provider.d.ts +0 -2
  170. package/core/utils/get-web3-provider.js +0 -18
  171. package/core/utils/gql-queries.d.ts +0 -12
  172. package/core/utils/gql-queries.js +0 -90
  173. package/core/utils/index.js +0 -23
  174. package/core/utils/map-filter.d.ts +0 -8
  175. package/core/utils/map-filter.js +0 -20
  176. package/core/utils/serialize-bigint.d.ts +0 -1
  177. package/core/utils/serialize-bigint.js +0 -8
  178. package/core/utils/to-unix.d.ts +0 -1
  179. package/core/utils/to-unix.js +0 -25
  180. package/index.js +0 -17
  181. /package/core/class/GraphQL/{index.d.ts → index.ts} +0 -0
  182. /package/core/class/entities/{index.d.ts → index.ts} +0 -0
  183. /package/core/class/{index.d.ts → index.ts} +0 -0
  184. /package/core/{index.d.ts → index.ts} +0 -0
  185. /package/core/utils/gelato/{index.d.ts → index.ts} +0 -0
  186. /package/core/utils/{index.d.ts → index.ts} +0 -0
  187. /package/{core/class/Gelato/Gelato.d.ts → csv-upload/.gitkeep} +0 -0
  188. /package/{index.d.ts → index.ts} +0 -0
package/readme.md ADDED
@@ -0,0 +1,872 @@
1
+ # Karma GAP SDK
2
+
3
+ ## Summary
4
+
5
+ - [Karma GAP SDK](#karma-gap-sdk)
6
+ - [Summary](#summary)
7
+ - [1. What is GAP SDK?](#1-what-is-gap-sdk)
8
+ - [2. Architecture](#2-architecture)
9
+ - [Attestations](#attestations)
10
+ - [3. Getting started](#3-getting-started)
11
+ - [4. Fetching Entities](#4-fetching-entities)
12
+ - [5. Creating entities in the Frontend](#5-creating-entities-in-the-frontend)
13
+ - [Deleting an entity](#deleting-an-entity)
14
+ - [Updating details](#updating-details)
15
+ - [6. Create entities in a Backend](#6-create-entities-in-a-backend)
16
+ - [7. Gasless Transactions with Gelato](#7-gasless-transactions-with-gelato)
17
+ - [External API](#external-api)
18
+ - [8. Custom API](#8-custom-api)
19
+ - [9. Import cost](#9-import-cost)
20
+ - [Contact Us](#contact-us)
21
+
22
+ ## 1. What is GAP SDK?
23
+
24
+ The GAP SDK is a library for easy integration with [Grantee Accountability Protocol](https://gap.karmahq.xyz). Using this library, you will be able to:
25
+
26
+ - Fetch and display Communities, Projects, Members, Grants, Milestones, and all of their dependencies.
27
+ - Create, update and delete all of the above.
28
+ - Utilize gasless transactions with [Gelato Relay](https://relay.gelato.network).
29
+ - Utilize custom API endpoints to speed up queries instead of accessing data onchain through RPC end points.
30
+
31
+ ## 2. Architecture
32
+
33
+ The GAP SDK is module-based and adheres to specific parameters to ensure organization and ease of maintenance. At its core, the SDK is divided into the following modules:
34
+
35
+ 1. **GAP Facade**: This object is responsible for centralizing resources within the SDK, providing all the tools and methods required to retrieve attestations from the network and convert them into concrete objects for display, attestation, modification, and revocation. The GAP Facade includes the `fetcher` module and stores all the necessary settings for the instance.
36
+
37
+ 2. **Attestations**: The attestation module is versatile and can accommodate various types of attestations available at [EAS](https://attest.sh). It can also be inherited to cater to specific attestation types, such as a Project, for example. This generic object is capable of performing attestations and revocations through the `Schema` module.
38
+
39
+ 3. **Schemas**: The Schema is an **abstract module** that assumes the responsibility of establishing a connection with the EAS infrastructure. This class contains all the parameters, methods, and interactions needed to communicate with the blockchain.
40
+
41
+ 4. **Entities**: An entity represents a specific attestation `type` customized to fulfill specific requirements, such as data processing prior to attestation or method overrides. An entity always extends the `Attestation` module and inherits all its features.
42
+
43
+ 5. **Contract**: To meet the requirements of this SDK, a custom intermediary contract is utilized to add an abstraction layer to the original EAS contracts. This is essential for a better user experience, reduced gas costs, and monitoring. Karma's GAP SDK employs a special contract for this purpose. For more information, please refer to the [Gap Contracts](https://github.com/show-karma/gap-contracts) repository.
44
+
45
+ 6. **Fetcher**: This **abstract module** is responsible for interacting with EAS or a custom API to retrieve attestations and transform them into instances of `Attestation`. The `Fetcher` module is indispensable if a custom API is desired.
46
+
47
+ Here's an example of how all these modules work together when retrieving Projects:
48
+
49
+ ![img](docs/images/dfd-get-projects.png)
50
+
51
+ In this diagram, you can already discern the benefits of using a Custom API to obtain data from the network and construct your own indexer, as opposed to relying on EAS's GraphQL API. We will delve into this further in [Section 8](#8-custom-api).
52
+
53
+ > **Note**: GAP currently does not fully support multichain, and creating more than one instance can result in unexpected errors when using the fetcher. This feature is currently under development.
54
+
55
+ ### Attestations
56
+
57
+ All the data in the protocol is stored as attestations using [EAS](https://attest.sh). Attestations are categorized into various types of entities to establish a relationship between them and enable users to modify their attestation details without losing references to the main attestation. As a result, entities like community, project, grant, and members require two attestations: the first defines the entity, and the second defines its details. Due to this structure, all these entities will include a `details` property that contains all the data inserted into that attestation. For example:
58
+
59
+ ```ts
60
+ import { Project } from "@show-karma/karma-gap-sdk";
61
+
62
+ export function printProjectDetails(project: Project) {
63
+ console.log({
64
+ title: project.details?.title,
65
+ imageURL: project.details?.imageURL,
66
+ description: project.details?.description,
67
+ });
68
+ }
69
+ ```
70
+
71
+ This example can be applied to all the entities in the diagram below, each of which has its own interface and details parameters.
72
+
73
+ > When effectively using this SDK, you may notice differences between the data listed in the diagram and the actual entity. This discrepancy arises from the need to structure our classes to facilitate the use of certain parameters, such as `Grant.communities`, which is not included in the diagram. The diagram only encompasses attestation data, and some data is added during runtime and is not part of the on-chain attestation.
74
+
75
+ ![architecture](./docs/images/attestation-architecture.png)
76
+
77
+ ## 3. Getting started
78
+
79
+ After setting up your project, install GAP SDK with `yarn` or `npm`:
80
+
81
+ `$ yarn add karma-gap-sdk`
82
+
83
+ `$ npm i karma-gap-sdk`
84
+
85
+ After installing, you can instantiate GAP:
86
+
87
+ ```ts
88
+ // gap.client.ts;
89
+ import { GAP } from "@show-karma/karma-gap-sdk";
90
+
91
+ const client = new GAP({
92
+ globalSchemas: false,
93
+ network: "optimism", // can be any of our supported networks. you can check here -> https://github.com/show-karma/karma-gap-sdk/blob/main/core/types.ts#L80
94
+ apiClient: new GapIndexerClient("https://gapapi.karmahq.xyz"), // custom api client, see Section 8;
95
+ });
96
+
97
+ export default client;
98
+ ```
99
+
100
+ The `GAP` class is used to create an instance of the client.
101
+
102
+ > Please note that the need for multiple instances arises primarily when using the default EAS API client, as it offers distinct endpoints for various networks. If you're utilizing a custom API, you can implement methods to filter by network and prevent instance mutation.
103
+
104
+ The `apiClient` option is employed when you wish to use a Custom API. The SDK provides a standard custom API that can be initiated with `apiClient: new GapIndexerClient(url)`. However, it's also possible to develop your own API in any programming language and data modeling and utilize it as your client. To achieve this, create your class and extend the abstract class `Fetcher`:
105
+
106
+ ```ts
107
+ // MyCustomApiClient.ts
108
+ import { Fetcher } from "@show-karma/karma-gap-sdk/core/class/Fetcher";
109
+
110
+ export class MyCustomApiClient extends Fetcher {
111
+ // ... implement all Fetcher methods following its return types and arguments.
112
+ }
113
+ ```
114
+
115
+ [..] Then you can use it on GAP client. More details about how to implement a custom fetcher on [Section 8](#8-custom-api).
116
+
117
+ ```ts
118
+ // gap.client.ts;
119
+ import { GAP } from "@show-karma/karma-gap-sdk";
120
+ import { MyCustomApiClient } from "./MyCustomApiClient.ts";
121
+
122
+ const gap = new GAP({
123
+ network: "optimism-goerli", // sepolia, optimism,
124
+ apiClient: new MyCustomApiClient("https://my-custom-api.mydomain.com"),
125
+ // gelatoOpts: for gasless transactions, see it on Chap. 7;
126
+ // ipfsKey: for cheaper attestations;
127
+ });
128
+
129
+ export default gap;
130
+ ```
131
+
132
+ The `gelatoOpts` option is used when developers aim to provide gasless transactions for a better user experience. For more details about this feature, please refer to [Chapter 7](#7-gasless-transactions-with-gelato).
133
+
134
+ The `ipfsKey` is utilized to upload a project's data to the InterPlanetary File System (IPFS) and then include the resulting IPFS hash in the body of the Attestation. This approach is advantageous because it significantly reduces the size of the Attestation body. By storing the bulk of data on IPFS—a decentralized storage solution—and referencing it via a hash, the overall cost of creating and sending Attestations is reduced, making the process more efficient and cost-effective.
135
+
136
+ ## 4. Fetching Entities
137
+
138
+ After initializing the GAP client, you are now able to fetch entities available including:
139
+
140
+ - Communities
141
+ - Projects
142
+ - Grants
143
+ - Grant updates
144
+ - Members of Projects
145
+ - Milestones
146
+ - Milestone updates
147
+
148
+ Indeed, you can retrieve all available entities, but we provide methods primarily for the higher-level entities, as this aligns with the intended behavior. When examining the `Fetcher` interface, you can:
149
+
150
+ - Retrieve communities along with their related grants.
151
+ - Obtain projects, which will contain related members and grants. Note that grants will include related updates and milestones, and milestones will also include their updates.
152
+ - Fetch grants for grantees.
153
+ - Retrieve projects from grantees.
154
+ - Access milestones associated with a grant.
155
+ - Get members of a project.
156
+
157
+ To begin using the fetcher, simply call `gap.fetch.<target>(...args)` as demonstrated in the following example:
158
+
159
+ ```ts
160
+ import { gap } from "./gap-client";
161
+
162
+ gap.fetch.projects().then((res) => {
163
+ res.forEach((project) => {
164
+ console.log(project.details.title);
165
+ });
166
+ });
167
+
168
+ gap.fetch
169
+ .projectBySlug("my-project-slug")
170
+ .then((project) => {
171
+ console.log(project.details.name);
172
+ })
173
+ .catch((er) => {
174
+ console.error(er.message);
175
+ });
176
+ ```
177
+
178
+ ## 5. Creating entities in the Frontend
179
+
180
+ Creating entities (by adding attestations) using the GAP SDK is quite straightforward. Developers only need to define what they want to attest, and we provide facilities for this module. To avoid frequent wallet pop-ups for individual entity attestations, we've developed a special contract that handles multiple attestations and their relationships. This means you can transact once and attest multiple times. Let's walk through an example:
181
+
182
+ Suppose a user wants to create a project, and this project will include:
183
+
184
+ 1. Its details (title, image, and description).
185
+ 2. Two members.
186
+ 3. A grant.
187
+ 4. The grant will have one milestone.
188
+
189
+ > To attest a grant, it will require a community, so consider that a community already exists. To create a community, the user needs to go through [this link](https://tally.so/r/wd0jeq).
190
+
191
+ After setting up the GAP client, you can proceed to:
192
+
193
+ ```ts
194
+ // get-dummy-project.ts
195
+ import {
196
+ Project,
197
+ ProjectDetails,
198
+ MemberOf,
199
+ Grant,
200
+ GapSchema,
201
+ } from "@show-karma/karma-gap-sdk";
202
+
203
+ export function getDummyProject() {
204
+ // Creating Project
205
+ const project = new Project({
206
+ data: { project: true },
207
+ chainID: 420, // 420 is the chainID of the optimism-sepolia
208
+ schema: gap.findSchema("Project"),
209
+ // Owner address, usually whoever is connected to the app
210
+ recipient: "0xd7d...25f2",
211
+ });
212
+
213
+ // Adding details to the project
214
+ project.details = new ProjectDetails({
215
+ data: {
216
+ title: "My Project",
217
+ description: "My Description",
218
+ imageURL: "https://loremflickr.com/320/240/kitten",
219
+ links: [{ type: "github", url: "https://github.com/my-org" }],
220
+ tags: [{ name: "DAO" }, { name: "UI/UX" }],
221
+ },
222
+ schema: gap.findSchema("ProjectDetails"),
223
+ recipient: project.recipient,
224
+ });
225
+
226
+ const member_1 = new MemberOf({
227
+ data: { memberOf: true },
228
+ schema: gap.findSchema("MemberOf"),
229
+ refUID: project.uid,
230
+ // member 1 address
231
+ recipient: "0x8dC...A8b4",
232
+ });
233
+
234
+ const member_2 = new MemberOf({
235
+ data: { memberOf: true },
236
+ schema: gap.findSchema("MemberOf"),
237
+ refUID: project.uid,
238
+ // member 2 address
239
+ recipient: "0xabc...A7b3",
240
+ });
241
+
242
+ // Add members to the project
243
+ project.members.push(member_1, member_2);
244
+
245
+ // Creating Grant
246
+ const grant = new Grant({
247
+ // Address of the related community
248
+ data: { communityUID: "0xabc...def" },
249
+ schema: gap.findSchema("Grant"),
250
+ recipient: project.recipient,
251
+ });
252
+
253
+ // Adding details to Grant
254
+ grant.details = new GrantDetails({
255
+ data: {
256
+ title: "Build the Gap App",
257
+ proposalURL: "https://pantera.com/",
258
+ description: "Grant Description",
259
+ // cycle: grant cycle, optional
260
+ // season: grant season, optional
261
+ },
262
+ schema: gap.findSchema("GrantDetails"),
263
+ recipient: project.recipient,
264
+ });
265
+
266
+ // Creating milestone
267
+ const milestone = new Milestone({
268
+ data: {
269
+ title: "Build the Home Page",
270
+ description: "Milestone Description",
271
+ endsAt: Date.now() + 1000000,
272
+ },
273
+ schema: gap.findSchema("Milestone"),
274
+ recipient: project.recipient,
275
+ });
276
+
277
+ grant.milestones.push(milestone);
278
+
279
+ // Add grants to the project
280
+ project.grants.push(grant);
281
+
282
+ return project;
283
+ }
284
+ ```
285
+
286
+ Once you have set up the project with all its dependencies, it's time to attest. You can do this by calling `project.attest` and providing a signer to sign the transaction. The `signer` can be an ethers.js wallet or a Web3.js provider, as long as it satisfies the `SignerOrProvider` interface. In some cases, the EAS API may indicate that these providers do not perfectly match the signer's interface, but in most cases, this can be easily resolved by using `any` typing.
287
+
288
+ ```ts
289
+ // useSigner.ts
290
+
291
+ // wagmi/react example
292
+ import { useWalletClient } from "wagmi";
293
+
294
+ export function walletClientToSigner(walletClient: WalletClient) {
295
+ const { account, chain, transport } = walletClient;
296
+ const network = {
297
+ chainId: chain.id,
298
+ name: chain.name,
299
+ ensAddress: chain.contracts?.ensRegistry?.address,
300
+ };
301
+ const provider = new providers.Web3Provider(transport, network);
302
+ const signer = provider.getSigner(account.address);
303
+
304
+ return signer;
305
+ }
306
+
307
+ export function useSigner() {
308
+ const { data: walletClient } = useWalletClient();
309
+
310
+ const [signer, setSigner] = useState<JsonRpcSigner | undefined>(undefined);
311
+ useEffect(() => {
312
+ async function getSigner() {
313
+ if (!walletClient) return;
314
+
315
+ const tmpSigner = walletClientToSigner(walletClient);
316
+
317
+ setSigner(tmpSigner);
318
+ }
319
+
320
+ getSigner();
321
+ }, [walletClient]);
322
+ return signer;
323
+ }
324
+ ```
325
+
326
+ Then, in the attestation file:
327
+
328
+ ```ts
329
+ import { getDummyProject } from "util/get-dummy-project";
330
+ import { useSigner } from "util/useSigner";
331
+
332
+ export const MyComponent: React.FC = () => {
333
+ const signer = useSigner();
334
+
335
+ const attestProject = async () => {
336
+ const project = getDummyProject();
337
+ // any typing is required here as it
338
+ // does not naturally fits the EAS
339
+ // SignerOrProvider interface.
340
+ await project.attest(signer as any);
341
+ console.log(
342
+ `Attested Project ${project.details.title} with uid ${project.uid}`
343
+ );
344
+ };
345
+ };
346
+ ```
347
+
348
+ The previous example is related to when a user wishes to attest a project with all its relationships, but it's not mandatory to follow the same method. It's entirely possible to transact separate attestations using the refUID property for each attestation. As an example, let's say the user wants to add another grant to the project:
349
+
350
+ ```ts
351
+ // add-grant-to-project.ts
352
+ import { Grant, GrantDetails, GapSchema, Hex } from "@show-karma/karma-gap-sdk";
353
+
354
+ export function addGrantToProject(
355
+ grant: IGrantDetails,
356
+ communityUID: Hex,
357
+ recipient: Hex,
358
+ projectUID: Hex
359
+ ): Grant {
360
+ const grant = new Grant({
361
+ data: { communityUID },
362
+ schema: GapSchema.find("Grant"),
363
+ recipient,
364
+ // The ref UID will create a reference from
365
+ // the current grant to an already attested
366
+ // project
367
+ refUID: projectUID,
368
+ });
369
+
370
+ return grant;
371
+ }
372
+ ```
373
+
374
+ So using the last example, we can get:
375
+
376
+ ```ts
377
+ import { getDummyProject } from 'util/get-dummy-project';
378
+ import { addGrantToProject } from 'util/add-grant-to-project';
379
+ import { Project } from '@show-karma/karma-gap-sdk';
380
+
381
+ export const MyComponent: React.FC = () => {
382
+ const signer = useSigner();
383
+ const [project, setProject] = useState<Project>()
384
+
385
+ const attestProject = async () => {
386
+ const project = getDummyProject();
387
+ // any typing is required here as it
388
+ // does not naturally fits the EAS
389
+ // SignerOrProvider interface.
390
+ await project.attest(signer as any);
391
+ setProject(project);
392
+ console.log(
393
+ `Attested Project ${project.details.title} with uid ${project.uid}`
394
+ );
395
+ };
396
+
397
+ const addGrant = () => {
398
+ if(!project) throw new Error('No project set up.');
399
+
400
+ const grant = addGrantToProject(
401
+ {
402
+ title: 'Grant 2',
403
+ description: 'Grant 2 description',
404
+ proposalURL: 'https://example.com'
405
+ },
406
+ communityUID: '0xa...bcde',
407
+ recipient: project.recipient,
408
+ projectUID: project.uid
409
+ );
410
+
411
+ await grant.attest(signer as any);
412
+ project.grants.push(grant);
413
+ console.log(`Grant ${grant.details.title} attested with uid ${grant.uid}`)
414
+ };
415
+ };
416
+ ```
417
+
418
+ > This approach can be applied to any subsequent attestation, whether it's adding a milestone to a grant, a grant to a project, members to a project, or updating Project, Grant, and Community details. It's also available when approving, rejecting, or completing a milestone. If any of these operations are performed multiple times, the latest one will take precedence.
419
+
420
+ Following any type of attestation, the SDK will associate UIDs with the objects, making them accessible after the attestation is completed. For instance, if you perform the project attestation with all its dependents, you can retrieve the attestation UID of the project, its details, grants, milestones, and so on.
421
+
422
+ ### Deleting an entity
423
+
424
+ Since every object returned by the Fetcher is also an Attestation, to delete an entitye, you just have to revoke the attestation by calling attestation.revoke.
425
+
426
+ ```ts
427
+ // revoke-project.ts
428
+ import { SignerOrProvider, Project } from "@show-karma/karma-gap-sdk";
429
+
430
+ export async function revokeProject(
431
+ project: Project,
432
+ signer: SignerOrProvider
433
+ ) {
434
+ await project.revoke(signer);
435
+ }
436
+ ```
437
+
438
+ ### Updating details
439
+
440
+ To update the details of a Community, Project, or Grant, simply replace the current details and attest again.
441
+
442
+ ```ts
443
+ // update-project-details.ts
444
+ import {
445
+ SignerOrProvider,
446
+ Project,
447
+ IProjectDetails,
448
+ ProjectDetails,
449
+ GapSchema,
450
+ } from "@show-karma/karma-gap-sdk";
451
+
452
+ export async function updateProjectDetails(
453
+ project: Project,
454
+ data: IProjectDetails,
455
+ signer: SignerOrProvider
456
+ ) {
457
+ // If project details already exists:
458
+ project.details.setValues(data);
459
+
460
+ // and if they not, we need to instantiate
461
+ // project.details = new ProjectDetails({
462
+ // data,
463
+ // recipient: project.recipient,
464
+ // schema: GapSchema.find('ProjectDetails'),
465
+ // });
466
+
467
+ await project.details.attest(signer);
468
+ console.log(`Project ${project.details.name} was updated.`);
469
+
470
+ // You can return the project or not. As it is a reference to
471
+ // the original project, the details will be updated
472
+ // in the previous instance.
473
+ return project;
474
+ }
475
+ ```
476
+
477
+ > Note that you cannot update Milestone without losing all of its references.
478
+
479
+ ## 6. Create entities in a Backend
480
+
481
+ To create entities in the backend, follow the same content as provided in [Section 5](#5-attesting-data-in-a-frontend). The only distinction between them is that in the backend, you'll need to instantiate an `ethers.js` wallet at runtime to sign attestations.
482
+
483
+ ```ts
484
+ import { gap } from "gap-client";
485
+ import { getDummyProject } from "util/get-dummy-project";
486
+
487
+ // Create the web3 provider
488
+ const web3 = new ethers.providers.JsonRpcProvider(
489
+ "https://my-provider-url.com"
490
+ );
491
+
492
+ // Creating a ethersjs wallet
493
+ const wallet = new ethers.Wallet("0xabc...def1", web3);
494
+
495
+ const project = getDummyProject();
496
+
497
+ project.attest(wallet as any).then(() => {
498
+ // After attesting, project.uid should be filled.
499
+ console.log(
500
+ `Project ${project.details.name} attested with uid ${project.uid}`
501
+ );
502
+ });
503
+ ```
504
+
505
+ ## 7. Gasless Transactions with Gelato
506
+
507
+ Gasless transactions are an excellent option when a developer aims to enhance the user experience and attestation flow. In this SDK, we leverage [Gelato Relay](https://relay.gelato.network) to sponsor transactions, eliminating the need for users to cover network fees\*.
508
+
509
+ > \* Currently, this feature is available for all attestations except milestone completion/approvals/rejection updates. Gasless support for these specific attestations is under development and may be released soon.
510
+
511
+ Before using gasless transactions, it's essential to visit the [Gelato Relay](https://relay.gelato.network) app, set up and fund your account on one of the available networks (e.g., optimism goerli, sepolia, or optimism mainnet), configure the contract, and obtain your API key.
512
+
513
+ > The ABI for our contract can be found [here](https://github.com/show-karma/karma-gap-sdk/blob/dev/core/abi/MultiAttester.json).
514
+
515
+ > For the security of your Gelato account, only enable gasless transactions for the `multiSequentialAttest`, `attest`, and `multiRevoke` methods.
516
+
517
+ Continuing with how to use gasless transactions, developers will encounter the following options when creating a GAP instance:
518
+
519
+ ````ts
520
+ interface GAPArgs {
521
+ // ... Other GAP client constructor arguments
522
+ /**
523
+ * Defined if the transactions will be gasless or not.
524
+ *
525
+ * In case of true, the transactions will be sent through [Gelato](https://gelato.network)
526
+ * and an API key is needed.
527
+ *
528
+ * > __Note that to safely transact through Gelato, the user must
529
+ * have set a handlerUrl and not expose gelato api in the frontend.__
530
+ */
531
+ gelatoOpts?: {
532
+ /**
533
+ * Endpoint in which the transaction will be sent.
534
+ * A custom endpoint will ensure that the transaction will be sent through Gelato
535
+ * and api keys won't be exposed in the frontend.
536
+ *
537
+ * __If coding a backend, you can use `apiKey` prop instead.__
538
+ *
539
+ * `core/utils/gelato/sponsor-handler.ts` is a base handler that can be used
540
+ * together with NextJS API routes.
541
+ *
542
+ * @example
543
+ *
544
+ * ```ts
545
+ * // pages/api/gelato.ts
546
+ * import { handler as sponsorHandler } from "core/utils/gelato/sponsor-handler";
547
+ *
548
+ * export default const handler(req, res) => sponsorHandler(req, res, "GELATO_API_KEY_ENV_VARIABLE");
549
+ *
550
+ * ```
551
+ */
552
+ sponsorUrl?: string;
553
+ /**
554
+ * If true, env_gelatoApiKey will be marked as required.
555
+ * This means that the endpoint at sponsorUrl is contained in this application.
556
+ *
557
+ * E.g. Next.JS api route.
558
+ */
559
+ contained?: boolean;
560
+ /**
561
+ * The env key of gelato api key that will be used in the handler.
562
+ *
563
+ * @example
564
+ *
565
+ * ```
566
+ * // .env
567
+ * GELATO_API_KEY=1234567890
568
+ *
569
+ * // sponsor-handler.ts
570
+ *
571
+ * export async function handler(req, res) {
572
+ * // ...code
573
+ *
574
+ * const { env_gelatoApiKey } = GAP.gelatoOpts;
575
+ *
576
+ * // Will be used to get the key from environment.
577
+ * const { [env_gelatoApiKey]: apiKey } = process.env;
578
+ *
579
+ * // send txn
580
+ * // res.send(result);
581
+ * }
582
+ * ```
583
+ */
584
+ env_gelatoApiKey?: string;
585
+ /**
586
+ * API key to be used in the handler.
587
+ *
588
+ * @deprecated Use this only if you have no option of setting a backend, next/nuxt api route
589
+ * or if this application is a backend.
590
+ *
591
+ * > __This will expose the api key if used in the frontend.__
592
+ */
593
+ apiKey?: string;
594
+ /**
595
+ * If true, will use gelato to send transactions.
596
+ */
597
+ useGasless?: boolean;
598
+ };
599
+ }
600
+ ````
601
+
602
+ If `gasless` transactions are required, developers should be aware that it can be used in three modes, all of which require setting `gelatoOpts.useGasless: true`:
603
+
604
+ 1. **With API Key:**
605
+ This method is recommended when used in an external API. If utilized at the frontend level, the API key becomes visible to all users. To implement this method, simply fill in `gelatoOpts.env_gelatoApiKey: '<gelato api key>'`. Please note that a deprecation warning will appear with this disclaimer, but rest assured, this option will not be removed from the SDK. The constructor will appear as follows:
606
+
607
+ ```ts
608
+ new GAP({
609
+ network: 'optimism-goerli',
610
+ gelatoOpts: {
611
+ env_gelatoApiKey: '<GELATO_API_KEY>'
612
+ // to use gasless. it can be mutated
613
+ // through GAP.gelatoOpts.useGasless = <boolval>
614
+ useGasless: true
615
+ }
616
+ })
617
+ ```
618
+
619
+ 2. **With External API Support:**
620
+ In this scenario, you're using an external API, such as your indexer, to provide a sponsored transaction URL that communicates with Gelato. Here, the API key will not be visible. To use this method, simply provide the `gelatoOpts.sponsorUrl`.
621
+
622
+ ```ts
623
+ new GAP({
624
+ network: 'optimism-goerli',
625
+ gelatoOpts: {
626
+ sponsorUrl: 'https://my-api.mydomain.com/sponsor-url-name'
627
+ // to use gasless. it can be mutated
628
+ // through GAP.useGasless = <boolval>
629
+ useGasless: true
630
+ }
631
+ })
632
+ ```
633
+
634
+ 3. **With Self-Contained API Support:**
635
+ This case is similar to #2, but the difference is that you're using a self-contained API, such as Next.js API, which doesn't require an external backend to request the transaction. In this case, you will need to provide:
636
+
637
+ ```ts
638
+ new GAP({
639
+ network: "optimism-goerli",
640
+ gelatoOpts: {
641
+ sponsorUrl: "/api/my-contained-sponsor-url",
642
+ // marking contained as required will make possible
643
+ // to send transactions through a NextJS api.
644
+ contained: true,
645
+ // to use gasless. it can be mutated
646
+ // through GAP.useGasless = <boolval>
647
+ useGasless: true,
648
+ },
649
+ });
650
+ ```
651
+
652
+ When using a self-contained API to hide API keys, we offer a plug-and-play utility for Next.js. You can utilize it by importing `import { handler } from karma-gap-sdk` and placing it under `/pages/api/sponsored-txn.ts`.
653
+
654
+ ```ts
655
+ // pages/api/sponsored-handler.ts
656
+ import {
657
+ type ApiRequest,
658
+ handler as sponsorTxnHandler,
659
+ } from "@show-karma/karma-gap-sdk";
660
+ import type { NextApiResponse } from "next";
661
+
662
+ const handler = (req: ApiRequest, res: NextApiResponse) =>
663
+ sponsorTxnHandler(req as ApiRequest, res, "NEXT_GELATO_API_KEY");
664
+
665
+ export default handler;
666
+ ```
667
+
668
+ > Please note that `NEXT_GELATO_API_KEY` is not an actual API key but the name of the environment variable to retrieve from `process.env`. This setup will not expose the API key in the frontend. Your `.env` file should contain a field like `NEXT_GELATO_API_KEY=abcdefg123`. For more details, refer to [sponsor-handler.ts L63](https://github.com/show-karma/karma-gap-sdk/blob/f2f3f863c8b2b475ca74bd76bb9290a075c12f60/core/utils/gelato/sponsor-handler.ts#L63).
669
+
670
+ After placing the API page, set `gelatoOpts.sponsorUrl: '/api/sponsored-txn'`, and all transactions will be routed through the Gelato Relay network.
671
+
672
+ ### External API
673
+
674
+ When using an external API to facilitate gasless transactions, you'll need to create an endpoint with a structure similar to the example below:
675
+
676
+ ```ts
677
+ // gelato/sponsor-handler.ts
678
+ import { GelatoRelay } from "@gelatonetwork/relay-sdk";
679
+ import { Gelato } from "@show-karma/karma-gap-sdk/core/utils/gelato/";
680
+ // Exception Handler available under your development.
681
+ import { HttpException } from "../error/HttpException";
682
+
683
+ export type SponsoredCall = [
684
+ {
685
+ data: string;
686
+ chainId: string;
687
+ target: string;
688
+ },
689
+ string,
690
+ {
691
+ retries: number;
692
+ },
693
+ ];
694
+
695
+ const assertionObj = [
696
+ {
697
+ data: /0x[a-fA-F0-9]+/gim,
698
+ chainId: /\d+/,
699
+ target: /0x[a-fA-F0-9]{40}/gim,
700
+ },
701
+ /\{apiKey\}/,
702
+ {
703
+ retries: /\d+/,
704
+ },
705
+ ];
706
+
707
+ function assert(body: any): body is Parameters<GelatoRelay["sponsoredCall"]> {
708
+ if (!Array.isArray(body) || body.length !== assertionObj.length)
709
+ throw new HttpException("Invalid request body: wrong length.", 400);
710
+
711
+ assertionObj.forEach((item, index) => {
712
+ // check if objects from assertion Object are present in body
713
+ // and test them using the regexp from the assertion Object
714
+ if (typeof item === "object") {
715
+ Object.entries(item).forEach(([key, value]) => {
716
+ if (!body[index][key]?.toString().match(value))
717
+ throw new HttpException(
718
+ `Invalid request body: ${value} doesn't match body[${index}][${key}].`,
719
+ 400
720
+ );
721
+ });
722
+ }
723
+ // test other items as strings
724
+ else if (!body[index]?.toString().match(item))
725
+ throw new HttpException(
726
+ `Invalid request body: ${item} doesn't match body[${index}].`,
727
+ 400
728
+ );
729
+ });
730
+
731
+ return true;
732
+ }
733
+
734
+ async function sendTransaction(payload: SponsoredCall) {
735
+ try {
736
+ if (!assert(payload)) return;
737
+
738
+ const { GELATO_API_KEY: apiKey } = process.env;
739
+ if (!apiKey) throw new Error("Api key not provided.");
740
+
741
+ payload[1] = apiKey;
742
+
743
+ const [request, sponsorApiKey, options] = payload;
744
+
745
+ const result = await Gelato.sendByApiKey(request, sponsorApiKey, options);
746
+ const txId = await result.wait();
747
+
748
+ return { txId, chainId: request.chainId };
749
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
750
+ } catch (error: any) {
751
+ throw new HttpException(error.message, 400); // Bad Request
752
+ }
753
+ }
754
+
755
+ const gelato = {
756
+ sendTransaction,
757
+ };
758
+
759
+ export { gelato };
760
+ ```
761
+
762
+ > If you wish to customize this example, please note that the transaction body must adhere to the format of the SponsoredCall interface, as this interface represents the required arguments in the EAS contract call.
763
+
764
+ After setting up a custom endpoint, the constructor in your frontend app will appear as follows:
765
+
766
+ ```ts
767
+ new GAP({
768
+ network: 'optimism-goerli',
769
+ gelatoOpts: {
770
+ sponsorUrl: 'https://my-api.mydomain.xyz/path/to/sponsored-txn'
771
+ // to use gasless. it can be mutated
772
+ // through GAP.useGasless = <boolval>
773
+ useGasless: true
774
+ }
775
+ })
776
+ ```
777
+
778
+ These are all the settings needed to enable gasless transactions with the GAP SDK, and from this point forward, users should not be required to pay for gas.
779
+
780
+ > Please note that with this option, you will cover the gas fees through Gelato.
781
+
782
+ ## 8. Custom API
783
+
784
+ The SDK offers two methods for fetching data from the network:
785
+
786
+ 1. Using the [EAS GraphQL API](https://optimism-goerli-bedrock.easscan.org/graphql).
787
+ 2. Using a custom-made API.
788
+
789
+ When using the default EAS provider, users can access all the features offered by the SDK. However, it may lead to slow response times. This is because of the architectural limitations of the EAS API, as discussed in [Chapter 2](#2-architecture). The EAS API architecture doesn't support relationships between attestations, which necessitates multiple calls to retrieve the desired result, such as a project with all its dependents.
790
+
791
+ To address this issue, the SDK includes the `Fetcher` module, allowing developers to create their own service and integrate it with the SDK. Integration is achieved by extending the fetcher API and implementing its methods. If you are not going to use a specific method or if your service does not support it, you should implement an error handler or return an empty response.
792
+
793
+ You can review the Fetcher interface in [this file](https://github.com/show-karma/karma-gap-sdk/blob/dev/core/class/Fetcher.ts).
794
+
795
+ ```ts
796
+ // my-fetcher.ts
797
+ import { Fetcher } from "@show-karma/karma-gap-sdk/core/class/Fetcher.ts";
798
+ import { Attestation } from "@show-karma/karma-gap-sdk";
799
+
800
+ const Endpoints = {
801
+ projects: {
802
+ byIdOrSlug: (uid: Hex) => `/projects/${uidOrSlug}`,
803
+ },
804
+ };
805
+
806
+ export class MyFetcher extends Fetcher {
807
+ projectById(uid: `0x${string}`): Promise<Project> {
808
+ // Note that the Fetcher class extends an axios utility class
809
+ // and provides a client for performing http requests
810
+ // by calling this.client
811
+ const project = await this.client.get(
812
+ Endpoints.projects.byIdOrSlug(uid) /* ,{...axiosOpts} */
813
+ );
814
+
815
+ if (!data) throw new Error("Attestation not found");
816
+ // You need to return a Project instance
817
+ return Project.from([data])[0];
818
+ }
819
+
820
+ async projects(name?: string): Promise<Project[]> {
821
+ const { data } = await this.client.get<Project[]>(Endpoints.project.all(), {
822
+ params: {
823
+ "filter[title]": name,
824
+ },
825
+ });
826
+
827
+ return Project.from(data);
828
+ }
829
+ // ... other methods
830
+ }
831
+ ```
832
+
833
+ > You can check a functional example [here](https://github.com/show-karma/karma-gap-sdk/blob/dev/core/class/karma-indexer/GapIndexerClient.ts).
834
+
835
+ After implementing your own client, you can setup the GAP client:
836
+
837
+ ```ts
838
+ // gap.client.ts;
839
+ import { GAP } from "@show-karma/karma-gap-sdk";
840
+ import { MyFetcher } from "./MyFetcher";
841
+
842
+ const gap = new GAP({
843
+ network: "optimism-goerli", // sepolia, optimism,
844
+ // Use your client here
845
+ apiClient: new MyFetcher("https://my-api.mydomain.com"),
846
+ });
847
+
848
+ export default gap;
849
+ ```
850
+
851
+ > Note that your API service should return data that aligns with the interfaces provided by each Attestation for proper compatibility with this SDK. This ensures that the data is structured correctly to work seamlessly with the SDK.
852
+
853
+ ## 9. Import cost
854
+
855
+ Unfortunately, the import cost of the SDK is quite high due some dependencies that we use on the project as `@ethereum-attestation-service/eas-contracts`, `@gelatonetwork/relay-sdk` and `ethers`.
856
+ We plan to do some changes in the future to minify these import costs like replace `ethers` to use other lightweight libraries like `viem`.
857
+
858
+ If you just want to use GAP SDK to fetch infos from the network, you can use our `GapIndexerApi` class.
859
+
860
+ This way you can **avoid** the import cost of the SDK.
861
+
862
+ ```ts
863
+ const getProjectInfo = () => {
864
+ const gapIndexerApi = new GapIndexerApi("https://gapapi.karmahq.xyz");
865
+ const project = await gapIndexerApi.projectBySlug(<YOUR_PROJECT_SLUG_OR_UID>).then((res) => res.data);
866
+ return project;
867
+ }
868
+ ```
869
+
870
+ ## Contact Us
871
+
872
+ If you have any questions on SDK usage, join [our discord](https://discord.com/invite/X4fwgzPReJ) to get help