├── .DS_Store ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ └── publish.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── README.md ├── examples ├── custom-point-distribution │ ├── README.md │ ├── index.ts │ └── package.json └── next-js │ ├── .env │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── next.config.js │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── next.svg │ └── vercel.svg │ ├── src │ ├── app │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── page.module.css │ │ └── page.tsx │ └── lib │ │ └── guild.ts │ └── tsconfig.json ├── package-lock.json ├── package.json ├── src ├── client.ts ├── clients │ ├── guild.ts │ ├── guildAdmin.ts │ ├── guildReward.ts │ ├── platform.ts │ ├── platformUser.ts │ ├── requirement.ts │ ├── role.ts │ ├── rolePlatform.ts │ ├── user.ts │ └── userAddress.ts ├── common.ts ├── error.ts ├── index.ts └── utils.ts ├── tests ├── auth.test.ts ├── callGuildAPI.test.ts ├── clients │ ├── actions │ │ ├── accessCheck.test.ts │ │ └── join.test.ts │ ├── guild.test.ts │ ├── guildAdmin.test.ts │ ├── guildPlatform.test.ts │ ├── platform.test.ts │ ├── platformUser.test.ts │ ├── requirement.test.ts │ ├── role.test.ts │ ├── rolePlatform.test.ts │ ├── user.test.ts │ └── userAddress.test.ts ├── common.ts ├── points.test.ts └── utils.ts ├── tsconfig.json ├── v2-migration-guide.md └── vitest.config.ts /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guildxyz/guild-sdk/75470add817abdac0842847b89757473cf0b0a5c/.DS_Store -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | examples -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2021": true, 4 | "node": true 5 | }, 6 | "extends": ["airbnb-base", "prettier"], 7 | "settings": { 8 | "import/resolver": { 9 | "node": { 10 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 11 | } 12 | } 13 | }, 14 | "parser": "@typescript-eslint/parser", 15 | "parserOptions": { 16 | "ecmaVersion": 12, 17 | "sourceType": "module" 18 | }, 19 | "plugins": ["@typescript-eslint"], 20 | "rules": { 21 | "import/extensions": [ 22 | "error", 23 | "ignorePackages", 24 | { 25 | "jsx": "never", 26 | "ts": "never", 27 | "tsx": "never" 28 | } 29 | ] 30 | }, 31 | "overrides": [ 32 | { 33 | "files": ["**/tests/*.test.ts"], 34 | "env": { 35 | "jest": true 36 | } 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish @guildxyz/sdk 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | build: 10 | runs-on: self-hosted 11 | container: node:lts-alpine3.18 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | # Setup .npmrc file to publish to npm 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: "20.x" 19 | registry-url: "https://registry.npmjs.org" 20 | 21 | - name: "Install dependencies" 22 | run: npm ci 23 | 24 | - name: "Bump package version" 25 | run: | 26 | npm version $( echo "$GITHUB_REF_NAME" | sed 's/v//g' ) --git-tag-version=false 27 | 28 | - name: "Publish package to NPM" 29 | run: npm publish 30 | env: 31 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | others/ 3 | node_modules/ 4 | .vscode 5 | .env* 6 | dist 7 | *.env -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | others/ 2 | node_modules/ 3 | .vscode 4 | .env* 5 | src/ 6 | tests/ 7 | examples/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | tsconfig.json 4 | .github 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Guild SDK for TypeScript | WIP

3 | 4 | 5 | 6 |
7 | Application 8 |   •   9 | Twitter 10 |   •   11 | Github 12 |   •   13 | Discord 14 |
15 | 16 | 17 | 18 | ## Summary 19 | 20 | The Guild SDK library is a Typescript library for interacting with the Guild API. This document explains how to authenticate, manage your Guilds easily and automate token-gated access in any application with this SDK. 21 | 22 | Guild.xyz is the membership layer protocol for web3 communities, making community management easy and interoperable between platforms. 23 | 24 | ## Migration Guide to V2 25 | 26 | ⚠️ `1.x.x versions` of the SDK are **_deprecated_**, these versions won't work after **_2024-01-31_**. Please migrate to the latest version. You can find the migration guide [HERE](https://github.com/guildxyz/guild-sdk/blob/main/v2-migration-guide.md#guild-sdk-v2-migration-guide). 27 | 28 | ## Demo app 29 | 30 | A demo app is available [here](https://github.com/guildxyz/guild-sdk/tree/main/examples/next-js), it shows how to: 31 | 32 | - Connect wallet (with wagmi) 33 | - Sign a message 34 | - Get user profile with signature 35 | - Fetch data (with SWR) 36 | - Guild roles 37 | - User memberships 38 | - Leaderboard 39 | 40 | ## Contents 41 | 42 | - [Installation](#installation) 43 | - [Importing the package and creating a Guild client](#importing-the-package-and-creating-a-guild-client) 44 | - [SignerFunctions and Authentication](#signerfunctions-and-authentication) 45 | - [ethers.js](#creating-a-signer-from-an-ethers-wallet) 46 | - [web3-react](#creating-a-custom-signer-for-usage-with-web3-react) 47 | - [wagmi](#creating-a-custom-signer-for-usage-with-wagmi) 48 | - [EIP-1271](#support-for-eip-1271-smart-contract-wallets) 49 | - [Clients](#clients) 50 | - [Guild client](#guild-client) 51 | - [Points](#points) 52 | - [Guild admin client](#guild-admin-client) 53 | - [Guild reward client](#guild-reward-client) 54 | - [Role client](#role-client) 55 | - [Requirement client](#requirement-client) 56 | - [Role reward client](#role-reward-client) 57 | - [User client](#user-client) 58 | - [User address client](#user-address-client) 59 | - [User platform client](#user-platform-client) 60 | - [Modular / multi-platform architecture](#modular--multi-platform-architecture) 61 | - [Examples](#examples) 62 | - [Example flow from Create Guild to Join](#example-flow-from-create-guild-to-join) 63 | - [Multiple telegram groups guild](#multiple-telegram-groups-guild) 64 | 65 | ### Installation 66 | 67 | To install our SDK, open your terminal and run: 68 | 69 | ``` 70 | npm i @guildxyz/sdk 71 | ``` 72 | 73 | ### Importing the package and creating a Guild client 74 | 75 | ```typescript 76 | import { createGuildClient, createSigner } from "@guildxyz/sdk"; 77 | 78 | // The only parameter is the name of your project 79 | const guildClient = createGuildClient("My project"); 80 | ``` 81 | 82 | ### SignerFunctions and Authentication 83 | 84 | #### `Creating a signer from an ethers wallet` 85 | 86 | ```ts 87 | import { ethers } from "ethers"; 88 | 89 | const ethersWallet = new ethers.Wallet(...); 90 | 91 | const signerFunction = createSigner.fromEthersWallet(ethersWallet); 92 | ``` 93 | 94 | #### `Creating a custom signer for usage with web3-react` 95 | 96 | ```ts 97 | import { useWeb3React } from "@web3-react/core"; 98 | 99 | const { account: walletAddress, library } = useWeb3React(); 100 | 101 | const signerFunction = createSigner.custom( 102 | (message) => library.getSigner(account).signMessage(signableMessage), 103 | address 104 | ); 105 | ``` 106 | 107 | #### `Creating a custom signer for usage with wagmi` 108 | 109 | ```ts 110 | import { useAccount, useSignMessage } from "wagmi"; 111 | 112 | const { signMessageAsync } = useSignMessage(); 113 | const { address } = useAccount(); 114 | 115 | const signerFunction = createSigner.custom( 116 | (message) => signMessageAsync({ message }), 117 | address 118 | ); 119 | ``` 120 | 121 | #### `Support for EIP-1271 smart contract wallets` 122 | 123 | For signatures produced by EIP-1272 wallets, pass `{ chainIdOfSmartContractWallet: chainId }` as the third parameter of `createSigner.custom`, where `chainId` is the chain where the wallet operates. The Guild backend will try to call `isValidSignature` on the specified chain. 124 | We have an example app under [`examples`](https://github.com/guildxyz/guild-sdk/tree/main/examples), which covers this parameter 125 | 126 | ### Clients 127 | 128 | We have multiple clients for different entities. These clients are created from the `guildClient` that we created above. 129 | 130 | #### `Guild client` 131 | 132 | ```ts 133 | const { guild: client } = guildClient; 134 | 135 | // Get Guild by its numeric ID 136 | const guild = await client.get(guildId); 137 | 138 | // Get Guild by its urlName (slug) 139 | const guild = await client.get(urlName); 140 | 141 | // Get multiple guilds by their IDs 142 | const guilds = await client.getMany([guildId1, guildId2]); 143 | 144 | // Search guilds with pagination 145 | const guilds = await client.search({ limit: 10, offset: 0, search: "our" }); 146 | 147 | // Get the members of a guild 148 | const members = await client.getMembers( 149 | guildId, 150 | signerFunction // Optional, if a valid signer is provided, the result will contain private data 151 | ); 152 | 153 | // Join into a guild 154 | const joinResult = await client.join(guildId, signerFunction); 155 | 156 | // Check access to a guild, signerFuncion is required 157 | await client.accessCheck(guildId, signerFunction); 158 | 159 | // Create a new guild, check the possible creation parameters according to the typing 160 | await client.create( 161 | { 162 | // In this example we are creating a guild with one FREE role 163 | name: "My Guild", 164 | urlName: "my-guild", 165 | roles: [{ name: "My Role", requirements: [{ type: "FREE" }] }], 166 | }, 167 | signerFunction 168 | ); 169 | 170 | // Update an existing guild, check the possible update parameters according to the typing 171 | await client.update(guildId, { description: "Edited" }, signerFunction); 172 | 173 | // Delete a guild 174 | await client.delete(guildId, signerFunction); 175 | ``` 176 | 177 | ##### `Points` 178 | 179 | ```ts 180 | // For a given role, create a new point system, and assign some points as reward 181 | const created = await guild.role.reward.create( 182 | guildId, 183 | roleId, // This role will have the 5 points reward 184 | { 185 | guildPlatform: { 186 | platformGuildId: "my-points", // Some unique name for your point system 187 | platformName: "POINTS", 188 | platformGuildData: { name: "coins" }, // Assign a custom name for the points 189 | }, 190 | platformRoleData: { score: 5 }, // Members will get this many points 191 | }, 192 | signerFunction 193 | ); 194 | 195 | // Use an existing point system for a role 196 | const created = await guild.role.reward.create( 197 | guildId, 198 | roleId, // This role will have the 10 points reward 199 | { 200 | guildPlatformId, // The ID of the existing guildPlatform (reward) object 201 | platformRoleData: { score: 10 }, 202 | }, 203 | signerFunction 204 | ); 205 | 206 | // Get leaderboard for a specific point guild reward 207 | const { leaderboard, aroundUser } = await guild.getLeaderboard( 208 | guildId, 209 | guildPlatformId, 210 | signerFunction // Optional. If provided, the response will include an "aroundUser" field, which contains leaderboard items from around the user's position, otherwise it will be undefined 211 | ); 212 | 213 | // Get user's rank in a specific reward 214 | const response = await user.getRankInGuild(userId, guildId, guildPlatformId); // Returns the leaderboard position of a user for the given reward 215 | 216 | // Get all the points of a user across all relevant rewards 217 | const response = await user.getPoints(userId, signerFunction); 218 | ``` 219 | 220 | #### `Guild admin client` 221 | 222 | ```ts 223 | const { 224 | guild: { admin: adminClient }, 225 | } = guildClient; 226 | 227 | // Get all admins of a guild 228 | const admins = await adminClient.getAll(guildIdOrUrlName); 229 | 230 | // Get a specific admin of a guild 231 | const admin = await adminClient.get(guildIdOrUrlName, userIdOfAdmin); 232 | ``` 233 | 234 | #### `Guild reward client` 235 | 236 | ```ts 237 | const { 238 | guild: { reward: guildRewardClient }, 239 | } = guildClient; 240 | 241 | // Get a guild reward (like a Discord server) 242 | const guildReward = await guildRewardClient.get( 243 | guildIdOrUrlName, 244 | guildPlatformId, 245 | signerFunction // Optional, if a valid signer is provided, the result will contain private data 246 | ); 247 | 248 | // Get all rewards of a guild 249 | const guildRewards = await guildRewardClient.getAll( 250 | guildIdOrUrlName, 251 | signerFunction // Optional, if a valid signer is provided, the result will contain private data 252 | ); 253 | 254 | // Add a new reward to a guild 255 | const createdGuildReward = await guildRewardClient.create(guildIdOrUrlName, { 256 | platformName: "DISCORD", // In this example we are adding a Discord server 257 | platformGuildId: "", 258 | }); 259 | 260 | // Delete a reward from a guild 261 | await guildRewardClient.delete( 262 | guildIdOrUrlName, 263 | guildPlatformId, 264 | signerFunction 265 | ); 266 | ``` 267 | 268 | #### `Role client` 269 | 270 | ```ts 271 | const { 272 | guild: { role: roleClient }, 273 | } = guildClient; 274 | 275 | // Get a role 276 | await roleClient.get( 277 | guildIdOrUrlName, 278 | roleId, 279 | signerFunction // Optional, if a valid signer is provided, the result will contain private data 280 | ); 281 | 282 | // Get all roles of a guild 283 | await roleClient.getAll( 284 | guildIdOrUrlName, 285 | signerFunction // Optional, if a valid signer is provided, the result will contain private data 286 | ); 287 | 288 | // Create a new role. Refer to the typing for other possible input parameters, like description, logic, or visibility 289 | const createdRole = await roleClient.create( 290 | guildIdOrUrlName, 291 | { 292 | name: "My new role", 293 | requirements: [{ type: "FREE" }], 294 | }, 295 | signerFunction 296 | ); 297 | 298 | // Update an existing role 299 | const updatedRole = await roleClient.update( 300 | guildIdOrUrlName, 301 | roleId, 302 | { description: "Edited" }, 303 | signerFunction 304 | ); 305 | 306 | // Delete a role 307 | await roleClient.delete(guildIdOrUrlName, roleId, signerFunction); 308 | ``` 309 | 310 | #### `Requirement client` 311 | 312 | ```ts 313 | const { 314 | guild: { 315 | role: { requirement: requirementClient }, 316 | }, 317 | } = guildClient; 318 | 319 | // Get a requirement 320 | const requirement = await requirementClient.get( 321 | guildIdOrUrlName, 322 | roleId, 323 | requirementId, 324 | signerFunction // Optional, if a valid signer is provided, the result will contain private data 325 | ); 326 | 327 | // Get all requirements of a role 328 | const requirements = await requirementClient.getAll( 329 | guildIdOrUrlName, 330 | roleId, 331 | signerFunction // Optional, if a valid signer is provided, the result will contain private data 332 | ); 333 | 334 | // Create a new requirement 335 | const createdRequirement = await requirementClient.create( 336 | guildIdOrUrlName, 337 | roleId, 338 | { type: "FREE" }, 339 | signerFunction 340 | ); 341 | 342 | // Update an existing requirement (for example addresses in an ALLOWLIST requirement) 343 | const updatedRequirement = await requirementClient.update( 344 | guildIdOrUrlName, 345 | roleId, 346 | requirementId, 347 | { data: { addresses: ["0x..."] } }, // Lowercased addresses 348 | signerFunction 349 | ); 350 | 351 | // Delete a requirement 352 | await requirementClient.delete( 353 | guildIdOrUrlName, 354 | roleId, 355 | requirementId, 356 | signerFunction 357 | ); 358 | ``` 359 | 360 | #### `Role reward client` 361 | 362 | ```ts 363 | const { 364 | guild: { 365 | role: { reward: rewardClient }, 366 | }, 367 | } = guildClient; 368 | 369 | // Get a role reward 370 | const reward = rewardClient.get( 371 | guildIdOrUrlName, 372 | roleId, 373 | rolePlatformId, 374 | signerFunction // Optional, if a valid signer is provided, the result will contain private data 375 | ); 376 | 377 | // Get all rewards of a role 378 | const roleReward = rewardClient.getAll( 379 | guildIdOrUrlName, 380 | roleId, 381 | signerFunction // Optional, if a valid signer is provided, the result will contain private data 382 | ); 383 | 384 | // Create a role reward (for example a Discord role) with a guild reward (Discord server) 385 | const createdReward = rewardClient.create( 386 | guildIdOrUrlName, 387 | roleId, 388 | { 389 | guildPlatform: { 390 | // Here we are also creating a guild reward (the Discord server) 391 | platformName: "DISCORD", 392 | platformGuildId: "", 393 | }, 394 | platformRoleId: "", 395 | }, 396 | signerFunction 397 | ); 398 | 399 | // Or create a role reward using an existing guild reward 400 | const createdReward = rewardClient.create( 401 | guildIdOrUrlName, 402 | roleId, 403 | { 404 | guildPlatformId, // Here we are passing the id of an existing guild role (in this case a Discord server) 405 | platformRoleId: "", 406 | }, 407 | signerFunction 408 | ); 409 | 410 | // Update an existing role reward 411 | const updatedRoleReward = rewardClient.update( 412 | guildIdOrUrlName, 413 | roleId, 414 | rolePlatformId, 415 | { visibility: "HIDDEN" }, // In this example we update a reward's visibility to HIDDEN 416 | signerFunction 417 | ); 418 | 419 | // Delete a role reward 420 | rewardClient.delete(guildIdOrUrlName, roleId, rolePlatformId, signerFunction); 421 | ``` 422 | 423 | #### `User client` 424 | 425 | ```ts 426 | const { user: userClient } = guildClient; 427 | 428 | // Get a user by numeric ID, or an address 429 | const user = await userClient.get(userIdOrAddress); 430 | 431 | // Get current memberships of a user 432 | const userMemberships = await userClient.getMemberships( 433 | userIdOrAddress, 434 | signerFunction // Optional, if a valid signer is provided, the result will contain private data 435 | ); 436 | 437 | // Get a user's profile 438 | const profile = await userClient.getProfile( 439 | userIdOrAddress, 440 | signerFunction // Optional, if a valid signer is provided, the result will contain private data 441 | ); 442 | 443 | // Delete a user 444 | await userClient.delete(userIdOrAddress, signerFunction); 445 | ``` 446 | 447 | #### `User address client` 448 | 449 | ```ts 450 | const { 451 | user: { address: userAddressClient }, 452 | } = guildClient; 453 | 454 | // Get a user address 455 | const userAddress = await userAddressClient.get( 456 | userIdOrAddress, // Used for identifying the guild user 457 | address, // The userAddress with this address will be returned 458 | signerFunction 459 | ); 460 | 461 | // Get all addresses of a user 462 | const userAddresses = await userAddressClient.getAll( 463 | userIdOrAddress, 464 | signerFunction 465 | ); 466 | 467 | // Create (connect / link) a new user address 468 | const linkedAddress = await userAddressClient.create( 469 | userIdOrAddress, 470 | signerFunctionOfAddressToLink, // Should be a SignerFunction that is derived from the wallet that is being linked to the user. Can be obtained as described above 471 | signerFunction 472 | ); 473 | 474 | // Update a user address 475 | const updatedUserAddress = await userAddressClient.update( 476 | userIdOrAddress, 477 | addressToUpdate, 478 | { isPrimary: true }, // In this example we update an address to be primary 479 | signerFunction 480 | ); 481 | 482 | // Delete (disconnect / unlink) a user address 483 | await userAddressClient.delete( 484 | userIdOrAddress, 485 | addressToUpdate, 486 | signerFunction 487 | ); 488 | ``` 489 | 490 | #### `User platform client` 491 | 492 | ```ts 493 | const { 494 | user: { platform: userPlatformClient }, 495 | } = guildClient; 496 | 497 | // Get a user platform connection 498 | const userPlatform = await userPlatformClient.get( 499 | userIdOrAddress, 500 | platformId, 501 | signerFunction 502 | ); 503 | 504 | // Get all platform connections of a user 505 | const userPlatforms = await userPlatformClient.getAll( 506 | userIdOrAddress, 507 | signerFunction 508 | ); 509 | 510 | // Delete (disconnect / unlink) a platform connection 511 | await userPlatformClient.delete(userIdOrAddress, platformId, signerFunction); 512 | ``` 513 | 514 | ### Modular / multi-platform architecture 515 | 516 | Guild.xyz no longer limits its platform gating functionalities to a single gateable Discord server or Telegram group. In the new multi-platform architecture you can gate more platforms in a single guild/role. 517 | 518 | The `guildPlatform` entity refers to a platform gated by the guild. It contains information about the gate platform, e.g.: a Discord server's id (`platformGuildId` which is a uniqu identifier of this platform) and optionally some additional data like the `inviteChannel` in the `platformRoleData` property in this case. 519 | 520 | The `rolePlatform` entity connects a `guildPlatform` to a role indicating that this role gives access to that platform. It can also contain some additional information about the platform (`platformRoleId` and `platformRoleData`), in Discord's case it's the Discord-role's id. 521 | 522 | Note that for example in Telegram's case `platformRoleId` is not required; only `platformGuild` (which refers to a telegram group's id) needs to be provided in `guildPlatform`. 523 | 524 | ### Examples 525 | 526 | #### `Example flow from Create Guild to Join` 527 | 528 | ```ts 529 | import { createGuildClient, createSigner } from "@guildxyz/sdk"; 530 | import { Wallet } from "ethers"; 531 | import { randomBytes } from "crypto"; 532 | 533 | // Creating a guild client 534 | const guildClient = createGuildClient("sdk-readme-example"); 535 | 536 | // Creating a random wallet for the example 537 | const wallet = new Wallet(randomBytes(32).toString("hex")); 538 | 539 | // Creating a signer function 540 | const signerFunction = createSigner.fromEthersWallet(wallet); 541 | 542 | // Creating a Guild 543 | 544 | await guildClient.guild.create( 545 | { 546 | name: "My New Guild", 547 | urlName: "my-new-guild-123", // Optinal 548 | description: "Cool stuff", // Optional 549 | admins: ["0x916b1aBC3C38852B338a22B08aF19DEe14113627"], // Optional 550 | showMembers: true, // Optional 551 | hideFromExplorer: false, // Optional 552 | theme: [{ color: "#000000" }], // Optional 553 | guildPlatforms: [ 554 | // Optional (declaring the gated platforms) 555 | { 556 | platformName: "DISCORD", 557 | platformGuildId: "717317894983225012", 558 | platformGuildData: { inviteChannel: "832195274127999019" }, 559 | }, 560 | ], 561 | roles: [ 562 | { 563 | name: "My First Role", 564 | logic: "AND", 565 | requirements: [ 566 | { 567 | type: "ALLOWLIST", 568 | data: { 569 | addresses: [ 570 | "0xedd9C1954c77beDD8A2a524981e1ea08C7E484Be", 571 | "0x1b64230Ad5092A4ABeecE1a50Dc7e9e0F0280304", 572 | ], 573 | }, 574 | }, 575 | ], 576 | rolePlatforms: [ 577 | // Optional (connecting gated platforms to the role) 578 | { 579 | guildPlatformIndex: 0, 580 | platformRoleId: "947846353822178118", 581 | }, 582 | ], 583 | }, 584 | { 585 | name: "My Second Role", 586 | logic: "OR", 587 | requirements: [ 588 | { 589 | type: "ERC20", 590 | chain: "ETHEREUM", 591 | address: "0xf76d80200226ac250665139b9e435617e4ba55f9", 592 | data: { 593 | amount: 1, 594 | }, 595 | }, 596 | { 597 | type: "ERC721", 598 | chain: "ETHEREUM", 599 | address: "0x734AA2dac868218D2A5F9757f16f6f881265441C", 600 | data: { 601 | amount: 1, 602 | }, 603 | }, 604 | ], 605 | rolePlatforms: [ 606 | // Optional (connecting gated platforms to the role) 607 | { 608 | guildPlatformIndex: 0, 609 | platformRoleId: "283446353822178118", 610 | }, 611 | ], 612 | }, 613 | ], 614 | }, 615 | signerFunction 616 | ); 617 | 618 | // Joining to a Guild if any role is accessible by the given address 619 | await guildClient.guild.join(myGuild.id, signerFunction); 620 | ``` 621 | 622 | #### `Multiple telegram groups guild` 623 | 624 | ```typescript 625 | const myGuild = await guildClient.guild.create( 626 | { 627 | name: "My Telegram Guild", 628 | guildPlatforms: [ 629 | { 630 | platformName: "TELEGRAM", // Telegram group 0 631 | platformGuildId: "-1001190870894", 632 | }, 633 | { 634 | platformName: "TELEGRAM", // Telegram group 1 635 | platformGuildId: "-1003847238493", 636 | }, 637 | { 638 | platformName: "TELEGRAM", // Telegram group 2 639 | platformGuildId: "-1008347384212", 640 | }, 641 | ], 642 | roles: [ 643 | { 644 | name: "My First Role", 645 | logic: "AND", 646 | requirements: [ 647 | { 648 | type: "ALLOWLIST", 649 | data: { 650 | addresses: [ 651 | "0xedd9C1954c77beDD8A2a524981e1ea08C7E484Be", 652 | "0x1b64230Ad5092A4ABeecE1a50Dc7e9e0F0280304", 653 | ], 654 | }, 655 | }, 656 | ], 657 | rolePlatforms: [ 658 | { 659 | guildPlatformIndex: 0, // Telegram group 0 660 | }, 661 | { 662 | guildPlatformIndex: 2, // Telegram group 2 663 | }, 664 | ], 665 | }, 666 | { 667 | name: "My Second Role", 668 | logic: "OR", 669 | requirements: [ 670 | { 671 | type: "ERC20", 672 | chain: "ETHEREUM", 673 | address: "0xf76d80200226ac250665139b9e435617e4ba55f9", 674 | data: { 675 | amount: 1, 676 | }, 677 | }, 678 | ], 679 | rolePlatforms: [ 680 | { 681 | guildPlatformIndex: 1, // Telegram group 1 682 | }, 683 | ], 684 | }, 685 | ], 686 | }, 687 | signerFunction 688 | ); 689 | ``` 690 | -------------------------------------------------------------------------------- /examples/custom-point-distribution/README.md: -------------------------------------------------------------------------------- 1 | # Custom point distribution 2 | 3 | This example script shows how you can distribute points on Guild based on custom data / logic 4 | 5 | ## Setup 6 | 7 | - Install the dependencies: `npm i` 8 | - Set the `PRIVATE_KEY` environment variable: `export PRIVATE_KEY=0x...` 9 | - If you already have a Guild, in which you intend to create the point reward, the account of the private key has to be an admin in that Guild, so it has the necessary permissions to create and update the points 10 | 11 | ## Creating a Guild 12 | 13 | This example app can create a Guild by executing `npx ts-node index.js create-guild` 14 | It will print the Guild's URL, and the relevant ID-s for the point updates 15 | 16 | It sets the following things in the Guild: 17 | 18 | - Basic data (its name, urlName, ...) 19 | - A role with a GUILD_SNAPSHOT requirement. This requirement will be the source of the point distribution 20 | - A POINTS reward, which taks it's input from the requirement 21 | 22 | ## Editing the points 23 | 24 | The script shows how you can edit the points by running `npx ts-node index.js edit {guildId} {roleId} {requirementId}` 25 | 26 | The `snapshot` represents the point distribution. It maps addresses to point values 27 | 28 | > When a snapshot is updated this way, the whole previous `data` field fill be overwritten, so you'll have to call this update with the whole list every time it changes 29 | -------------------------------------------------------------------------------- /examples/custom-point-distribution/index.ts: -------------------------------------------------------------------------------- 1 | import { createGuildClient, createSigner } from "@guildxyz/sdk"; 2 | import { randomBytes } from "crypto"; 3 | import { privateKeyToAccount } from "viem/accounts"; 4 | 5 | const YOUR_PROJECT_NAME = "snapshot-test-snippet"; 6 | 7 | const client = createGuildClient(YOUR_PROJECT_NAME); 8 | 9 | if (!process.env.PRIVATE_KEY) { 10 | throw new Error("Please set the PRIVATE_KEY env var"); 11 | } 12 | 13 | // Load an account here (viem is just an example, any library works by implementing the singer function in createSigner.custom()) 14 | const viemAccount = privateKeyToAccount( 15 | process.env.PRIVATE_KEY as `0x${string}` 16 | ); 17 | // Pass a function which signs a message with the admin account, and the address of this account 18 | const signer = createSigner.custom( 19 | (message) => viemAccount.signMessage({ message }), 20 | viemAccount.address 21 | ); 22 | 23 | // If you don't have a guild yet, you can create one by calling this function 24 | async function createGuildWithSnapshot() { 25 | try { 26 | const createdGuild = await client.guild.create( 27 | { 28 | name: "For snapshot testing", 29 | urlName: "snapshot-testing", 30 | contacts: [], 31 | roles: [ 32 | { 33 | name: "Test role", 34 | requirements: [ 35 | { 36 | type: "GUILD_SNAPSHOT", 37 | data: { 38 | snapshot: [ 39 | { 40 | key: "0x0000000000000000000000000000000000000000", 41 | value: 10, 42 | }, 43 | ], 44 | }, 45 | }, 46 | ], 47 | }, 48 | ], 49 | }, 50 | signer 51 | ); 52 | 53 | console.log("Guild created!"); 54 | 55 | const guildId = createdGuild.id; 56 | const roleId = createdGuild.roles[0].id; 57 | const requirementId = createdGuild.roles[0].requirements[0].id; 58 | 59 | console.log(createdGuild.guildPlatforms); 60 | 61 | // Created the reward on the guild 62 | await client.guild.role.reward.create( 63 | guildId, 64 | roleId, 65 | { 66 | guildPlatform: { 67 | platformName: "POINTS", 68 | platformGuildId: `unique-name-${randomBytes(4).toString("hex")}`, 69 | platformGuildData: { 70 | name: "Tokens", 71 | }, 72 | }, 73 | platformRoleData: { score: "0" }, 74 | dynamicAmount: { 75 | operation: { 76 | type: "LINEAR", 77 | input: [ 78 | { 79 | type: "REQUIREMENT_AMOUNT", 80 | roleId, 81 | requirementId, 82 | }, 83 | ], 84 | }, 85 | }, 86 | }, 87 | signer 88 | ); 89 | console.log(`https://guild.xyz/${createdGuild.urlName}`); 90 | console.log( 91 | `You can now edit the snapshot with:\nnpx ts-node index.ts edit ${guildId} ${roleId} ${requirementId}` 92 | ); 93 | return createdGuild; 94 | } catch (error) { 95 | console.error(error); 96 | throw error; 97 | } 98 | } 99 | 100 | // This function edits an existing snapshot 101 | async function editSnapshot( 102 | guildId: number, 103 | roleId: number, 104 | requirementId: number 105 | ) { 106 | try { 107 | const editedRequirement = await client.guild.role.requirement.update( 108 | guildId, 109 | roleId, 110 | requirementId, 111 | { 112 | data: { 113 | // The provided snapshot overwrites the previous array 114 | snapshot: [ 115 | { key: "0x0000000000000000000000000000000000000000", value: 12 }, 116 | { key: "0x0000000000000000000000000000000000000001", value: 10 }, 117 | ], 118 | }, 119 | }, 120 | signer 121 | ); 122 | console.log("Snapshot edited!"); 123 | return editedRequirement; 124 | } catch (error) { 125 | console.error(error); 126 | throw error; 127 | } 128 | } 129 | 130 | /** 131 | * Set the `PRIVATE_KEY` env var. It should be an admin in the snapshot's guild, 132 | * so it has access to edit the snapshot 133 | * 134 | * Create a guild with `npx ts-node index.ts create-guild` 135 | * 136 | * Edit the snapshot with `npx ts-node index.ts edit {guildId} {roleId} {requirementId}` 137 | * 138 | * The three ID-s for editing can be retrieved by the 139 | * https://api.guild.xyz/v2/guilds/guild-page/{urlName} endpoint. 140 | * The creation command also outputs them for the freshly created guild 141 | * 142 | * If the guild is created externally, make sure that the `PRIVATE_KEY`'s account is 143 | * added to the guild as an admin 144 | */ 145 | async function main() { 146 | const [, , command, ...params] = process.argv; 147 | switch (command) { 148 | case "create-guild": { 149 | await createGuildWithSnapshot(); 150 | break; 151 | } 152 | case "edit": { 153 | const [guildIdStr, roleIdStr, requirementIdStr] = params; 154 | await editSnapshot(+guildIdStr, +roleIdStr, +requirementIdStr); 155 | break; 156 | } 157 | } 158 | } 159 | 160 | main(); 161 | -------------------------------------------------------------------------------- /examples/custom-point-distribution/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "custom-point-distribution", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": {}, 7 | "author": "", 8 | "license": "ISC", 9 | "dependencies": { 10 | "@guildxyz/sdk": "^2.5.0", 11 | "viem": "^2.17.10" 12 | }, 13 | "devDependencies": { 14 | "ts-node": "^10.9.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/next-js/.env: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=9c4d98baa956e03db8fa96f45d6aa78e -------------------------------------------------------------------------------- /examples/next-js/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /examples/next-js/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | 37 | 38 | !.env -------------------------------------------------------------------------------- /examples/next-js/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /examples/next-js/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { reactStrictMode: false }; 3 | 4 | module.exports = nextConfig; 5 | -------------------------------------------------------------------------------- /examples/next-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-js", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@chakra-ui/react": "^2.8.2", 13 | "@emotion/react": "^11.11.3", 14 | "@emotion/styled": "^11.11.0", 15 | "@guildxyz/sdk": "^2.6.8", 16 | "@guildxyz/types": "^1.10.0", 17 | "framer-motion": "^11.0.3", 18 | "next": "latest", 19 | "react": "latest", 20 | "react-dom": "latest", 21 | "swr": "^2.2.4", 22 | "viem": "^1.12.2", 23 | "wagmi": "^1.4.2" 24 | }, 25 | "devDependencies": { 26 | "@types/node": "latest", 27 | "@types/react": "latest", 28 | "@types/react-dom": "latest", 29 | "eslint": "latest", 30 | "eslint-config-next": "latest", 31 | "typescript": "latest" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/next-js/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/next-js/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/next-js/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/guildxyz/guild-sdk/75470add817abdac0842847b89757473cf0b0a5c/examples/next-js/src/app/favicon.ico -------------------------------------------------------------------------------- /examples/next-js/src/app/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --max-width: 1100px; 3 | --border-radius: 12px; 4 | --font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', 5 | 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro', 6 | 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace; 7 | 8 | --foreground-rgb: 0, 0, 0; 9 | --background-start-rgb: 214, 219, 220; 10 | --background-end-rgb: 255, 255, 255; 11 | 12 | --primary-glow: conic-gradient( 13 | from 180deg at 50% 50%, 14 | #16abff33 0deg, 15 | #0885ff33 55deg, 16 | #54d6ff33 120deg, 17 | #0071ff33 160deg, 18 | transparent 360deg 19 | ); 20 | --secondary-glow: radial-gradient( 21 | rgba(255, 255, 255, 1), 22 | rgba(255, 255, 255, 0) 23 | ); 24 | 25 | --tile-start-rgb: 239, 245, 249; 26 | --tile-end-rgb: 228, 232, 233; 27 | --tile-border: conic-gradient( 28 | #00000080, 29 | #00000040, 30 | #00000030, 31 | #00000020, 32 | #00000010, 33 | #00000010, 34 | #00000080 35 | ); 36 | 37 | --callout-rgb: 238, 240, 241; 38 | --callout-border-rgb: 172, 175, 176; 39 | --card-rgb: 180, 185, 188; 40 | --card-border-rgb: 131, 134, 135; 41 | } 42 | 43 | @media (prefers-color-scheme: dark) { 44 | :root { 45 | --foreground-rgb: 255, 255, 255; 46 | --background-start-rgb: 0, 0, 0; 47 | --background-end-rgb: 0, 0, 0; 48 | 49 | --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); 50 | --secondary-glow: linear-gradient( 51 | to bottom right, 52 | rgba(1, 65, 255, 0), 53 | rgba(1, 65, 255, 0), 54 | rgba(1, 65, 255, 0.3) 55 | ); 56 | 57 | --tile-start-rgb: 2, 13, 46; 58 | --tile-end-rgb: 2, 5, 19; 59 | --tile-border: conic-gradient( 60 | #ffffff80, 61 | #ffffff40, 62 | #ffffff30, 63 | #ffffff20, 64 | #ffffff10, 65 | #ffffff10, 66 | #ffffff80 67 | ); 68 | 69 | --callout-rgb: 20, 20, 20; 70 | --callout-border-rgb: 108, 108, 108; 71 | --card-rgb: 100, 100, 100; 72 | --card-border-rgb: 200, 200, 200; 73 | } 74 | } 75 | 76 | * { 77 | box-sizing: border-box; 78 | padding: 0; 79 | margin: 0; 80 | } 81 | 82 | html, 83 | body { 84 | max-width: 100vw; 85 | overflow-x: hidden; 86 | } 87 | 88 | body { 89 | color: rgb(var(--foreground-rgb)); 90 | background: linear-gradient( 91 | to bottom, 92 | transparent, 93 | rgb(var(--background-end-rgb)) 94 | ) 95 | rgb(var(--background-start-rgb)); 96 | } 97 | 98 | a { 99 | color: inherit; 100 | text-decoration: none; 101 | } 102 | 103 | @media (prefers-color-scheme: dark) { 104 | html { 105 | color-scheme: dark; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /examples/next-js/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Inter } from "next/font/google"; 4 | import React from "react"; 5 | import { createPublicClient, http } from "viem"; 6 | import { polygon } from "viem/chains"; 7 | import { WagmiConfig, createConfig } from "wagmi"; 8 | import "../lib/guild"; 9 | import "./globals.css"; 10 | 11 | const inter = Inter({ subsets: ["latin"] }); 12 | 13 | const config = createConfig({ 14 | autoConnect: true, 15 | publicClient: createPublicClient({ 16 | chain: polygon, 17 | transport: http(), 18 | }), 19 | }); 20 | 21 | export default function RootLayout({ 22 | children, 23 | }: { 24 | children: React.ReactNode; 25 | }) { 26 | return ( 27 | 28 | 29 | {children} 30 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /examples/next-js/src/app/page.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: space-between; 5 | align-items: center; 6 | padding: 6rem; 7 | min-height: 100vh; 8 | } 9 | 10 | .description { 11 | display: inherit; 12 | justify-content: inherit; 13 | align-items: inherit; 14 | font-size: 0.85rem; 15 | max-width: var(--max-width); 16 | width: 100%; 17 | z-index: 2; 18 | font-family: var(--font-mono); 19 | } 20 | 21 | .description a { 22 | display: flex; 23 | justify-content: center; 24 | align-items: center; 25 | gap: 0.5rem; 26 | } 27 | 28 | .description p { 29 | position: relative; 30 | margin: 0; 31 | padding: 1rem; 32 | background-color: rgba(var(--callout-rgb), 0.5); 33 | border: 1px solid rgba(var(--callout-border-rgb), 0.3); 34 | border-radius: var(--border-radius); 35 | } 36 | 37 | .code { 38 | font-weight: 700; 39 | font-family: var(--font-mono); 40 | } 41 | 42 | .grid { 43 | display: grid; 44 | grid-template-columns: repeat(4, minmax(25%, auto)); 45 | max-width: 100%; 46 | width: var(--max-width); 47 | } 48 | 49 | .card { 50 | padding: 1rem 1.2rem; 51 | border-radius: var(--border-radius); 52 | background: rgba(var(--card-rgb), 0); 53 | border: 1px solid rgba(var(--card-border-rgb), 0); 54 | transition: background 200ms, border 200ms; 55 | } 56 | 57 | .card span { 58 | display: inline-block; 59 | transition: transform 200ms; 60 | } 61 | 62 | .card h2 { 63 | font-weight: 600; 64 | margin-bottom: 0.7rem; 65 | } 66 | 67 | .card p { 68 | margin: 0; 69 | opacity: 0.6; 70 | font-size: 0.9rem; 71 | line-height: 1.5; 72 | max-width: 30ch; 73 | } 74 | 75 | .center { 76 | display: flex; 77 | justify-content: center; 78 | align-items: center; 79 | position: relative; 80 | padding: 4rem 0; 81 | } 82 | 83 | .center::before { 84 | background: var(--secondary-glow); 85 | border-radius: 50%; 86 | width: 480px; 87 | height: 360px; 88 | margin-left: -400px; 89 | } 90 | 91 | .center::after { 92 | background: var(--primary-glow); 93 | width: 240px; 94 | height: 180px; 95 | z-index: -1; 96 | } 97 | 98 | .center::before, 99 | .center::after { 100 | content: ''; 101 | left: 50%; 102 | position: absolute; 103 | filter: blur(45px); 104 | transform: translateZ(0); 105 | } 106 | 107 | .logo { 108 | position: relative; 109 | } 110 | /* Enable hover only on non-touch devices */ 111 | @media (hover: hover) and (pointer: fine) { 112 | .card:hover { 113 | background: rgba(var(--card-rgb), 0.1); 114 | border: 1px solid rgba(var(--card-border-rgb), 0.15); 115 | } 116 | 117 | .card:hover span { 118 | transform: translateX(4px); 119 | } 120 | } 121 | 122 | @media (prefers-reduced-motion) { 123 | .card:hover span { 124 | transform: none; 125 | } 126 | } 127 | 128 | /* Mobile */ 129 | @media (max-width: 700px) { 130 | .content { 131 | padding: 4rem; 132 | } 133 | 134 | .grid { 135 | grid-template-columns: 1fr; 136 | margin-bottom: 120px; 137 | max-width: 320px; 138 | text-align: center; 139 | } 140 | 141 | .card { 142 | padding: 1rem 2.5rem; 143 | } 144 | 145 | .card h2 { 146 | margin-bottom: 0.5rem; 147 | } 148 | 149 | .center { 150 | padding: 8rem 0 6rem; 151 | } 152 | 153 | .center::before { 154 | transform: none; 155 | height: 300px; 156 | } 157 | 158 | .description { 159 | font-size: 0.8rem; 160 | } 161 | 162 | .description a { 163 | padding: 1rem; 164 | } 165 | 166 | .description p, 167 | .description div { 168 | display: flex; 169 | justify-content: center; 170 | position: fixed; 171 | width: 100%; 172 | } 173 | 174 | .description p { 175 | align-items: center; 176 | inset: 0 0 auto; 177 | padding: 2rem 1rem 1.4rem; 178 | border-radius: 0; 179 | border: none; 180 | border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); 181 | background: linear-gradient( 182 | to bottom, 183 | rgba(var(--background-start-rgb), 1), 184 | rgba(var(--callout-rgb), 0.5) 185 | ); 186 | background-clip: padding-box; 187 | backdrop-filter: blur(24px); 188 | } 189 | 190 | .description div { 191 | align-items: flex-end; 192 | pointer-events: none; 193 | inset: auto 0 0; 194 | padding: 2rem; 195 | height: 200px; 196 | background: linear-gradient( 197 | to bottom, 198 | transparent 0%, 199 | rgb(var(--background-end-rgb)) 40% 200 | ); 201 | z-index: 1; 202 | } 203 | } 204 | 205 | /* Tablet and Smaller Desktop */ 206 | @media (min-width: 701px) and (max-width: 1120px) { 207 | .grid { 208 | grid-template-columns: repeat(2, 50%); 209 | } 210 | } 211 | 212 | @media (prefers-color-scheme: dark) { 213 | .vercelLogo { 214 | filter: invert(1); 215 | } 216 | 217 | .logo { 218 | filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); 219 | } 220 | } 221 | 222 | @keyframes rotate { 223 | from { 224 | transform: rotate(360deg); 225 | } 226 | to { 227 | transform: rotate(0deg); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /examples/next-js/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Button, 5 | ChakraProvider, 6 | HStack, 7 | ListItem, 8 | OrderedList, 9 | Spinner, 10 | Stack, 11 | Text, 12 | UnorderedList, 13 | } from "@chakra-ui/react"; 14 | import { createSigner } from "@guildxyz/sdk"; 15 | import { UserProfile } from "@guildxyz/types"; 16 | import { useState } from "react"; 17 | import useSWR from "swr"; 18 | import { useAccount, useConnect, useDisconnect, useSignMessage } from "wagmi"; 19 | import { InjectedConnector } from "wagmi/connectors/injected"; 20 | import { WalletConnectConnector } from "wagmi/connectors/walletConnect"; 21 | import guildClient from "../lib/guild"; 22 | 23 | // Id of Our Guild (https://guild.xyz/our-guild) 24 | // You can check your guild's id with the following endpoint: 25 | // https://api.guild.xyz/v2/guilds/our-guild 26 | const GUILD_ID = 1985; 27 | 28 | function fetchUserMembershipsInGuild(address: `0x${string}`, guildId: number) { 29 | return guildClient.user 30 | .getMemberships(address) 31 | .then((results) => results.find((item) => item.guildId === guildId)); 32 | } 33 | 34 | function fetchRoleNames(guildId: number) { 35 | return guildClient.guild.role 36 | .getAll(guildId) 37 | .then((roles) => 38 | Object.fromEntries(roles.map(({ id, name }) => [id, name])) 39 | ); 40 | } 41 | 42 | async function fetchLeaderboard( 43 | guildIdOrUrlName: number | string, 44 | isAllUser: boolean = false 45 | ) { 46 | const rewards = await guildClient.guild.reward.getAll(guildIdOrUrlName); 47 | 48 | // platformId === 13 means that the reward is point-based 49 | const pointsReward = rewards.find((reward) => reward.platformId === 13); 50 | 51 | // The guildPlatformId parameter could also be hardcoded 52 | // isAllUser means, that the response contains the whole leaderboard, while the value is false, it returns the first 500 user & address 53 | return guildClient.guild.getLeaderboard( 54 | guildIdOrUrlName, 55 | pointsReward!.id, 56 | undefined, 57 | isAllUser 58 | ); 59 | } 60 | 61 | export default function Home() { 62 | const { address } = useAccount(); 63 | 64 | const { connect: connectInjected } = useConnect({ 65 | connector: new InjectedConnector(), 66 | }); 67 | const { connect: connectWalletConnect } = useConnect({ 68 | connector: new WalletConnectConnector({ 69 | options: { projectId: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID }, 70 | }), 71 | }); 72 | const { disconnect } = useDisconnect(); 73 | const { signMessageAsync } = useSignMessage(); 74 | 75 | const [profile, setProfile] = useState(); 76 | 77 | const { data: userMemberships, isLoading: isUserMembershipsLoading } = useSWR( 78 | !!address ? ["memberships", address, GUILD_ID] : null, 79 | ([, ...props]) => fetchUserMembershipsInGuild(...props) 80 | ); 81 | 82 | const { data: roles, isLoading: isRolesLoading } = useSWR( 83 | ["roles", GUILD_ID], 84 | ([, ...props]) => fetchRoleNames(...props) 85 | ); 86 | 87 | const { data: leaderboard, isLoading: isLeaderboardLoading } = useSWR( 88 | ["leaderboard", "walletconnect", false], 89 | ([, ...params]) => fetchLeaderboard(...params) 90 | ); 91 | 92 | return ( 93 | 94 | 95 | {address ? ( 96 | <> 97 | 98 | Connected to {address} 99 | 100 | 101 | 102 | 103 | ) : ( 104 | 105 | 106 | 109 | 110 | )} 111 | 112 | {!!address && ( 113 | <> 114 | Fetch user profile 115 | {!profile ? ( 116 | 131 | ) : ( 132 | {JSON.stringify(profile)} 133 | )} 134 | 135 | )} 136 | 137 | List Memberships 138 | 139 | {isUserMembershipsLoading || isRolesLoading ? ( 140 | 141 | ) : !userMemberships || !roles ? ( 142 | No data 143 | ) : ( 144 | 145 | {userMemberships.roleIds.map((roleId) => ( 146 | 147 | {roles[roleId]} (#{roleId}) 148 | 149 | ))} 150 | 151 | )} 152 | 153 | Listing Point Leaderboard 154 | 155 | {leaderboard?.isRevalidating && ( 156 | 157 | Leaderboard is currently revalidating, current data might be 158 | inconsistent. 159 | 160 | )} 161 | 162 | {isLeaderboardLoading ? ( 163 | 164 | ) : !leaderboard ? ( 165 | No data 166 | ) : ( 167 | 168 | {leaderboard.leaderboard.map(({ userId, address, totalPoints }) => ( 169 | 170 | {address} ({totalPoints} points) 171 | 172 | ))} 173 | 174 | )} 175 | 176 | 177 | ); 178 | } 179 | -------------------------------------------------------------------------------- /examples/next-js/src/lib/guild.ts: -------------------------------------------------------------------------------- 1 | import { createGuildClient } from "@guildxyz/sdk"; 2 | 3 | const client = createGuildClient("next-js-example-app"); 4 | 5 | 6 | export default client 7 | -------------------------------------------------------------------------------- /examples/next-js/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@guildxyz/sdk", 3 | "version": "2.6.8", 4 | "description": "SDK for Guild.xyz Public API ", 5 | "main": "./dist/index.js", 6 | "module": "./dist/index.mjs", 7 | "types": "./dist/index.d.ts", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/guildxyz/guild-sdk.git" 11 | }, 12 | "keywords": [ 13 | "guild", 14 | "guildxyz", 15 | "guildauth", 16 | "agoraxyz", 17 | "agoraspace" 18 | ], 19 | "author": { 20 | "name": "baloo", 21 | "email": "cs-balazs@guild.xyz" 22 | }, 23 | "contributors": [ 24 | { 25 | "name": "ejay", 26 | "email": "ejay@guild.xyz" 27 | }, 28 | { 29 | "name": "Devid", 30 | "email": "devid@guild.xyz" 31 | } 32 | ], 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/guildxyz/guild-sdk/issues" 36 | }, 37 | "homepage": "https://guild.xyz", 38 | "scripts": { 39 | "build": "tsup ./src/index.ts", 40 | "prepublishOnly": "npm run build", 41 | "prepare": "npm run build", 42 | "test": "npx dotenv-cli -e .test.env -- npx vitest run", 43 | "testWithPrivileged": "npx dotenv-cli -e .test.privileged.env -- npm run test" 44 | }, 45 | "dependencies": { 46 | "@guildxyz/types": "^1.9.42", 47 | "ethers": "^6.7.1", 48 | "randombytes": "^2.1.0" 49 | }, 50 | "devDependencies": { 51 | "@guildxyz/queues": "^0.0.8", 52 | "@types/node": "^20.6.3", 53 | "@types/randombytes": "^2.0.2", 54 | "@typescript-eslint/eslint-plugin": "^6.7.2", 55 | "@typescript-eslint/parser": "^6.7.2", 56 | "dotenv-cli": "^7.3.0", 57 | "eslint": "^8.49.0", 58 | "eslint-config-airbnb-base": "^15.0.0", 59 | "eslint-config-prettier": "^9.0.0", 60 | "lint-staged": "^14.0.1", 61 | "prettier": "^3.0.3", 62 | "pretty-quick": "^3.1.3", 63 | "tsup": "^7.2.0", 64 | "typescript": "^5.2.2", 65 | "vitest": "^0.34.5", 66 | "zod": "^3.22.2" 67 | }, 68 | "lint-staged": { 69 | "*.{js,ts}": "eslint --fix --cache --cache-location 'node_modules/.cache/.eslintcache'", 70 | "*.{js,ts,md}": "pretty-quick --staged" 71 | }, 72 | "tsup": { 73 | "format": [ 74 | "esm", 75 | "cjs" 76 | ], 77 | "dts": { 78 | "resolve": true, 79 | "entry": [ 80 | "./src/index.ts" 81 | ], 82 | "compilerOptions": { 83 | "moduleResolution": "node", 84 | "strict": true, 85 | "strictNullChecks": true 86 | } 87 | }, 88 | "clean": true, 89 | "target": "esnext" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/client.ts: -------------------------------------------------------------------------------- 1 | import guild from "./clients/guild"; 2 | import platform from "./clients/platform"; 3 | import user from "./clients/user"; 4 | import { setProjectName } from "./common"; 5 | 6 | const createGuildClient = (projectName: string) => { 7 | if (typeof projectName !== "string" || projectName.length <= 0) 8 | throw Error("Project name should be a non-empty string"); 9 | 10 | setProjectName(projectName); 11 | 12 | return { guild, platform, user }; 13 | }; 14 | 15 | export type GuildClient = ReturnType 16 | 17 | export default createGuildClient; 18 | -------------------------------------------------------------------------------- /src/clients/guild.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GetGuildMembersResponse, 3 | GetLeaderboardResponse, 4 | Guild, 5 | GuildCreationResponse, 6 | Schemas, 7 | } from "@guildxyz/types"; 8 | import { 9 | SignerFunction, 10 | callGuildAPI, 11 | castDateInLeaderboardItem, 12 | } from "../utils"; 13 | import guildAdmin from "./guildAdmin"; 14 | import guildReward from "./guildReward"; 15 | import role from "./role"; 16 | 17 | const guild = { 18 | role, 19 | 20 | reward: guildReward, 21 | 22 | admin: guildAdmin, 23 | 24 | get: (guildIdOrUrlName: number | string) => 25 | callGuildAPI({ url: `/guilds/${guildIdOrUrlName}`, method: "GET" }), 26 | 27 | getMany: (guildIds: number[]) => 28 | callGuildAPI({ 29 | url: `/guilds`, 30 | method: "GET", 31 | queryParams: { 32 | guildIds: guildIds.join(","), 33 | }, 34 | queryParamsSchema: "GuildGetManyQueryParamsSchema", 35 | }), 36 | 37 | search: (params: Schemas["GuildSearchQueryParams"]) => 38 | callGuildAPI({ 39 | url: `/guilds`, 40 | method: "GET", 41 | queryParams: params, 42 | queryParamsSchema: "GuildSearchQueryParamsSchema", 43 | }), 44 | 45 | /** 46 | * If a signer is provided, the response will include an aroundUser field, which holds leaderboard items from around the signer user 47 | */ 48 | getLeaderboard: ( 49 | guildIdOrUrlName: number | string, 50 | guildPlatformId: number, 51 | signer?: SignerFunction, 52 | isAllUser: boolean = false, 53 | forceRecalculate: boolean = false 54 | ) => 55 | callGuildAPI({ 56 | url: `/guilds/${guildIdOrUrlName}/points/${guildPlatformId}/leaderboard?isAllUser=${isAllUser}&forceRecalculate=${forceRecalculate}`, 57 | method: "GET", 58 | signer, 59 | }).then( 60 | (result) => 61 | { 62 | aroundUser: result.aroundUser?.map(castDateInLeaderboardItem), 63 | leaderboard: result.leaderboard.map(castDateInLeaderboardItem), 64 | isRevalidating: !!result.isRevalidating, 65 | } 66 | ), 67 | 68 | getMembers: (guildId: number, signer?: SignerFunction) => 69 | callGuildAPI({ 70 | url: `/guilds/${guildId}/members`, 71 | method: "GET", 72 | signer, 73 | }), 74 | 75 | getUserMemberships: ( 76 | guildId: number, 77 | userId: number, 78 | signer?: SignerFunction 79 | ) => 80 | callGuildAPI>({ 81 | url: `/guilds/${guildId}/members/${userId}`, 82 | method: "GET", 83 | signer, 84 | }), 85 | 86 | create: ( 87 | guildCreationParams: Schemas["GuildCreationPayload"], 88 | signer: SignerFunction 89 | ) => 90 | callGuildAPI({ 91 | url: `/guilds`, 92 | method: "POST", 93 | signer, 94 | body: { 95 | data: guildCreationParams, 96 | schema: "GuildCreationPayloadSchema", 97 | }, 98 | }), 99 | 100 | update: ( 101 | guildId: number, 102 | guildUpdateParams: Schemas["GuildUpdatePayload"], 103 | signer: SignerFunction 104 | ) => 105 | callGuildAPI({ 106 | url: `/guilds/${guildId}`, 107 | method: "PUT", 108 | body: { 109 | data: guildUpdateParams, 110 | schema: "GuildUpdatePayloadSchema", 111 | }, 112 | signer, 113 | }), 114 | 115 | delete: (guildId: number, signer: SignerFunction) => 116 | callGuildAPI({ 117 | url: `/guilds/${guildId}`, 118 | method: "DELETE", 119 | signer, 120 | }), 121 | 122 | join: (guildId: number, signer: SignerFunction) => 123 | callGuildAPI<{ success: boolean; accessedRoleIds: number[] }>({ 124 | url: `/v1/user/join`, 125 | method: "POST", 126 | body: { 127 | schema: "JoinActionPayloadSchema", 128 | data: { 129 | guildId, 130 | }, 131 | }, 132 | signer, 133 | }), 134 | 135 | // join: ( 136 | // guildId: number, 137 | // signer: SignerFunction, 138 | // pollOptions?: PollOptions 139 | // ) => 140 | // createAndAwaitJob( 141 | // "/actions/join", 142 | // { 143 | // schema: "JoinActionPayloadSchema", 144 | // data: { guildId }, 145 | // }, 146 | // { guildId }, 147 | // signer, 148 | // pollOptions 149 | // ), 150 | 151 | accessCheck: (guildId: number, signer: SignerFunction) => 152 | callGuildAPI< 153 | Array<{ 154 | roleId: number; 155 | access: boolean | null; 156 | requirements: Array<{ requirementId: number; access: boolean | null }>; 157 | errors: Array<{ 158 | requirementId: number; 159 | msg: string; 160 | errorType: string; 161 | subType: string; 162 | }>; 163 | }> 164 | >({ 165 | url: `/v1/guild/access/${guildId}/0x0000000000000000000000000000000000000000`, 166 | method: "GET", 167 | signer, 168 | }), 169 | 170 | // accessCheck: ( 171 | // guildId: number, 172 | // signer: SignerFunction, 173 | // pollOptions?: PollOptions 174 | // ) => 175 | // createAndAwaitJob( 176 | // "/actions/access-check", 177 | // { 178 | // schema: "JoinActionPayloadSchema", 179 | // data: { guildId }, 180 | // }, 181 | // { guildId }, 182 | // signer, 183 | // pollOptions 184 | // ), 185 | 186 | // statusUpdate: async ( 187 | // guildId: number, 188 | // signer: SignerFunction, 189 | // pollOptions?: PollOptions 190 | // ) => { 191 | // const roles = await guild.role.getAll(guildId, signer); 192 | // return createAndAwaitJob( 193 | // "/actions/status-update", 194 | // { 195 | // schema: "StatusUpdateActionPayloadSchema", 196 | // data: { 197 | // roleIds: (roles ?? []).map(({ id }) => id), 198 | // }, 199 | // }, 200 | // { guildId }, 201 | // signer, 202 | // pollOptions 203 | // ); 204 | // }, 205 | }; 206 | 207 | export default guild; 208 | -------------------------------------------------------------------------------- /src/clients/guildAdmin.ts: -------------------------------------------------------------------------------- 1 | import { GuildAdmin, Schemas } from "@guildxyz/types"; 2 | import { SignerFunction, callGuildAPI } from "../utils"; 3 | 4 | const guildAdmin = { 5 | get: (guildIdOrUrlName: string | number, userId: number) => 6 | callGuildAPI({ 7 | url: `/guilds/${guildIdOrUrlName}/admins/${userId}`, 8 | method: "GET", 9 | }), 10 | 11 | getAll: (guildIdOrUrlName: string | number) => 12 | callGuildAPI({ 13 | url: `/guilds/${guildIdOrUrlName}/admins`, 14 | method: "GET", 15 | }), 16 | 17 | create: ( 18 | guildIdOrUrlName: string | number, 19 | guildAdminCreationParams: Schemas["GuildAdminCreationPayload"], 20 | signer: SignerFunction 21 | ) => 22 | callGuildAPI({ 23 | url: `/guilds/${guildIdOrUrlName}/admins`, 24 | method: "POST", 25 | body: { 26 | schema: "GuildAdminCreationPayloadSchema", 27 | data: guildAdminCreationParams, 28 | }, 29 | signer, 30 | }), 31 | 32 | update: ( 33 | guildIdOrUrlName: string | number, 34 | adminUserId: number, 35 | guildAdminUpdateParams: Schemas["GuildAdminUpdatePayload"], 36 | signer: SignerFunction 37 | ) => 38 | callGuildAPI({ 39 | url: `/guilds/${guildIdOrUrlName}/admins/${adminUserId}`, 40 | method: "PUT", 41 | body: { 42 | schema: "GuildAdminUpdatePayloadSchema", 43 | data: guildAdminUpdateParams, 44 | }, 45 | signer, 46 | }), 47 | 48 | delete: ( 49 | guildIdOrUrlName: string | number, 50 | adminUserId: number, 51 | signer: SignerFunction 52 | ) => 53 | callGuildAPI({ 54 | url: `/guilds/${guildIdOrUrlName}/admins/${adminUserId}`, 55 | method: "DELETE", 56 | signer, 57 | }), 58 | }; 59 | 60 | export default guildAdmin; 61 | -------------------------------------------------------------------------------- /src/clients/guildReward.ts: -------------------------------------------------------------------------------- 1 | import { GuildReward, Schemas } from "@guildxyz/types"; 2 | import { SignerFunction, callGuildAPI } from "../utils"; 3 | 4 | const guildReward = { 5 | get: ( 6 | guildIdOrUrlName: string | number, 7 | guildPlatformId: number, 8 | signer?: SignerFunction 9 | ) => 10 | callGuildAPI({ 11 | url: `/guilds/${guildIdOrUrlName}/guild-platforms/${guildPlatformId}`, 12 | method: "GET", 13 | signer, 14 | }), 15 | 16 | getAll: (guildIdOrUrlName: string | number, signer?: SignerFunction) => 17 | callGuildAPI({ 18 | url: `/guilds/${guildIdOrUrlName}/guild-platforms`, 19 | method: "GET", 20 | signer, 21 | }), 22 | 23 | create: ( 24 | guildIdOrUrlName: string | number, 25 | guildPlatformCreationParams: Schemas["GuildRewardCreation"], 26 | signer: SignerFunction 27 | ) => 28 | callGuildAPI({ 29 | url: `/guilds/${guildIdOrUrlName}/guild-platforms`, 30 | method: "POST", 31 | body: { 32 | schema: "GuildRewardCreationSchema", 33 | data: guildPlatformCreationParams, 34 | }, 35 | signer, 36 | }), 37 | 38 | update: ( 39 | guildIdOrUrlName: string | number, 40 | guildPlatformId: number, 41 | guildPlatformUpdateParams: Schemas["GuildRewardUpdate"], 42 | signer: SignerFunction 43 | ) => 44 | callGuildAPI({ 45 | url: `/guilds/${guildIdOrUrlName}/guild-platforms/${guildPlatformId}`, 46 | method: "PUT", 47 | body: { 48 | schema: "GuildRewardUpdateSchema", 49 | data: guildPlatformUpdateParams, 50 | }, 51 | signer, 52 | }), 53 | 54 | delete: ( 55 | guildIdOrUrlName: string | number, 56 | guildPlatformId: number, 57 | signer: SignerFunction 58 | ) => 59 | callGuildAPI({ 60 | url: `/guilds/${guildIdOrUrlName}/guild-platforms/${guildPlatformId}`, 61 | method: "DELETE", 62 | signer, 63 | }), 64 | }; 65 | 66 | export default guildReward; 67 | -------------------------------------------------------------------------------- /src/clients/platform.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GuildByPlatformResponse, 3 | PlatformName, 4 | User, 5 | UserGuildAccessesByPlatformResponse, 6 | } from "@guildxyz/types"; 7 | import { SignerFunction, callGuildAPI } from "../utils"; 8 | 9 | const platform = { 10 | getGuildByPlatform: ( 11 | platformName: PlatformName, 12 | platformGuildId: string, 13 | signer?: SignerFunction 14 | ) => 15 | callGuildAPI({ 16 | url: `/platforms/${platformName}/guilds/${platformGuildId}`, 17 | method: "GET", 18 | signer, 19 | }), 20 | 21 | getUserGuildAccessByPlatform: ( 22 | platformName: PlatformName, 23 | platformGuildId: string, 24 | platformUserId: string, 25 | signer?: SignerFunction 26 | ) => 27 | callGuildAPI({ 28 | url: `/platforms/${platformName}/guilds/${platformGuildId}/users/${platformUserId}/access`, 29 | method: "GET", 30 | signer, 31 | }), 32 | 33 | withPlatformName: (platformName: PlatformName) => ({ 34 | getGuildByPlatform: (platformGuildId: string, signer?: SignerFunction) => 35 | platform.getGuildByPlatform(platformName, platformGuildId, signer), 36 | 37 | getUserGuildAccessByPlatform: ( 38 | platformGuildId: string, 39 | platformUserId: string, 40 | signer?: SignerFunction 41 | ) => 42 | platform.getUserGuildAccessByPlatform( 43 | platformName, 44 | platformGuildId, 45 | platformUserId, 46 | signer 47 | ), 48 | 49 | getUserByPlatformUserId: (platformUserId: string, signer: SignerFunction) => 50 | callGuildAPI({ 51 | url: `/platforms/${platformName}/users/${platformUserId}`, 52 | method: "GET", 53 | signer, 54 | }), 55 | }), 56 | }; 57 | 58 | export default platform; 59 | -------------------------------------------------------------------------------- /src/clients/platformUser.ts: -------------------------------------------------------------------------------- 1 | import { PlatformUser, Schemas } from "@guildxyz/types"; 2 | import { SignerFunction, callGuildAPI } from "../utils"; 3 | 4 | const platformUser = { 5 | get: ( 6 | userIdOrAddress: string | number, 7 | platformId: number, 8 | signer: SignerFunction 9 | ) => 10 | callGuildAPI({ 11 | url: `/users/${userIdOrAddress}/platform-users/${platformId}`, 12 | method: "GET", 13 | signer, 14 | }), 15 | 16 | getAll: (userIdOrAddress: string | number, signer: SignerFunction) => 17 | callGuildAPI({ 18 | url: `/users/${userIdOrAddress}/platform-users`, 19 | method: "GET", 20 | signer, 21 | }), 22 | 23 | create: ( 24 | userIdOrAddress: string | number, 25 | platformUserCreationParams: Schemas["PlatformUserCreation"], 26 | signer: SignerFunction 27 | ) => 28 | callGuildAPI({ 29 | url: `/users/${userIdOrAddress}/platform-users`, 30 | method: "POST", 31 | body: { 32 | schema: "PlatformUserCreationSchema", 33 | data: platformUserCreationParams, 34 | }, 35 | signer, 36 | }), 37 | 38 | delete: ( 39 | userIdOrAddress: string | number, 40 | platformId: number, 41 | signer: SignerFunction 42 | ) => 43 | callGuildAPI({ 44 | url: `/users/${userIdOrAddress}/platform-users/${platformId}`, 45 | method: "DELETE", 46 | signer, 47 | }), 48 | }; 49 | 50 | export default platformUser; 51 | -------------------------------------------------------------------------------- /src/clients/requirement.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Requirement, 3 | RequirementUpdatePayload, 4 | Schemas, 5 | } from "@guildxyz/types"; 6 | import { SignerFunction, callGuildAPI } from "../utils"; 7 | 8 | const requirement = { 9 | get: ( 10 | guildIdOrUrlName: string | number, 11 | roleId: number, 12 | requirementId: number, 13 | signer?: SignerFunction 14 | ) => 15 | callGuildAPI({ 16 | url: `/guilds/${guildIdOrUrlName}/roles/${roleId}/requirements/${requirementId}`, 17 | method: "GET", 18 | signer, 19 | }), 20 | 21 | getAll: ( 22 | guildIdOrUrlName: string | number, 23 | roleId: number, 24 | signer?: SignerFunction 25 | ) => 26 | callGuildAPI({ 27 | url: `/guilds/${guildIdOrUrlName}/roles/${roleId}/requirements`, 28 | method: "GET", 29 | signer, 30 | }), 31 | 32 | create: ( 33 | guildIdOrUrlName: string | number, 34 | roleId: number, 35 | requirementCreationParams: Schemas["RequirementCreationPayload"], 36 | signer: SignerFunction 37 | ) => 38 | callGuildAPI({ 39 | url: `/guilds/${guildIdOrUrlName}/roles/${roleId}/requirements`, 40 | method: "POST", 41 | body: { 42 | data: requirementCreationParams, 43 | schema: "RequirementCreationPayloadSchema", 44 | }, 45 | signer, 46 | }), 47 | 48 | update: ( 49 | guildIdOrUrlName: string | number, 50 | roleId: number, 51 | requirementId: number, 52 | requirementUpdateParams: RequirementUpdatePayload, 53 | signer: SignerFunction 54 | ) => 55 | callGuildAPI({ 56 | url: `/guilds/${guildIdOrUrlName}/roles/${roleId}/requirements/${requirementId}`, 57 | method: "PUT", 58 | body: { 59 | data: requirementUpdateParams, 60 | schema: "RequirementUpdatePayloadSchema", 61 | }, 62 | signer, 63 | }), 64 | 65 | delete: ( 66 | guildIdOrUrlName: string | number, 67 | roleId: number, 68 | requirementId: number, 69 | signer: SignerFunction 70 | ) => 71 | callGuildAPI({ 72 | url: `/guilds/${guildIdOrUrlName}/roles/${roleId}/requirements/${requirementId}`, 73 | method: "DELETE", 74 | signer, 75 | }), 76 | }; 77 | 78 | export default requirement; 79 | -------------------------------------------------------------------------------- /src/clients/role.ts: -------------------------------------------------------------------------------- 1 | import { Role, RoleCreationResponse, Schemas } from "@guildxyz/types"; 2 | import { SignerFunction, callGuildAPI } from "../utils"; 3 | import requirement from "./requirement"; 4 | import rolePlatform from "./rolePlatform"; 5 | 6 | const role = { 7 | requirement, 8 | 9 | reward: rolePlatform, 10 | 11 | get: ( 12 | guildIdOrUrlName: number | string, 13 | roleId: number, 14 | signer?: SignerFunction 15 | ) => 16 | callGuildAPI({ 17 | url: `/guilds/${guildIdOrUrlName}/roles/${roleId}`, 18 | method: "GET", 19 | signer, 20 | }), 21 | 22 | getAll: (guildIdOrUrlName: number | string, signer?: SignerFunction) => 23 | callGuildAPI({ 24 | url: `/guilds/${guildIdOrUrlName}/roles`, 25 | method: "GET", 26 | signer, 27 | }), 28 | 29 | create: ( 30 | guildIdOrUrlName: number | string, 31 | roleCreationParams: Schemas["RoleCreationPayload"], 32 | signer: SignerFunction 33 | ) => 34 | callGuildAPI({ 35 | url: `/guilds/${guildIdOrUrlName}/roles`, 36 | method: "POST", 37 | body: { 38 | data: roleCreationParams, 39 | schema: "RoleCreationPayloadSchema", 40 | }, 41 | signer, 42 | }), 43 | 44 | update: ( 45 | guildIdOrUrlName: number | string, 46 | roleId: number, 47 | roleUpdateParams: Schemas["RoleUpdatePayload"], 48 | signer: SignerFunction 49 | ) => 50 | callGuildAPI({ 51 | url: `/guilds/${guildIdOrUrlName}/roles/${roleId}`, 52 | method: "PUT", 53 | body: { 54 | data: roleUpdateParams, 55 | schema: "RoleUpdatePayloadSchema", 56 | }, 57 | signer, 58 | }), 59 | 60 | delete: ( 61 | guildIdOrUrlName: number | string, 62 | roleId: number, 63 | signer: SignerFunction 64 | ) => 65 | callGuildAPI({ 66 | url: `/guilds/${guildIdOrUrlName}/roles/${roleId}`, 67 | method: "DELETE", 68 | signer, 69 | }), 70 | }; 71 | 72 | export default role; 73 | -------------------------------------------------------------------------------- /src/clients/rolePlatform.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RolePlatformClaimResponse, 3 | RoleReward, 4 | Schemas, 5 | } from "@guildxyz/types"; 6 | import { SignerFunction, callGuildAPI } from "../utils"; 7 | 8 | const rolePlatform = { 9 | get: ( 10 | guildIdOrUrlName: string | number, 11 | roleId: number, 12 | rolePlatformId: number, 13 | signer?: SignerFunction 14 | ) => 15 | callGuildAPI({ 16 | url: `/guilds/${guildIdOrUrlName}/roles/${roleId}/role-platforms/${rolePlatformId}`, 17 | method: "GET", 18 | signer, 19 | }), 20 | 21 | getAll: ( 22 | guildIdOrUrlName: string | number, 23 | roleId: number, 24 | signer?: SignerFunction 25 | ) => 26 | callGuildAPI({ 27 | url: `/guilds/${guildIdOrUrlName}/roles/${roleId}/role-platforms`, 28 | method: "GET", 29 | signer, 30 | }), 31 | 32 | claim: ( 33 | guildIdOrUrlName: string | number, 34 | roleId: number, 35 | rolePlatformId: number, 36 | args: string[], 37 | signer: SignerFunction 38 | ) => 39 | callGuildAPI({ 40 | url: `/guilds/${guildIdOrUrlName}/roles/${roleId}/role-platforms/${rolePlatformId}/claim`, 41 | method: "POST", 42 | body: { 43 | data: { args }, 44 | schema: "RolePlatformClaimPayloadSchema", 45 | }, 46 | signer, 47 | }), 48 | 49 | create: ( 50 | guildIdOrUrlName: string | number, 51 | roleId: number, 52 | rolePlatformCreationParams: Schemas["RoleRewardCreationPayload"], 53 | signer: SignerFunction 54 | ) => 55 | callGuildAPI({ 56 | url: `/guilds/${guildIdOrUrlName}/roles/${roleId}/role-platforms`, 57 | method: "POST", 58 | body: { 59 | data: rolePlatformCreationParams, 60 | schema: "RoleRewardCreationPayloadSchema", 61 | }, 62 | signer, 63 | }), 64 | 65 | update: ( 66 | guildIdOrUrlName: string | number, 67 | roleId: number, 68 | rolePlatformId: number, 69 | rolePlatformUpdateParams: Schemas["RoleRewardUpdatePayload"], 70 | signer: SignerFunction 71 | ) => 72 | callGuildAPI({ 73 | url: `/guilds/${guildIdOrUrlName}/roles/${roleId}/role-platforms/${rolePlatformId}`, 74 | method: "PUT", 75 | body: { 76 | data: rolePlatformUpdateParams, 77 | schema: "RoleRewardUpdatePayloadSchema", 78 | }, 79 | signer, 80 | }), 81 | 82 | delete: ( 83 | guildIdOrUrlName: string | number, 84 | roleId: number, 85 | rolePlatformId: number, 86 | signer: SignerFunction 87 | ) => 88 | callGuildAPI({ 89 | url: `/guilds/${guildIdOrUrlName}/roles/${roleId}/role-platforms/${rolePlatformId}`, 90 | method: "DELETE", 91 | signer, 92 | }), 93 | }; 94 | 95 | export default rolePlatform; 96 | -------------------------------------------------------------------------------- /src/clients/user.ts: -------------------------------------------------------------------------------- 1 | import { 2 | LeaderboardItem, 3 | MembershipResult, 4 | PlatformName, 5 | PublicUserProfile, 6 | User, 7 | UserPointsResponse, 8 | UserProfile, 9 | } from "@guildxyz/types"; 10 | import { 11 | SignerFunction, 12 | callGuildAPI, 13 | castDateInLeaderboardItem, 14 | } from "../utils"; 15 | import platformUser from "./platformUser"; 16 | import userAddress from "./userAddress"; 17 | 18 | const user = { 19 | platform: platformUser, 20 | 21 | address: userAddress, 22 | 23 | get: (userIdOrAddress: string | number) => 24 | callGuildAPI({ 25 | url: `/users/${userIdOrAddress}`, 26 | method: "GET", 27 | }), 28 | 29 | getPoints: (userIdOrAddress: number | string, signer: SignerFunction) => 30 | callGuildAPI({ 31 | url: `/users/${userIdOrAddress}/points`, 32 | method: "GET", 33 | signer, 34 | }), 35 | 36 | getRankInGuild: ( 37 | userIdOrAddress: number | string, 38 | guildIdOrUrlName: number | string, 39 | guildPlatformId: number 40 | ) => 41 | callGuildAPI({ 42 | url: `/guilds/${guildIdOrUrlName}/points/${guildPlatformId}/users/${userIdOrAddress}`, 43 | method: "GET", 44 | }).then(castDateInLeaderboardItem), 45 | 46 | getProfile: ( 47 | userIdOrAddress: string | number, 48 | signer?: Sig 49 | ) => 50 | callGuildAPI({ 51 | url: `/users/${userIdOrAddress}/profile`, 52 | method: "GET", 53 | signer: typeof signer === "string" ? undefined : signer, 54 | }), 55 | 56 | getMemberships: (userIdOrAddress: string | number, signer?: SignerFunction) => 57 | callGuildAPI({ 58 | url: `/users/${userIdOrAddress}/memberships`, 59 | method: "GET", 60 | signer, 61 | }), 62 | 63 | delete: (userIdOrAddress: string | number, signer: SignerFunction) => 64 | callGuildAPI({ 65 | url: `/users/${userIdOrAddress}`, 66 | method: "DELETE", 67 | signer, 68 | }), 69 | 70 | listGateables: ( 71 | userIdOrAddress: string | number, 72 | platformName: PlatformName, 73 | signer: SignerFunction 74 | ) => 75 | callGuildAPI({ 76 | url: `/users/${userIdOrAddress}/platforms/${platformName}/gateables`, 77 | method: "GET", 78 | signer, 79 | }), 80 | }; 81 | 82 | export default user; 83 | -------------------------------------------------------------------------------- /src/clients/userAddress.ts: -------------------------------------------------------------------------------- 1 | import { Schemas, UserAddress } from "@guildxyz/types"; 2 | import { SignerFunction, callGuildAPI } from "../utils"; 3 | 4 | const userAddress = { 5 | get: ( 6 | userIdOrAddress: string | number, 7 | address: string, 8 | signer: SignerFunction 9 | ) => 10 | callGuildAPI({ 11 | url: `/users/${userIdOrAddress}/addresses/${address}`, 12 | method: "GET", 13 | signer, 14 | }), 15 | 16 | getAll: (userIdOrAddress: string | number, signer: SignerFunction) => 17 | callGuildAPI({ 18 | url: `/users/${userIdOrAddress}/addresses`, 19 | method: "GET", 20 | signer, 21 | }), 22 | 23 | create: async ( 24 | userIdOrAddress: string | number, 25 | signerOfNewAddress: SignerFunction, 26 | signer: SignerFunction 27 | ) => { 28 | const { params, sig } = await signerOfNewAddress( 29 | {}, 30 | (p) => `Address: ${p.addr}\nNonce: ${p.nonce}\n Timestamp: ${p.ts}` 31 | ); 32 | 33 | return callGuildAPI({ 34 | url: `/users/${userIdOrAddress}/addresses`, 35 | method: "POST", 36 | body: { 37 | schema: "UserAddressCreationPayloadSchema", 38 | data: { 39 | address: params.addr, 40 | signature: sig, 41 | nonce: params.nonce, 42 | timestamp: +params.ts, 43 | }, 44 | }, 45 | signer, 46 | }); 47 | }, 48 | 49 | update: ( 50 | userIdOrAddress: string | number, 51 | address: string, 52 | addressUpdateParams: Schemas["UserAddressUpdatePayload"], 53 | signer: SignerFunction 54 | ) => 55 | callGuildAPI({ 56 | url: `/users/${userIdOrAddress}/addresses/${address}`, 57 | method: "PUT", 58 | body: { 59 | schema: "UserAddressUpdatePayloadSchema", 60 | data: addressUpdateParams, 61 | }, 62 | signer, 63 | }), 64 | 65 | delete: ( 66 | userIdOrAddress: string | number, 67 | address: string, 68 | signer: SignerFunction 69 | ) => 70 | callGuildAPI({ 71 | url: `/users/${userIdOrAddress}/addresses/${address}`, 72 | method: "DELETE", 73 | signer, 74 | }), 75 | }; 76 | 77 | export default userAddress; 78 | -------------------------------------------------------------------------------- /src/common.ts: -------------------------------------------------------------------------------- 1 | import { consts } from "@guildxyz/types"; 2 | 3 | const globals = { 4 | apiBaseUrl: process.env.GUILD_SDK_BASE_URL ?? "https://api.guild.xyz/v2", 5 | headers: { 6 | "Content-Type": "application/json", 7 | [consts.SDK_VERSION_HEADER_NAME]: "2.0.0-rc.2", 8 | [consts.SDK_PROJECT_NAME_HEADER_NAME]: "", 9 | }, 10 | }; 11 | 12 | const setProjectName = (projectName: string) => { 13 | globals.headers[consts.SDK_PROJECT_NAME_HEADER_NAME] = projectName; 14 | }; 15 | 16 | export { globals, setProjectName }; 17 | -------------------------------------------------------------------------------- /src/error.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-classes-per-file */ 2 | import type { ZodError } from "zod"; 3 | 4 | class GuildAPICallFailed extends Error { 5 | endpoint: string; 6 | 7 | message: string; 8 | 9 | correlationId: string; 10 | 11 | statusCode: number; 12 | 13 | constructor( 14 | endpoint: string, 15 | message: string, 16 | statusCode: number, 17 | correlationId: string 18 | ) { 19 | super( 20 | `Guild API call failed on ${endpoint} with http code ${statusCode} with message: ${ 21 | message ?? "Unexpected Error" 22 | }\nID: ${correlationId}` 23 | ); 24 | this.endpoint = endpoint; 25 | this.message = message; 26 | this.statusCode = statusCode; 27 | this.correlationId = correlationId; 28 | } 29 | } 30 | 31 | /** 32 | * This error represents a validation on a guild request body. When this 33 | * error is thrown, no request was sent. Instances of this error contin a 34 | * `zodError` field, which contains information on why the supplied data 35 | * didn't pass validation 36 | */ 37 | class GuildSDKValidationError> extends Error { 38 | zodError: Err; 39 | 40 | constructor(zodError: Err) { 41 | super(`A value passed as request body did not pass validation.`); 42 | this.zodError = zodError; 43 | } 44 | } 45 | 46 | export { GuildAPICallFailed, GuildSDKValidationError }; 47 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as createGuildClient, type GuildClient } from "./client"; 2 | export { GuildAPICallFailed, GuildSDKValidationError } from "./error"; 3 | export { createSigner } from "./utils"; 4 | 5 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | import { consts, LeaderboardItem, schemas, Schemas } from "@guildxyz/types"; 3 | import { keccak256, type Wallet } from "ethers"; 4 | import randomBytes from "randombytes"; 5 | import type { z } from "zod"; 6 | import { globals } from "./common"; 7 | import { GuildAPICallFailed, GuildSDKValidationError } from "./error"; 8 | 9 | export const recreateMessage = (params: Schemas["Authentication"]["params"]) => 10 | `${params.msg}\n\nAddress: ${params.addr}\nMethod: ${params.method}${ 11 | params.method === consts.AuthMethod.EIP1271 12 | ? `\nChainId: ${params.chainId}` 13 | : "" 14 | }${params.hash ? `\nHash: ${params.hash}` : ""}\nNonce: ${ 15 | params.nonce 16 | }\nTimestamp: ${params.ts}`; 17 | 18 | export type SignerFunction = ( 19 | // eslint-disable-next-line no-unused-vars 20 | payload?: any, 21 | // eslint-disable-next-line no-unused-vars 22 | getMessage?: (params: Schemas["Authentication"]["params"]) => string 23 | ) => Promise<{ 24 | params: Schemas["Authentication"]["params"]; 25 | sig: string; 26 | payload: string; 27 | }>; 28 | 29 | type SignerOptions = { 30 | msg?: string; 31 | }; 32 | 33 | export const createSigner = { 34 | fromEthersWallet: 35 | ( 36 | wallet: Wallet, 37 | { msg = "Please sign this message" }: SignerOptions = {} 38 | ): SignerFunction => 39 | async (payload = {}, getMessage = recreateMessage) => { 40 | const stringPayload = JSON.stringify(payload); 41 | 42 | const params = schemas.AuthenticationParamsSchema.parse({ 43 | method: consts.AuthMethod.EOA, 44 | addr: wallet.address, 45 | msg, 46 | nonce: randomBytes(32).toString("base64"), 47 | ts: `${Date.now()}`, 48 | hash: keccak256(Buffer.from(stringPayload)), 49 | }); 50 | 51 | if (params.method !== consts.AuthMethod.EOA) { 52 | throw new Error("This shouldn't happen, please open an issue"); 53 | } 54 | 55 | const sig = await wallet.signMessage(Buffer.from(getMessage(params))); 56 | 57 | return { params, sig, payload: stringPayload }; 58 | }, 59 | 60 | custom: 61 | ( 62 | // eslint-disable-next-line no-unused-vars 63 | sign: (message: string) => Promise, 64 | address: string, 65 | { 66 | msg = "Please sign this message", 67 | chainIdOfSmartContractWallet, 68 | }: { chainIdOfSmartContractWallet?: number } & SignerOptions = {} 69 | ): SignerFunction => 70 | async (payload = {}, getMessage = recreateMessage) => { 71 | const stringPayload = JSON.stringify(payload); 72 | 73 | const params = schemas.AuthenticationParamsSchema.parse({ 74 | addr: address, 75 | msg, 76 | nonce: randomBytes(32).toString("base64"), 77 | ts: `${Date.now()}`, 78 | hash: keccak256(Buffer.from(stringPayload)), 79 | ...(typeof chainIdOfSmartContractWallet === "number" 80 | ? { 81 | chainId: chainIdOfSmartContractWallet.toString(), 82 | method: consts.AuthMethod.EIP1271, 83 | } 84 | : { method: consts.AuthMethod.EOA }), 85 | }); 86 | 87 | const sig = await sign(getMessage(params)); 88 | 89 | return { params, sig, payload: stringPayload }; 90 | }, 91 | }; 92 | 93 | type SchemasImportType = (typeof import("@guildxyz/types"))["schemas"]; 94 | type SchemaNames = keyof SchemasImportType; 95 | type MappedSchemas = { 96 | [Schema in SchemaNames]: { 97 | schema: Schema; 98 | data: z.input; 99 | }; 100 | }[SchemaNames]; 101 | 102 | type CallGuildAPIParams = { 103 | url: string; 104 | queryParams?: Record; 105 | queryParamsSchema?: MappedSchemas["schema"]; 106 | signer?: SignerFunction; 107 | } & ( 108 | | { 109 | method: "GET"; 110 | } 111 | | { 112 | method: "POST" | "PUT" | "DELETE" | "PATCH"; 113 | body?: MappedSchemas; 114 | } 115 | ); 116 | 117 | // Couldn't get proper type inference if the type params are on the same function 118 | export const callGuildAPI = async ( 119 | params: CallGuildAPIParams 120 | ): Promise => { 121 | let parsedQueryParams = null; 122 | if (params.queryParams) { 123 | if (params.queryParamsSchema) { 124 | const validationResult = schemas[params.queryParamsSchema].safeParse( 125 | params.queryParams 126 | ); 127 | if (validationResult.success) { 128 | parsedQueryParams = params.queryParams; 129 | } else { 130 | throw new GuildSDKValidationError(validationResult.error); 131 | } 132 | } else { 133 | parsedQueryParams = params.queryParams; 134 | } 135 | } 136 | 137 | const queryParamEntries = Object.entries(parsedQueryParams ?? {}) 138 | .filter(([, value]) => !!value) 139 | .map(([key, value]) => [key, `${value}`]); 140 | 141 | const baseUrl = params.url.startsWith("/v1/") 142 | ? globals.apiBaseUrl.replace("/v2", "") 143 | : globals.apiBaseUrl; 144 | 145 | const url = 146 | queryParamEntries.length > 0 147 | ? `${baseUrl}${params.url}?${new URLSearchParams(queryParamEntries)}` 148 | : `${baseUrl}${params.url}`; 149 | 150 | let parsedPayload: any = {}; 151 | if (params.method !== "GET" && !!params.body?.schema && !!params.body?.data) { 152 | const validationResult = schemas[params.body.schema].safeParse( 153 | params.body.data 154 | ); 155 | if (validationResult.success) { 156 | parsedPayload = validationResult.data; 157 | } else { 158 | throw new GuildSDKValidationError(validationResult.error); 159 | } 160 | } 161 | 162 | const authentication = await params.signer?.(parsedPayload); 163 | 164 | const isPrivileged = "headers" in (authentication ?? {}); 165 | 166 | const response = await fetch(url, { 167 | method: params.method, 168 | body: 169 | // eslint-disable-next-line no-nested-ternary 170 | params.method === "GET" 171 | ? undefined 172 | : isPrivileged 173 | ? JSON.stringify(parsedPayload) 174 | : JSON.stringify(authentication ?? parsedPayload), 175 | headers: { 176 | ...(params.method === "GET" && authentication && !isPrivileged 177 | ? { 178 | [consts.PARAMS_HEADER_NAME]: Buffer.from( 179 | JSON.stringify(authentication.params) 180 | ).toString("base64"), 181 | [consts.SIG_HEADER_NAME]: Buffer.from( 182 | authentication.sig.startsWith("0x") 183 | ? authentication.sig.slice(2) 184 | : authentication.sig, 185 | "hex" 186 | ).toString("base64"), 187 | } 188 | : {}), 189 | ...globals.headers, 190 | ...(isPrivileged ? (authentication as any).headers : {}), 191 | }, 192 | }); 193 | 194 | const responseBody = await response.json(); 195 | 196 | if (!response.ok) { 197 | throw new GuildAPICallFailed( 198 | url.replace(baseUrl, ""), 199 | responseBody?.errors?.[0]?.msg ?? 200 | responseBody?.message ?? 201 | "Unexpected Error", 202 | response.status, 203 | response.headers.get("x-correlation-id") ?? "" 204 | ); 205 | } 206 | 207 | return responseBody; 208 | }; 209 | 210 | export type OnPoll = ( 211 | job: Job | null, 212 | promiseHandlers: { 213 | resolve: (value: Job | PromiseLike | null) => void; 214 | reject: (reason?: any) => void; 215 | } 216 | ) => void; 217 | 218 | export type PollOptions = { onPoll?: OnPoll; intervalMs?: number }; 219 | 220 | export const createAndAwaitJob = async < 221 | Job extends { done?: boolean; error?: any; errorMsg?: any }, 222 | >( 223 | url: string, 224 | body: MappedSchemas, 225 | queryParams: Record, 226 | signer: SignerFunction, 227 | { onPoll, intervalMs = 1000 }: PollOptions = {} 228 | ) => { 229 | await callGuildAPI<{ jobId: string }>({ 230 | url, 231 | method: "POST", 232 | body, 233 | signer, 234 | }); 235 | 236 | let interval: ReturnType; 237 | 238 | return new Promise((resolve, reject) => { 239 | interval = setInterval(() => { 240 | callGuildAPI({ 241 | url, 242 | method: "GET", 243 | queryParams, 244 | signer, 245 | }).then(([job = null]) => { 246 | onPoll?.(job, { resolve, reject }); 247 | 248 | if (!job) { 249 | reject(job); 250 | return; // Return is needed, so TS knows, that after this point job is not null 251 | } 252 | if (!job.done) return; 253 | 254 | if (job.error ?? job.errorMsg) reject(job); 255 | else resolve(job); 256 | }); 257 | }, intervalMs); 258 | }).finally(() => { 259 | clearInterval(interval); 260 | }); 261 | }; 262 | 263 | export function castDateInLeaderboardItem(leaderboardItem: LeaderboardItem) { 264 | return { 265 | ...leaderboardItem, 266 | oldestRoleDate: new Date(leaderboardItem.oldestRoleDate), 267 | }; 268 | } 269 | -------------------------------------------------------------------------------- /tests/auth.test.ts: -------------------------------------------------------------------------------- 1 | import { schemas } from "@guildxyz/types"; 2 | import { randomBytes } from "crypto"; 3 | import { Wallet, keccak256, toUtf8Bytes, verifyMessage } from "ethers"; 4 | import { assert, describe, expect, test } from "vitest"; 5 | import { createSigner, recreateMessage } from "../src/utils"; 6 | 7 | const WALLET = new Wallet(randomBytes(32).toString("hex")); 8 | const TEST_PAYLOAD = { someKey: "someValue" }; 9 | const TEST_MSG = "Some test message"; 10 | 11 | describe.concurrent("Authentication", () => { 12 | describe.concurrent("EOA Wallet", () => { 13 | test("Can sign simple message", async () => { 14 | const signer = createSigner.fromEthersWallet(WALLET); 15 | const { params, sig, payload } = await signer(); 16 | 17 | expect(() => 18 | schemas.AuthenticationParamsSchema.parse(params) 19 | ).not.toThrow(); 20 | expect(payload).toEqual("{}"); 21 | assert(sig.startsWith("0x")); 22 | expect(sig).toHaveLength(132); 23 | expect(verifyMessage(recreateMessage(params), sig)).toEqual( 24 | WALLET.address 25 | ); 26 | }); 27 | 28 | test("Can sign message with some payload", async () => { 29 | const signer = createSigner.fromEthersWallet(WALLET); 30 | const { params, sig, payload } = await signer(TEST_PAYLOAD); 31 | 32 | expect(() => 33 | schemas.AuthenticationParamsSchema.parse(params) 34 | ).not.toThrow(); 35 | expect(params).toMatchObject({ 36 | hash: keccak256(toUtf8Bytes(JSON.stringify(TEST_PAYLOAD))), 37 | }); 38 | expect(payload).toEqual(JSON.stringify(TEST_PAYLOAD)); 39 | assert(sig.startsWith("0x")); 40 | expect(sig).toHaveLength(132); 41 | expect(verifyMessage(recreateMessage(params), sig)).toEqual( 42 | WALLET.address 43 | ); 44 | }); 45 | 46 | test("Can sign message with some payload", async () => { 47 | const signer = createSigner.fromEthersWallet(WALLET, { msg: TEST_MSG }); 48 | const { params, sig, payload } = await signer(); 49 | 50 | expect(() => 51 | schemas.AuthenticationParamsSchema.parse(params) 52 | ).not.toThrow(); 53 | expect(params).toMatchObject({ msg: TEST_MSG }); 54 | expect(payload).toEqual("{}"); 55 | assert(sig.startsWith("0x")); 56 | expect(sig).toHaveLength(132); 57 | expect(verifyMessage(recreateMessage(params), sig)).toEqual( 58 | WALLET.address 59 | ); 60 | }); 61 | 62 | test("Can sign with given hex privateKey", async () => { 63 | const privateKeyHex = `0x${randomBytes(32).toString("hex")}`; 64 | const ethersWallet = new Wallet(privateKeyHex); 65 | const expectedAddr = ethersWallet.address; 66 | 67 | const signer = createSigner.fromEthersWallet(ethersWallet); 68 | const { params, sig, payload } = await signer(); 69 | 70 | expect(() => 71 | schemas.AuthenticationParamsSchema.parse(params) 72 | ).not.toThrow(); 73 | expect(params.addr).toEqual(expectedAddr.toLowerCase()); 74 | expect(payload).toEqual("{}"); 75 | assert(sig.startsWith("0x")); 76 | expect(sig).toHaveLength(132); 77 | expect(verifyMessage(recreateMessage(params), sig)).toEqual(expectedAddr); 78 | }); 79 | 80 | test("Can sign with given buffer privateKey", async () => { 81 | const privateKeyBuffer = randomBytes(32); 82 | const ethersWallet = new Wallet(`0x${privateKeyBuffer.toString("hex")}`); 83 | const expectedAddr = ethersWallet.address; 84 | 85 | const signer = createSigner.fromEthersWallet(ethersWallet); 86 | const { params, sig, payload } = await signer(); 87 | 88 | expect(() => 89 | schemas.AuthenticationParamsSchema.parse(params) 90 | ).not.toThrow(); 91 | expect(params.addr).toEqual(expectedAddr.toLowerCase()); 92 | expect(payload).toEqual("{}"); 93 | assert(sig.startsWith("0x")); 94 | expect(sig).toHaveLength(132); 95 | expect(verifyMessage(recreateMessage(params), sig)).toEqual(expectedAddr); 96 | }); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /tests/callGuildAPI.test.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from "crypto"; 2 | import { Wallet } from "ethers"; 3 | import { describe, expect, test } from "vitest"; 4 | import { GuildAPICallFailed, GuildSDKValidationError } from "../src/error"; 5 | import { callGuildAPI, createSigner } from "../src/utils"; 6 | 7 | const WALLET = new Wallet(randomBytes(32).toString("hex")); 8 | 9 | describe.concurrent("callGuildAPI", () => { 10 | test("It fails with validation error correctly", async () => { 11 | const signer = createSigner.fromEthersWallet(WALLET); 12 | 13 | try { 14 | await callGuildAPI({ 15 | url: `/guilds/0/roles/0/requirements`, 16 | method: "POST", 17 | body: { 18 | schema: "RequirementCreationPayloadSchema", 19 | data: {} as any, // Missing required "type" field 20 | }, 21 | signer, 22 | }); 23 | } catch (error) { 24 | expect(error).toBeInstanceOf(GuildSDKValidationError); 25 | expect(error.zodError).toBeTruthy(); 26 | expect(error.zodError?.issues).toBeTruthy(); // Not checking `instanceof ZodError`, as zod is only a dev dependency, so we can't access the class 27 | } 28 | }); 29 | 30 | test("It handles API errors (guild doesn't exist)", async () => { 31 | const signer = createSigner.fromEthersWallet(WALLET); 32 | 33 | try { 34 | await callGuildAPI({ 35 | url: `/guilds/0/roles/0/requirements`, 36 | method: "POST", 37 | body: { 38 | schema: "RequirementCreationPayloadSchema", 39 | data: { type: "ALLOWLIST", data: { addresses: [] } }, 40 | }, 41 | signer, 42 | }); 43 | } catch (error) { 44 | expect(error).toBeInstanceOf(GuildAPICallFailed); 45 | expect(error.endpoint).toEqual(`/guilds/0/roles/0/requirements`); 46 | expect(error.statusCode).toEqual(404); 47 | expect(error.message).toEqual("Guild not found"); 48 | } 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /tests/clients/actions/accessCheck.test.ts: -------------------------------------------------------------------------------- 1 | import { Wallet } from "ethers"; 2 | import { describe, expect, it } from "vitest"; 3 | import { createGuildClient, createSigner } from "../../../src"; 4 | 5 | const GUILD_ID = 1984; 6 | const TEST_WALLET_SIGNER = createSigner.fromEthersWallet( 7 | new Wallet(process.env.PRIVATE_KEY!) 8 | ); 9 | 10 | const { guild } = createGuildClient("vitest"); 11 | 12 | describe.skip("Access check action", () => { 13 | it("can check access", async () => { 14 | // const onPoll = vi.fn(); 15 | 16 | const result = await guild.accessCheck( 17 | GUILD_ID, 18 | TEST_WALLET_SIGNER 19 | // { 20 | // onPoll, 21 | // } 22 | ); 23 | 24 | // expect(result!.done).toBeTruthy(); 25 | expect(result.length).toBeGreaterThan(0); 26 | // expect(onPoll).toHaveBeenCalled(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/clients/actions/join.test.ts: -------------------------------------------------------------------------------- 1 | import { Wallet } from "ethers"; 2 | import { describe, expectTypeOf, it } from "vitest"; 3 | import { createGuildClient, createSigner } from "../../../src"; 4 | 5 | const GUILD_ID = 4486; 6 | const TEST_WALLET_SIGNER = createSigner.fromEthersWallet( 7 | new Wallet(process.env.PRIVATE_KEY!) 8 | ); 9 | 10 | const { guild } = createGuildClient("vitest"); 11 | 12 | describe.skip("Join action", () => { 13 | it("can join", async () => { 14 | // const onPoll = vi.fn(); 15 | const result = await guild.join( 16 | GUILD_ID, 17 | TEST_WALLET_SIGNER 18 | // { onPoll } 19 | ); 20 | 21 | expectTypeOf(result.success).toBeBoolean(); 22 | // expect(result!.done).toBeTruthy(); 23 | // expect(onPoll).toHaveBeenCalled(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/clients/guild.test.ts: -------------------------------------------------------------------------------- 1 | import { assert, describe, expect, it } from "vitest"; 2 | import { GuildSDKValidationError } from "../../src/error"; 3 | import { CLIENT, TEST_SIGNER, TEST_USER } from "../common"; 4 | import { createTestGuild, pick } from "../utils"; 5 | 6 | const createdGuild = await createTestGuild(); 7 | const createdGuild2 = await createTestGuild(); 8 | 9 | describe("Guild client", () => { 10 | it("Can get a guild by id", async () => { 11 | const response = await CLIENT.guild.get(createdGuild.id); 12 | 13 | expect(response).toMatchObject(pick(createdGuild, ["id", "urlName"])); 14 | }); 15 | 16 | it("Can get multiple guilds by ids", async () => { 17 | const response = await CLIENT.guild.getMany([ 18 | createdGuild.id, 19 | createdGuild2.id, 20 | ]); 21 | 22 | expect(response).toMatchObject([ 23 | pick(createdGuild, ["id", "urlName"]), 24 | pick(createdGuild2, ["id", "urlName"]), 25 | ]); 26 | }); 27 | 28 | it("Can get guild members", async () => { 29 | const response = await CLIENT.guild.getMembers(createdGuild.id); 30 | 31 | expect(response).toHaveLength(1); 32 | }); 33 | 34 | it("Can get user membership for guild", async () => { 35 | const response = await CLIENT.guild.getUserMemberships( 36 | createdGuild.id, 37 | TEST_USER.id 38 | ); 39 | 40 | expect(response).toHaveLength(1); 41 | }); 42 | 43 | it("Can update guild", async () => { 44 | try { 45 | await CLIENT.guild.update(createdGuild.id, {}, TEST_SIGNER); 46 | assert(false); 47 | } catch (error) { 48 | expect(error).toBeInstanceOf(GuildSDKValidationError); 49 | } 50 | 51 | const updated = await CLIENT.guild.update( 52 | createdGuild.id, 53 | { description: "EDITED" }, 54 | TEST_SIGNER 55 | ); 56 | 57 | expect(updated.description).toMatchObject("EDITED"); 58 | }); 59 | 60 | it("Subsequent GET returns updated data", async () => { 61 | const fetchedGuild = await CLIENT.guild.get(createdGuild.id); 62 | expect(fetchedGuild.description).toEqual("EDITED"); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /tests/clients/guildAdmin.test.ts: -------------------------------------------------------------------------------- 1 | import { Wallet } from "ethers"; 2 | import { describe, expect, it } from "vitest"; 3 | import { GuildAPICallFailed } from "../../src"; 4 | import { CLIENT, TEST_SIGNER, TEST_USER } from "../common"; 5 | import { createTestGuild } from "../utils"; 6 | 7 | const adminAddress = Wallet.createRandom().address.toLowerCase(); 8 | 9 | const guild = await createTestGuild(); 10 | 11 | describe("Guild admins", () => { 12 | it("get all", async () => { 13 | const admins = await CLIENT.guild.admin.getAll(guild.id); 14 | expect(admins.length).toBeGreaterThan(0); 15 | }); 16 | 17 | it("get", async () => { 18 | const admin = await CLIENT.guild.admin.get(guild.id, TEST_USER.id); 19 | expect(admin).toMatchObject({ userId: TEST_USER.id }); 20 | }); 21 | 22 | let adminUserId: number; 23 | 24 | it("create", async () => { 25 | const newAdmin = await CLIENT.guild.admin.create( 26 | guild.id, 27 | { 28 | address: adminAddress, 29 | isOwner: false, 30 | }, 31 | TEST_SIGNER 32 | ); 33 | 34 | adminUserId = newAdmin.userId; 35 | 36 | expect(newAdmin).toMatchObject({ isOwner: false }); 37 | }); 38 | 39 | it("get created admin", async () => { 40 | const admin = await CLIENT.guild.admin.get(guild.id, adminUserId); 41 | expect(admin).toMatchObject({ userId: adminUserId, isOwner: false }); 42 | }); 43 | 44 | it("delete", async () => { 45 | await CLIENT.guild.admin.delete( 46 | guild.id, 47 | adminUserId, 48 | 49 | TEST_SIGNER 50 | ); 51 | }); 52 | 53 | it("can't get created admin", async () => { 54 | try { 55 | await CLIENT.guild.admin.get(guild.id, adminUserId); 56 | } catch (error) { 57 | expect(error).toBeInstanceOf(GuildAPICallFailed); 58 | expect(error.statusCode).toEqual(404); 59 | } 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /tests/clients/guildPlatform.test.ts: -------------------------------------------------------------------------------- 1 | import { GuildReward } from "@guildxyz/types"; 2 | import { describe } from "node:test"; 3 | import { assert, expect, it } from "vitest"; 4 | import { GuildAPICallFailed } from "../../src/error"; 5 | import { CLIENT, TEST_SIGNER } from "../common"; 6 | import { createTestGuild, omit } from "../utils"; 7 | 8 | const guildPlatformToCreate = { 9 | platformGuildId: "my-point-system", 10 | platformName: "POINTS", 11 | platformGuildData: { name: "xp" }, 12 | } as const; 13 | 14 | const guildPlatformUpdate = { 15 | platformGuildData: { name: "coins" }, 16 | } as const; 17 | 18 | let createdGuildPlatform: GuildReward; 19 | 20 | const guild = await createTestGuild(); 21 | 22 | describe("guildPlatform client", () => { 23 | it("Can create guildPlatform", async () => { 24 | createdGuildPlatform = await CLIENT.guild.reward.create( 25 | guild.id, 26 | guildPlatformToCreate, 27 | TEST_SIGNER 28 | ); 29 | 30 | expect(createdGuildPlatform).toMatchObject( 31 | omit(guildPlatformToCreate, ["platformName"]) 32 | ); 33 | }); 34 | 35 | it("Can update guildPlatform", async () => { 36 | const updated = await CLIENT.guild.reward.update( 37 | guild.id, 38 | createdGuildPlatform.id, 39 | guildPlatformUpdate, 40 | TEST_SIGNER 41 | ); 42 | 43 | expect(updated).toMatchObject({ 44 | ...omit(guildPlatformToCreate, ["platformName"]), 45 | ...guildPlatformUpdate, 46 | }); 47 | }); 48 | 49 | it("Can fetch updated guildPlatform", async () => { 50 | const fetched = await CLIENT.guild.reward.get( 51 | guild.id, 52 | createdGuildPlatform.id, 53 | TEST_SIGNER 54 | ); 55 | 56 | expect(fetched).toMatchObject({ 57 | ...omit(guildPlatformToCreate, ["platformName"]), 58 | ...guildPlatformUpdate, 59 | }); 60 | }); 61 | 62 | it("Can fetch updated guildPlatform by guildId", async () => { 63 | const fetched = await CLIENT.guild.reward.getAll(guild.id, TEST_SIGNER); 64 | 65 | expect( 66 | fetched.some( 67 | ({ platformGuildData }) => 68 | platformGuildData.name === guildPlatformUpdate.platformGuildData.name 69 | ) 70 | ).toBeTruthy(); 71 | }); 72 | 73 | it("Can delete guildPlatform", async () => { 74 | await CLIENT.guild.reward.delete( 75 | guild.id, 76 | createdGuildPlatform.id, 77 | TEST_SIGNER 78 | ); 79 | }); 80 | 81 | it("Returns 404 after delete", async () => { 82 | try { 83 | await CLIENT.guild.reward.get( 84 | guild.id, 85 | createdGuildPlatform.id, 86 | TEST_SIGNER 87 | ); 88 | assert(false); 89 | } catch (error) { 90 | expect(error).toBeInstanceOf(GuildAPICallFailed); 91 | expect(error.statusCode).toEqual(404); 92 | } 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /tests/clients/platform.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { createGuildClient } from "../../src"; 3 | 4 | const OUR_GUILD_DC_SERVER_ID = "886314998131982336"; 5 | 6 | const { platform } = createGuildClient("vitest"); 7 | 8 | describe.skip("platform client", () => { 9 | it("Can get guild by platform data", async () => { 10 | const ourGuild = await platform.getGuildByPlatform( 11 | "DISCORD", 12 | OUR_GUILD_DC_SERVER_ID 13 | ); 14 | expect(ourGuild.urlName).toEqual("our-guild"); 15 | }); 16 | 17 | it("Can get user guild access by platform data", async () => { 18 | const ourGuild = await platform.getUserGuildAccessByPlatform( 19 | "DISCORD", 20 | OUR_GUILD_DC_SERVER_ID, 21 | "604927885530234908" 22 | ); 23 | expect(ourGuild.platformGuildId).toEqual(OUR_GUILD_DC_SERVER_ID); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/clients/platformUser.test.ts: -------------------------------------------------------------------------------- 1 | import { Wallet } from "ethers"; 2 | import { describe, expect, it } from "vitest"; 3 | import { createGuildClient } from "../../src"; 4 | import { createSigner } from "../../src/utils"; 5 | 6 | const TEST_WALLET_ADDRESS = new Wallet(process.env.PRIVATE_KEY!).address; 7 | const TEST_WALLET_SIGNER = createSigner.fromEthersWallet( 8 | new Wallet(process.env.PRIVATE_KEY!) 9 | ); 10 | 11 | const { user } = createGuildClient("vitest"); 12 | 13 | describe.skip.concurrent("platformUser client", () => { 14 | it("can get a platformUser", async () => { 15 | const result = await user.platform.get( 16 | TEST_WALLET_ADDRESS, 17 | 2, 18 | TEST_WALLET_SIGNER 19 | ); 20 | 21 | expect(result).toMatchObject({ platformId: 2 }); 22 | }); 23 | 24 | it("can get all platformUsers of user", async () => { 25 | const results = await user.platform.getAll( 26 | TEST_WALLET_ADDRESS, 27 | TEST_WALLET_SIGNER 28 | ); 29 | 30 | expect(results).toMatchObject([{ platformId: 2 }]); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /tests/clients/requirement.test.ts: -------------------------------------------------------------------------------- 1 | import { Schemas } from "@guildxyz/types"; 2 | import { Wallet } from "ethers"; 3 | import { assert, describe, expect, it } from "vitest"; 4 | import { GuildAPICallFailed, GuildSDKValidationError } from "../../src/error"; 5 | import { CLIENT, TEST_SIGNER } from "../common"; 6 | import { createTestGuild, omit } from "../utils"; 7 | 8 | const ALLOWLIST_ADDRESS = Wallet.createRandom().address; 9 | 10 | const guild = await createTestGuild(); 11 | let createdRequirement: Schemas["RequirementCreateResponse"]; 12 | 13 | describe("Requirement client", () => { 14 | const requirementToCreate: Schemas["RequirementCreationPayload"] = { 15 | type: "ALLOWLIST", 16 | data: { addresses: [] }, 17 | }; 18 | 19 | it("Can create requirement", async () => { 20 | createdRequirement = await CLIENT.guild.role.requirement.create( 21 | guild.id, 22 | guild.roles[0].id, 23 | requirementToCreate, 24 | TEST_SIGNER 25 | ); 26 | 27 | expect(createdRequirement).toMatchObject(requirementToCreate); 28 | }); 29 | 30 | it("Can get a requirement", async () => { 31 | const role = await CLIENT.guild.role.requirement.get( 32 | guild.id, 33 | guild.roles[0].id, 34 | createdRequirement.id 35 | ); 36 | expect(role).toMatchObject( 37 | omit(createdRequirement, ["deletedRequirements"]) 38 | ); 39 | }); 40 | 41 | it("Can get all requirements of role", async () => { 42 | const roles = await CLIENT.guild.role.requirement.getAll( 43 | guild.id, 44 | guild.roles[0].id 45 | ); 46 | 47 | expect(roles).toMatchObject([ 48 | omit(createdRequirement, ["deletedRequirements"]), 49 | ]); 50 | }); 51 | 52 | it("Can update requirement", async () => { 53 | const created = await CLIENT.guild.role.requirement.update( 54 | guild.id, 55 | guild.roles[0].id, 56 | createdRequirement.id, 57 | { data: { addresses: [ALLOWLIST_ADDRESS] } }, 58 | TEST_SIGNER 59 | ); 60 | expect(created.data.addresses).toEqual([ALLOWLIST_ADDRESS.toLowerCase()]); 61 | }); 62 | 63 | it("Can't change requirement type", async () => { 64 | try { 65 | await CLIENT.guild.role.requirement.update( 66 | guild.id, 67 | guild.roles[0].id, 68 | createdRequirement.id, 69 | { type: "FREE" } as any, 70 | TEST_SIGNER 71 | ); 72 | assert(false); 73 | } catch (error) { 74 | expect(error).toBeInstanceOf(GuildSDKValidationError); 75 | } 76 | }); 77 | 78 | it("Returns edited requirement", async () => { 79 | const role = await CLIENT.guild.role.requirement.get( 80 | guild.id, 81 | guild.roles[0].id, 82 | createdRequirement.id 83 | ); 84 | 85 | expect(role.data.addresses).toEqual([ALLOWLIST_ADDRESS.toLowerCase()]); 86 | }); 87 | 88 | // This is needed, so we can test deletion 89 | it("Create one more requirement", async () => { 90 | await CLIENT.guild.role.requirement.create( 91 | guild.id, 92 | guild.roles[0].id, 93 | { type: "ALLOWLIST", data: { addresses: [] } }, 94 | TEST_SIGNER 95 | ); 96 | }); 97 | 98 | it("Can delete requirement", async () => { 99 | await CLIENT.guild.role.requirement.delete( 100 | guild.id, 101 | guild.roles[0].id, 102 | createdRequirement.id, 103 | TEST_SIGNER 104 | ); 105 | }); 106 | 107 | it("Doesn't return after delete", async () => { 108 | try { 109 | await CLIENT.guild.role.requirement.get( 110 | guild.id, 111 | guild.roles[0].id, 112 | createdRequirement.id 113 | ); 114 | assert(false); 115 | } catch (error) { 116 | expect(error).toBeInstanceOf(GuildAPICallFailed); 117 | expect(error.statusCode).toEqual(404); 118 | } 119 | }); 120 | }); 121 | -------------------------------------------------------------------------------- /tests/clients/role.test.ts: -------------------------------------------------------------------------------- 1 | import { assert, describe, expect, it } from "vitest"; 2 | import { GuildAPICallFailed } from "../../src/error"; 3 | import { CLIENT, TEST_SIGNER } from "../common"; 4 | import { createTestGuild } from "../utils"; 5 | 6 | const guild = await createTestGuild(); 7 | 8 | describe("role create - update - delete", () => { 9 | let createdRoleId: number; 10 | 11 | it("Can create role", async () => { 12 | const created = await CLIENT.guild.role.create( 13 | guild.id, 14 | { name: "SDK Role Creation test", requirements: [{ type: "FREE" }] }, 15 | TEST_SIGNER 16 | ); 17 | 18 | createdRoleId = created.id; 19 | expect(created).toMatchObject({ 20 | name: "SDK Role Creation test", 21 | requirements: [{ type: "FREE" }], 22 | }); 23 | }); 24 | 25 | it("Can get created role", async () => { 26 | const role = await CLIENT.guild.role.get(guild.id, createdRoleId); 27 | expect(role.name).toEqual("SDK Role Creation test"); 28 | }); 29 | 30 | it("Can update role", async () => { 31 | const created = await CLIENT.guild.role.update( 32 | guild.id, 33 | createdRoleId, 34 | { description: "EDITED" }, 35 | TEST_SIGNER 36 | ); 37 | expect(created.description).toEqual("EDITED"); 38 | }); 39 | 40 | it("Returns edited role", async () => { 41 | const role = await CLIENT.guild.role.get(guild.id, createdRoleId); 42 | expect(role.description).toEqual("EDITED"); 43 | }); 44 | 45 | it("Can delete role", async () => { 46 | await CLIENT.guild.role.delete(guild.id, createdRoleId, TEST_SIGNER); 47 | }); 48 | 49 | it("Doesn't return after delete", async () => { 50 | try { 51 | await CLIENT.guild.role.get(guild.id, createdRoleId); 52 | assert(false); 53 | } catch (error) { 54 | expect(error).toBeInstanceOf(GuildAPICallFailed); 55 | expect(error.statusCode).toEqual(404); 56 | } 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /tests/clients/rolePlatform.test.ts: -------------------------------------------------------------------------------- 1 | import { GuildReward, RoleReward } from "@guildxyz/types"; 2 | import { randomBytes } from "crypto"; 3 | import { assert, describe, expect, it } from "vitest"; 4 | import { GuildAPICallFailed } from "../../src/error"; 5 | import { CLIENT, TEST_SIGNER } from "../common"; 6 | import { createTestGuild } from "../utils"; 7 | 8 | const guild = await createTestGuild(); 9 | 10 | const guildPlatformToCreate = { 11 | platformGuildId: `my-point-system-${randomBytes(4).toString("hex")}`, 12 | platformName: "POINTS", 13 | platformGuildData: { name: "xp" }, 14 | } as const; 15 | 16 | let createdGuildPlatform: GuildReward; 17 | let createdRolePlatform: RoleReward; 18 | 19 | describe("rolePlatform client", () => { 20 | it("Can create guildPlatform", async () => { 21 | createdGuildPlatform = await CLIENT.guild.reward.create( 22 | guild.id, 23 | guildPlatformToCreate, 24 | TEST_SIGNER 25 | ); 26 | 27 | expect(createdGuildPlatform.platformGuildId).toEqual( 28 | guildPlatformToCreate.platformGuildId 29 | ); 30 | }); 31 | 32 | it("Can create rolePlatform", async () => { 33 | createdRolePlatform = await CLIENT.guild.role.reward.create( 34 | guild.id, 35 | guild.roles[0].id, 36 | { 37 | guildPlatformId: createdGuildPlatform.id, 38 | platformRoleData: { score: "5" }, 39 | }, 40 | TEST_SIGNER 41 | ); 42 | 43 | expect(createdRolePlatform).toMatchObject({ 44 | guildPlatformId: createdGuildPlatform.id, 45 | platformRoleData: { score: "5" }, 46 | }); 47 | }); 48 | 49 | it("Can update rolePlatform", async () => { 50 | const updated = await CLIENT.guild.role.reward.update( 51 | guild.id, 52 | guild.roles[0].id, 53 | createdRolePlatform.id, 54 | { platformRoleData: { score: "15" } }, 55 | TEST_SIGNER 56 | ); 57 | 58 | expect(updated.platformRoleData!.score).toEqual("15"); 59 | }); 60 | 61 | it("Returns updated data", async () => { 62 | const created = await CLIENT.guild.role.reward.get( 63 | guild.id, 64 | guild.roles[0].id, 65 | createdRolePlatform.id 66 | ); 67 | 68 | expect(created.platformRoleData!.score).toEqual("15"); 69 | }); 70 | 71 | it("Returns updated data by role", async () => { 72 | const created = await CLIENT.guild.role.reward.getAll( 73 | guild.id, 74 | guild.roles[0].id 75 | ); 76 | 77 | expect(created).toMatchObject([{ platformRoleData: { score: "15" } }]); 78 | }); 79 | 80 | it("Can delete rolePlatform", async () => { 81 | await CLIENT.guild.role.reward.delete( 82 | guild.id, 83 | guild.roles[0].id, 84 | createdRolePlatform.id, 85 | TEST_SIGNER 86 | ); 87 | }); 88 | 89 | it("404 after delete", async () => { 90 | try { 91 | await CLIENT.guild.role.reward.get( 92 | guild.id, 93 | guild.roles[0].id, 94 | createdRolePlatform.id 95 | ); 96 | assert(false); 97 | } catch (error) { 98 | expect(error).toBeInstanceOf(GuildAPICallFailed); 99 | expect(error.statusCode).toEqual(404); 100 | } 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /tests/clients/user.test.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from "crypto"; 2 | import { Wallet } from "ethers"; 3 | import { assert, describe, expect, it } from "vitest"; 4 | import { GuildAPICallFailed } from "../../src"; 5 | import { createSigner } from "../../src/utils"; 6 | import { CLIENT } from "../common"; 7 | 8 | const NEW_WALLET = new Wallet(randomBytes(32).toString("hex")); 9 | const NEW_ADDRESS = NEW_WALLET.address.toLowerCase(); 10 | const NEW_SIGNER = createSigner.fromEthersWallet(NEW_WALLET); 11 | 12 | describe("User client", () => { 13 | it("user initially doesn't exist", async () => { 14 | try { 15 | await CLIENT.user.get(NEW_ADDRESS); 16 | assert(false); 17 | } catch (error) { 18 | expect(error).toBeInstanceOf(GuildAPICallFailed); 19 | expect((error as GuildAPICallFailed).statusCode).toEqual(404); 20 | } 21 | }); 22 | 23 | it("created user with a signed request", async () => { 24 | const profile = await CLIENT.user.getProfile(NEW_ADDRESS, NEW_SIGNER); 25 | 26 | expect(profile.addresses).toMatchObject([{ address: NEW_ADDRESS }]); 27 | }); 28 | 29 | it("can fetch user", async () => { 30 | const fetchedUser = await CLIENT.user.get(NEW_ADDRESS); 31 | 32 | expect(fetchedUser.id).toBeGreaterThan(0); 33 | }); 34 | 35 | it("can fetch user public user profile", async () => { 36 | const fetchedUserProfile = await CLIENT.user.getProfile(NEW_ADDRESS); 37 | 38 | expect(fetchedUserProfile.id).toBeGreaterThan(0); 39 | }); 40 | 41 | it("Can fetch user private user profile", async () => { 42 | const fetchedUserProfile = await CLIENT.user.getProfile( 43 | NEW_ADDRESS, 44 | NEW_SIGNER 45 | ); 46 | 47 | expect(fetchedUserProfile.id).toBeGreaterThan(0); 48 | expect(fetchedUserProfile.addresses).toMatchObject([ 49 | { address: NEW_ADDRESS }, 50 | ]); 51 | }); 52 | 53 | it("Can fetch memberships", async () => { 54 | const memberships = await CLIENT.user.getMemberships(NEW_ADDRESS); 55 | expect(memberships.length).toBe(0); 56 | }); 57 | 58 | it("can delete user", async () => { 59 | await CLIENT.user.delete(NEW_ADDRESS, NEW_SIGNER); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /tests/clients/userAddress.test.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from "crypto"; 2 | import { Wallet } from "ethers"; 3 | import { describe, expect, it } from "vitest"; 4 | import { createSigner } from "../../src/utils"; 5 | import { CLIENT, TEST_ADDRESS, TEST_SIGNER } from "../common"; 6 | 7 | const NEW_WALLET = new Wallet(randomBytes(32).toString("hex")); 8 | const NEW_ADDRESS = NEW_WALLET.address.toLowerCase(); 9 | const NEW_SIGNER = createSigner.fromEthersWallet(NEW_WALLET); 10 | 11 | describe("userAddress client", () => { 12 | it("Can create userAddress", async () => { 13 | const created = await CLIENT.user.address.create( 14 | TEST_ADDRESS, 15 | NEW_SIGNER, 16 | TEST_SIGNER 17 | ); 18 | 19 | expect(created).toMatchObject({ 20 | address: NEW_ADDRESS, 21 | isPrimary: false, 22 | }); 23 | }); 24 | 25 | it("Can get linked address", async () => { 26 | const address = await CLIENT.user.address.get( 27 | TEST_ADDRESS, 28 | NEW_ADDRESS, 29 | TEST_SIGNER 30 | ); 31 | 32 | expect(address.address).toEqual(NEW_ADDRESS); 33 | }); 34 | 35 | it("Can get linked addresses", async () => { 36 | const addresses = await CLIENT.user.address.getAll( 37 | TEST_ADDRESS, 38 | TEST_SIGNER 39 | ); 40 | 41 | expect(addresses.length).toEqual(2); 42 | }); 43 | 44 | it("Can delete userAddress", async () => { 45 | await CLIENT.user.address.delete(TEST_ADDRESS, NEW_ADDRESS, TEST_SIGNER); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /tests/common.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from "crypto"; 2 | import { Wallet } from "ethers"; 3 | import { createGuildClient, createSigner } from "../src"; 4 | 5 | export const CLIENT = createGuildClient("vitest"); 6 | export const TEST_WALLET = new Wallet(randomBytes(32).toString("hex")); 7 | export const TEST_ADDRESS = TEST_WALLET.address.toLowerCase(); 8 | export const TEST_SIGNER = createSigner.fromEthersWallet(TEST_WALLET); 9 | export const TEST_USER = await CLIENT.user.getProfile( 10 | TEST_ADDRESS, 11 | TEST_SIGNER 12 | ); 13 | -------------------------------------------------------------------------------- /tests/points.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | LeaderboardItem, 3 | RoleCreationResponse, 4 | RoleReward, 5 | UserPointsItem, 6 | } from "@guildxyz/types"; 7 | import { describe, expect, it, test } from "vitest"; 8 | import { CLIENT, TEST_ADDRESS, TEST_SIGNER, TEST_USER } from "./common"; 9 | import { createTestGuild } from "./utils"; 10 | 11 | const guild = await createTestGuild(); 12 | const role1 = guild.roles[0]; 13 | 14 | let createdRolePlatform: RoleReward; 15 | let role2: RoleCreationResponse; 16 | 17 | describe("points", () => { 18 | it("created second role", async () => { 19 | role2 = await CLIENT.guild.role.create( 20 | guild.id, 21 | { 22 | name: "Role 2", 23 | requirements: [{ type: "FREE" }], 24 | }, 25 | TEST_SIGNER 26 | ); 27 | 28 | expect(role2.id).toBeGreaterThan(0); 29 | }); 30 | 31 | test("initially has no guildPlatform", async () => { 32 | const guildPlatforms = await CLIENT.guild.reward.getAll( 33 | guild.id, 34 | TEST_SIGNER 35 | ); 36 | 37 | expect(guildPlatforms).toHaveLength(0); 38 | }); 39 | 40 | test("can create a points reward", async () => { 41 | createdRolePlatform = await CLIENT.guild.role.reward.create( 42 | guild.id, 43 | role1.id, 44 | { 45 | guildPlatform: { 46 | platformGuildId: "my-points", 47 | platformName: "POINTS", 48 | platformGuildData: { name: "coins" }, 49 | }, 50 | platformRoleData: { score: "5" }, 51 | }, 52 | TEST_SIGNER 53 | ); 54 | 55 | expect(createdRolePlatform.platformRoleData).toEqual({ score: "5" }); 56 | }); 57 | 58 | test("can create a points reward for other role", async () => { 59 | createdRolePlatform = await CLIENT.guild.role.reward.create( 60 | guild.id, 61 | role2.id, 62 | { 63 | guildPlatformId: createdRolePlatform.guildPlatformId, 64 | platformRoleData: { score: "10" }, 65 | }, 66 | TEST_SIGNER 67 | ); 68 | 69 | expect(createdRolePlatform.platformRoleData).toEqual({ score: "10" }); 70 | }); 71 | 72 | test("get leaderboard - just for triggering revalidation", async () => { 73 | await CLIENT.guild.getLeaderboard( 74 | guild.id, 75 | createdRolePlatform.guildPlatformId 76 | ); 77 | 78 | await new Promise((resolve) => { 79 | setTimeout(() => { 80 | resolve(); 81 | }, 2000); 82 | }); 83 | }); 84 | 85 | test("get leaderboard - not signed", async () => { 86 | const { leaderboard, aroundUser } = await CLIENT.guild.getLeaderboard( 87 | guild.id, 88 | createdRolePlatform.guildPlatformId 89 | ); 90 | 91 | expect(leaderboard).toBeTruthy(); 92 | expect(aroundUser).toBeFalsy(); 93 | expect(leaderboard).toHaveLength(1); 94 | expect(leaderboard[0]).toMatchObject({ 95 | // roleIds: [role1.id, role2.id], 96 | userId: TEST_USER.id, 97 | totalPoints: 15, 98 | // rank: 1, 99 | }); 100 | }); 101 | 102 | test("get leaderboard - signed", async () => { 103 | const { leaderboard, aroundUser } = await CLIENT.guild.getLeaderboard( 104 | guild.id, 105 | createdRolePlatform.guildPlatformId, 106 | TEST_SIGNER 107 | ); 108 | 109 | expect(leaderboard).toBeTruthy(); 110 | expect(aroundUser).toBeTruthy(); 111 | expect(aroundUser).toHaveLength(1); 112 | expect(leaderboard).toHaveLength(1); 113 | expect(leaderboard[0]).toMatchObject({ 114 | // roleIds: [role1.id, role2.id], 115 | userId: TEST_USER.id, 116 | totalPoints: 15, 117 | // rank: 1, 118 | }); 119 | expect(aroundUser![0]).toMatchObject({ 120 | // roleIds: [role1.id, role2.id], 121 | userId: TEST_USER.id, 122 | totalPoints: 15, 123 | // rank: 1, 124 | }); 125 | }); 126 | 127 | test("get user points", async () => { 128 | const response = await CLIENT.user.getPoints(TEST_USER.id, TEST_SIGNER); 129 | 130 | expect(response).toHaveLength(1); 131 | expect(response[0]).toMatchObject({ 132 | guildId: guild.id, 133 | guildPlatformId: createdRolePlatform.guildPlatformId, 134 | // roleIds: [role1.id, role2.id], 135 | totalPoints: 15, 136 | }); 137 | }); 138 | 139 | test("get user rank", async () => { 140 | const response = await CLIENT.user.getRankInGuild( 141 | TEST_USER.id, 142 | guild.id, 143 | createdRolePlatform.guildPlatformId 144 | ); 145 | 146 | expect(response).toMatchObject({ 147 | userId: TEST_USER.id, 148 | // roleIds: [role1.id, role2.id], 149 | totalPoints: 15, 150 | rank: 1, 151 | address: TEST_ADDRESS, 152 | }); 153 | }); 154 | 155 | test("can delete guildPlatform", async () => { 156 | await CLIENT.guild.reward.delete( 157 | guild.id, 158 | createdRolePlatform.guildPlatformId, 159 | TEST_SIGNER 160 | ); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /tests/utils.ts: -------------------------------------------------------------------------------- 1 | import { randomBytes } from "crypto"; 2 | import { afterAll } from "vitest"; 3 | import { CLIENT, TEST_SIGNER } from "./common"; 4 | 5 | export function omit, K extends keyof T>( 6 | obj: T, 7 | keys: K[] 8 | ): Omit { 9 | return Object.fromEntries( 10 | Object.keys(obj) 11 | .filter((key: keyof T) => !keys.includes(key as K)) 12 | .map((key) => [key, obj[key]]) 13 | ) as Omit; 14 | } 15 | 16 | export function pick, K extends keyof T>( 17 | obj: T, 18 | keys: K[] 19 | ): Pick { 20 | return Object.fromEntries(keys.map((key) => [key, obj[key]])) as Pick; 21 | } 22 | 23 | export async function createTestGuild() { 24 | const random = randomBytes(4).toString("hex"); 25 | 26 | const guild = await CLIENT.guild.create( 27 | { 28 | name: "SDK Test Guild", 29 | urlName: `sdk-test-guild-${random}`, 30 | contacts: [], 31 | roles: [{ name: "SDK Test Role", requirements: [{ type: "FREE" }] }], 32 | }, 33 | TEST_SIGNER 34 | ); 35 | 36 | afterAll(async () => { 37 | await CLIENT.guild.delete(guild.id, TEST_SIGNER); 38 | }); 39 | 40 | return guild; 41 | } 42 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, 8 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | "allowJs": true /* Allow javascript files to be compiled. */, 11 | "checkJs": false /* Report errors in .js files. */, 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ 13 | "declaration": true /* Generates corresponding '.d.ts' file. */, 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | //"sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./build" /* Redirect output structure to the directory. */, 18 | "rootDir": "./" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | "removeComments": true /* Do not emit comments to output. */, 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true /* Enable all strict type-checking options. */, 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | "strictNullChecks": true /* Enable strict null checks. */, 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ 44 | 45 | /* Module Resolution Options */ 46 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 47 | // "baseUrl": "./" /* Base directory to resolve non-absolute module names. */, 48 | // "paths": {} /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */, 49 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 50 | // "typeRoots": [], /* List of folders to include type definitions from. */ 51 | // "types": [], /* Type declaration files to be included in compilation. */ 52 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 53 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 54 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 55 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 56 | 57 | /* Source Map Options */ 58 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 61 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 62 | 63 | /* Experimental Options */ 64 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 65 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 66 | 67 | /* Advanced Options */ 68 | "skipLibCheck": true /* Skip type checking of declaration files. */, 69 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, 70 | "resolveJsonModule": true 71 | }, 72 | "exclude": ["node_modules"], 73 | "include": ["src"] 74 | } 75 | -------------------------------------------------------------------------------- /v2-migration-guide.md: -------------------------------------------------------------------------------- 1 |
2 |

Guild SDK v2 migration guide

3 | 4 | 5 | 6 |
7 | Application 8 |   •   9 | Twitter 10 |   •   11 | Github 12 |   •   13 | Discord 14 |
15 | 16 | ## Contents 17 | 18 | - [SignerFunction](#signing-a-message) 19 | - [Clients](#clients) 20 | - [Platform](#platform) 21 | - [User](#user) 22 | - [getMemberships](#getmemberships) 23 | - [join](#join) 24 | - [Guild](#guild) 25 | - [getAll](#getall) 26 | - [getByAddress](#getbyaddress) 27 | - [get](#get) 28 | - [getUserAccess](#getuseraccess) 29 | - [getUserMemberships](#getusermemberships) 30 | - [create](#create) 31 | - [update](#update) 32 | - [delete](#delete) 33 | - [Role](#role) 34 | - [get](#get-1) 35 | - [create](#create-1) 36 | - [update](#update-1) 37 | - [delete](#delete-1) 38 | 39 | ## Signing a message 40 | 41 | Use `createSigner` to construct a signer function. There are multiple ways to create a signer function, for example `createSigner.fromEthersWallet` can construct a signer from an `ethers.Walllet` 42 | 43 | ```ts 44 | import { createSigner, guild } from "@guildxyz/sdk"; 45 | import { Wallet } from "ethers"; 46 | import { randomBytes } from "crypto"; 47 | 48 | const privateKey = randomBytes(32); 49 | 50 | const mySigner = createSigner.fromEthersWallet( 51 | new Wallet(privateKey.toString("hex")) 52 | ); 53 | 54 | // Use it directly 55 | const myAuthData = await mySigner({ some: "payload" }); 56 | 57 | // Or pass it to a client method 58 | const createdGuild = await guild.create( 59 | { 60 | name: "My SDK test Guild", 61 | urlName: "my-sdk-test-guild", 62 | description: "My first Guild created with the SDK", 63 | roles: [{ name: "My SDK test Role", requirements: [{ type: "FREE" }] }], 64 | }, 65 | mySigner 66 | ); 67 | ``` 68 | 69 | ## Clients 70 | 71 | These clients have changed in the latest version of the SDK, and are now more flexible, and easier to use. The `platformName` parameter is no longer needed, as it is now a property of the client. The `signerAddress` and `sign` parameters are no longer needed either, as the clients now accept a single `signer: SignerFunction` parameter. The `SignerFunction` is a function that takes a payload, and returns a signature. See [Signing a message](#signing-a-message) for more details. 72 | 73 | ### Platform 74 | 75 | Use the `withPlatformName` function on the `platform` client to create a platform client that is fixed with a specific `platformName` 76 | 77 | ```ts 78 | import { platform } from "@guildxyz/sdk"; 79 | 80 | const discordClient = platform.withPlatformName("DISCORD") 81 | 82 | await discordClient.getGuildByPlatform(...) // Doesn't need a platformName parameter! 83 | ``` 84 | 85 | ### User 86 | 87 | #### `getMemberships` 88 | 89 | Now accepts user ids as well, and optionally takes a `SignerFunction`. If the signer is provided, and the signature is successfully validated, the result will be more detailed. 90 | 91 | ```ts 92 | import { user } from "@guildxyz/sdk"; 93 | 94 | const userMemberships = await user.getMemberships("0x...", mySigner); 95 | 96 | const guildMembership = userMemberships.find( 97 | ({ guildId }) => guildId === someGuildId 98 | ); 99 | ``` 100 | 101 | #### `join` 102 | 103 | Use the `actions.join` client. Create a new join action with `actions.join.start`, then poll it's state by either calling `actions.join.poll`, or `actions.join.await`. The former will make a single poll, while the latter will keep polling the state until the job is done 104 | 105 | ```ts 106 | import { actions } from "@guildxyz/sdk"; 107 | 108 | // Start a join action, if it hasn't been started yet 109 | await actions.join.start(someGuildId, mySigner); 110 | 111 | // Poll the job until it is done. Poll every 2 seconds, and log results 112 | const joinResult = actions.join.await(someGuildId, mySigner, console.log, 2000); 113 | ``` 114 | 115 | ### Guild 116 | 117 | #### `getAll` 118 | 119 | Use `guild.getMany` to fetch multiple guilds by their IDs. Use `guild.search` to search for guilds with paginated results. The `roles: string[]` field, containing the names of the guild's roles isn't returned anymore, if needed, it needs to be fetched separately with a `guild.role.getAll` call 120 | 121 | ```ts 122 | import { guild } from "@guildxyz/sdk"; 123 | 124 | const guilds = await guild.getMany([someGuildId, someOtherGuildId]); 125 | ``` 126 | 127 | #### `getByAddress` 128 | 129 | This method doesn't exist anymore 130 | 131 | #### `get` 132 | 133 | Use `guild.get`. The response won't include roles, rewards, nor admins. Those can be fetched with `guild.role.getAll`, `guild.reward.getAll`, and `guild.admin.getAll` 134 | 135 | ```ts 136 | import { guild } from "@guildxyz/sdk"; 137 | 138 | const guild = await guild.get(someGuildId); 139 | const guildPlatforms = await guild.reward.getAll(someGuildId); 140 | const roles = await guild.role.getAll(someGuildId); 141 | const admins = await guild.admin.getAll(someGuildId); 142 | ``` 143 | 144 | > Note that the role response won't include everything either, rolePlatforms/rewards, and requirements will be missing, those can be fetched with `guild.role.reward.getAll` and `guild.role.requirement.getAll` 145 | 146 | #### `getUserAccess` 147 | 148 | Use `guild.getMemberAccess`. It now accepts user id as well, and optionally a signer to get more detailed output 149 | 150 | ```ts 151 | import { guild } from "@guildxyz/sdk"; 152 | 153 | const memberAccess = await guild.getMemberAccess(someGuildId, someUserId); 154 | ``` 155 | 156 | #### `getUserMemberships` 157 | 158 | Use `user.getMemberships` to fetch for all the guilds, where the user is a member 159 | 160 | ```ts 161 | import { user } from "@guildxyz/sdk"; 162 | 163 | const memberships = await user.getMemberships(someUserId); 164 | 165 | const guildMembership = memberships.find( 166 | ({ guildId }) => guildId === someGuildId 167 | ); 168 | ``` 169 | 170 | #### `create` 171 | 172 | Use `guild.create`. Instead of separate `signerAddress`, and `sign` params, it now accepts a single `signer: SignerFunction` 173 | 174 | ```ts 175 | import { guild } from "@guildxyz/sdk"; 176 | 177 | const createdGuild = await guild.create( 178 | { 179 | name: "My SDK test Guild", 180 | urlName: "my-sdk-test-guild", 181 | description: "My first Guild created with the SDK", 182 | roles: [{ name: "My SDK test Role", requirements: [{ type: "FREE" }] }], 183 | }, 184 | mySigner 185 | ); 186 | ``` 187 | 188 | #### `update` 189 | 190 | Use `guild.update`. Instead of separate `signerAddress`, and `sign` params, it now accepts a single `signer: SignerFunction` 191 | 192 | ```ts 193 | import { guild } from "@guildxyz/sdk"; 194 | 195 | const updatedGuild = await guild.update( 196 | someGuildId, 197 | { 198 | description: "I've edited my first Guild created with the SDK", 199 | }, 200 | mySigner 201 | ); 202 | ``` 203 | 204 | #### `delete` 205 | 206 | Use `guild.delete`. Instead of separate `signerAddress`, and `sign` params, it now accepts a single `signer: SignerFunction`. The `success` flag in the response isn't returned anymore, if the call resolved, then the deletion was successful, if it rejected, then something went wrong, and a `GuildAPICallFailed` error is thrown 207 | 208 | ```ts 209 | import { guild } from "@guildxyz/sdk"; 210 | 211 | await guild.delete(someGuildId, mySigner); 212 | ``` 213 | 214 | ### Role 215 | 216 | #### `get` 217 | 218 | Use `guild.role.get`. It now needs a `guildIdOrUrlName` param as well, and optionally takes a signer for more detailed results. The `guildId`, `members`, `requirements` and `rolePlatforms` fields aren't included in the result anymore. `guildId` is assumed to be known, as it is needed to make the get call, the other three can be fetched sepatarately with `guild.getMembers`, `guild.role.requirement.getAll` and `guild.role.reward.getAll` 219 | 220 | ```ts 221 | import { guild } from "@guildxyz/sdk"; 222 | 223 | const role = await guild.role.get(someGuildId, someRoleId); 224 | ``` 225 | 226 | #### `create` 227 | 228 | Use `guild.role.create`. Instead of separate `signerAddress`, and `sign` params, it now accepts a single `signer: SignerFunction`. Similarly to `get`, the `guildId`, `members`, `requirements` and `rolePlatforms` fields aren't included in the response 229 | 230 | ```ts 231 | import { guild } from "@guildxyz/sdk"; 232 | 233 | const createdRole = await guild.role.create( 234 | someGuildId, 235 | { name: "My new Role", requirements: [{ type: "FREE" }] }, 236 | mySigner 237 | ); 238 | ``` 239 | 240 | #### `update` 241 | 242 | Use `guild.role.update`. Instead of separate `signerAddress`, and `sign` params, it now accepts a single `signer: SignerFunction`. Similarly to `get`, the `guildId`, `members`, `requirements` and `rolePlatforms` fields aren't included in the response 243 | 244 | ```ts 245 | import { guild } from "@guildxyz/sdk"; 246 | 247 | const updatedRole = await guild.role.update( 248 | someGuildId, 249 | someRoleId, 250 | { description: "I've edited my new role" }, 251 | mySigner 252 | ); 253 | ``` 254 | 255 | #### `delete` 256 | 257 | Use `guild.role.delete`. Instead of separate `signerAddress`, and `sign` params, it now accepts a single `signer: SignerFunction`. The `success` flag in the response isn't returned anymore, if the call resolved, then the deletion was successful, if it rejected, then something went wrong, and a `GuildAPICallFailed` error is thrown 258 | 259 | ```ts 260 | import { guild } from "@guildxyz/sdk"; 261 | 262 | await guild.role.delete(someGuildId, someRoleId, mySigner); 263 | ``` 264 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | import { defineConfig } from "vitest/config"; 3 | 4 | export default defineConfig({ 5 | test: { 6 | testTimeout: 60_000, 7 | }, 8 | }); 9 | --------------------------------------------------------------------------------