rpgf.ts
1 import { browser } from '$app/environment'; 2 import { get } from 'svelte/store'; 3 import getOptionalEnvVar from '../get-optional-env-var/public'; 4 import stripTrailingSlash from '../strip-trailing-slash'; 5 import { refreshAccessToken, rpgfAccessJwtStore } from './siwe'; 6 import { z } from 'zod'; 7 import network from '$lib/stores/wallet/network'; 8 import { error } from '@sveltejs/kit'; 9 import walletStore from '$lib/stores/wallet/wallet.store'; 10 import { 11 roundSchema, 12 slugAvailableResponseSchema, 13 type CreateRoundDto, 14 type PatchRoundDto, 15 type Round, 16 } from './types/round'; 17 import { 18 applicationCategorySchema, 19 applicationFormSchema, 20 applicationSchema, 21 applicationVersionSchema, 22 listingApplicationSchema, 23 type Application, 24 type ApplicationCategory, 25 type ApplicationForm, 26 type ApplicationReviewDto, 27 type ApplicationVersion, 28 type CreateApplicationCategoryDto, 29 type CreateApplicationDto, 30 type CreateApplicationFormDto, 31 type ListingApplication, 32 type UpdateApplicationDto, 33 } from './types/application'; 34 import { 35 ballotSchema, 36 wrappedBallotSchema, 37 type Ballot, 38 type SubmitBallotDto, 39 type WrappedBallot, 40 } from './types/ballot'; 41 import { userSchema, type RpgfUser } from './types/user'; 42 import { auditLogSchema, type AuditLog } from './types/auditLog'; 43 import { 44 kycRequestSchema, 45 type CreateKycRequestForApplicationDto, 46 type KycRequest, 47 } from './types/kyc'; 48 import { customDatasetSchema, type CustomDataset } from './types/customDataset'; 49 import { signBallot } from './sign-ballot'; 50 51 const rpgfApiUrl = getOptionalEnvVar( 52 'PUBLIC_DRIPS_RPGF_URL', 53 true, 54 'RPGF functionality doesnt work.', 55 ); 56 const rpgfInternalApiUrl = getOptionalEnvVar( 57 'PUBLIC_INTERNAL_DRIPS_RPGF_URL', 58 true, 59 'RPGF functionality doesnt work.', 60 ); 61 62 export async function rpgfServerCall( 63 path: string, 64 method: string = 'GET', 65 body: unknown = null, 66 headers: Record<string, string> = {}, 67 f = fetch, 68 contentType: 69 | 'application/json' 70 | 'text/csv' 71 | 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' = 'application/json', 72 ) { 73 if (!rpgfApiUrl || !rpgfInternalApiUrl) { 74 throw new Error('Required environment variables are not set'); 75 } 76 77 const baseUrl = stripTrailingSlash(browser ? rpgfApiUrl : rpgfInternalApiUrl); 78 79 const res = await f(`${baseUrl}/api${path}`, { 80 method, 81 credentials: 'include', 82 headers: { 83 ...headers, 84 'Content-Type': contentType, 85 }, 86 body: body 87 ? contentType === 'application/json' 88 ? JSON.stringify(body) 89 : (body as BodyInit) 90 : null, 91 }); 92 93 return res; 94 } 95 96 export async function authenticatedRpgfServerCall( 97 path: string, 98 method: string = 'GET', 99 body: unknown = null, 100 f = fetch, 101 attemptRefresh: boolean = true, 102 disableErrorHandling = false, 103 contentType: 104 | 'application/json' 105 | 'text/csv' 106 | 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' = 'application/json', 107 ) { 108 const accessToken = get(walletStore).connected ? get(rpgfAccessJwtStore) : null; 109 110 const res = await rpgfServerCall( 111 path, 112 method, 113 body, 114 { 115 Authorization: accessToken ? `Bearer ${accessToken}` : '', 116 }, 117 f, 118 contentType, 119 ); 120 121 if (res.status === 401 && accessToken && attemptRefresh) { 122 // Try to refresh the access token 123 const { success } = await refreshAccessToken(); 124 125 if (success) { 126 return authenticatedRpgfServerCall(path, method, body, f, false); 127 } 128 } 129 130 if (res.status === 400 || res.status === 401) { 131 const errorBody = await res.text(); 132 133 let message; 134 if (errorBody) { 135 message = errorBody; 136 } else if (res.status === 400) { 137 message = 'Bad Request'; 138 } else { 139 message = 'Unauthorized'; 140 } 141 142 throw error(res.status, message); 143 } 144 145 if (disableErrorHandling) { 146 return res; 147 } 148 149 if (res.status === 500) { 150 // Server error, throw a generic error 151 throw error(500, 'Unexpected server error occurred.'); 152 } 153 154 return res; 155 } 156 157 export async function getRounds(f = fetch, own = false): Promise<Round[]> { 158 const res = await authenticatedRpgfServerCall( 159 `/rounds${own ? '/own' : ''}?chainId=${network.chainId}`, 160 'GET', 161 undefined, 162 f, 163 ); 164 165 const parsed = roundSchema.array().parse(await res.json()); 166 return parsed; 167 } 168 169 export async function createRound(f = fetch, draft: CreateRoundDto): Promise<Round> { 170 const res = await authenticatedRpgfServerCall(`/rounds`, 'PUT', draft, f); 171 172 const parsed = roundSchema.parse(await res.json()); 173 return parsed; 174 } 175 176 export async function updateRound(f = fetch, id: string, draft: PatchRoundDto): Promise<Round> { 177 // strip empty fields 178 const strippedDraft = Object.fromEntries( 179 Object.entries(draft).filter(([_, value]) => value !== undefined && value !== ''), 180 ) as PatchRoundDto; 181 182 const res = await authenticatedRpgfServerCall(`/rounds/${id}`, 'PATCH', strippedDraft, f); 183 184 const parsed = roundSchema.parse(await res.json()); 185 return parsed; 186 } 187 188 export async function deleteRound(f = fetch, id: string): Promise<Response> { 189 return await authenticatedRpgfServerCall(`/rounds/${id}`, 'DELETE', undefined, f); 190 } 191 192 export async function publishRound(f = fetch, id: string): Promise<Round> { 193 const res = await authenticatedRpgfServerCall(`/rounds/${id}/publish`, 'POST', undefined, f); 194 195 const parsed = roundSchema.parse(await res.json()); 196 return parsed; 197 } 198 199 export async function checkSlugAvailability(f = fetch, slug: string): Promise<boolean> { 200 const res = await authenticatedRpgfServerCall(`/rounds/check-slug/${slug}`, 'GET', undefined, f); 201 202 const { available } = slugAvailableResponseSchema.parse(await res.json()); 203 return available; 204 } 205 206 export async function getRound(f = fetch, slug: string): Promise<Round | null> { 207 const res = await authenticatedRpgfServerCall( 208 `/rounds/${slug}?chainId=${network.chainId}`, 209 'GET', 210 undefined, 211 f, 212 ); 213 const body = await res.json(); 214 215 if (res.status === 404) { 216 return null; 217 } 218 219 return roundSchema.parse(body); 220 } 221 222 export async function submitApplication( 223 f = fetch, 224 roundId: string, 225 application: CreateApplicationDto, 226 ): Promise<Application> { 227 const res = await authenticatedRpgfServerCall( 228 `/rounds/${roundId}/applications`, 229 'PUT', 230 application, 231 f, 232 ); 233 234 return applicationSchema.parse(await res.json()); 235 } 236 237 export async function updateApplication( 238 f = fetch, 239 roundId: string, 240 applicationId: string, 241 application: UpdateApplicationDto, 242 ): Promise<Application> { 243 const res = await authenticatedRpgfServerCall( 244 `/rounds/${roundId}/applications/${applicationId}`, 245 'POST', 246 application, 247 f, 248 ); 249 250 return applicationSchema.parse(await res.json()); 251 } 252 253 export async function getApplications( 254 f = fetch, 255 roundId: string, 256 limit: number = 1000, 257 offset: number = 0, 258 sortBy: string = 'createdAt:desc', 259 filterByUserId: string | null = null, 260 filterByStatus: 'approved' | 'rejected' | 'pending' | null = null, 261 filterByCategoryId: string | null = null, 262 ): Promise<ListingApplication[]> { 263 const res = await authenticatedRpgfServerCall( 264 `/rounds/${roundId}/applications?sort=${sortBy}&limit=${limit}&offset=${offset}${ 265 filterByUserId ? `&submitterUserId=${filterByUserId}` : '' 266 }${filterByStatus ? `&state=${filterByStatus}` : ''}${ 267 filterByCategoryId ? `&categoryId=${filterByCategoryId}` : '' 268 }`, 269 'GET', 270 undefined, 271 f, 272 ); 273 274 return listingApplicationSchema.array().parse(await res.json()); 275 } 276 277 export async function getApplicationsCsv( 278 f = fetch, 279 roundSlug: string, 280 onlyApproved = false, 281 ): Promise<string> { 282 const res = await authenticatedRpgfServerCall( 283 `/rounds/${roundSlug}/applications?format=csv${onlyApproved ? '&state=approved' : ''}`, 284 'GET', 285 undefined, 286 f, 287 ); 288 289 if (!res.ok) { 290 throw new Error(`${res.status} - Failed to fetch applications CSV: ${res.statusText}`); 291 } 292 293 return await res.text(); 294 } 295 296 export async function getApplicationsXlsx( 297 f = fetch, 298 roundSlug: string, 299 onlyApproved = false, 300 ): Promise<Blob> { 301 const res = await authenticatedRpgfServerCall( 302 `/rounds/${roundSlug}/applications?format=xlsx${onlyApproved ? '&state=approved' : ''}`, 303 'GET', 304 undefined, 305 f, 306 ); 307 308 if (!res.ok) { 309 throw new Error(`${res.status} - Failed to fetch applications XLSX: ${res.statusText}`); 310 } 311 312 return await res.blob(); 313 } 314 315 export async function getApplication( 316 f = fetch, 317 roundId: string, 318 applicationId: string, 319 ): Promise<Application> { 320 const res = await authenticatedRpgfServerCall( 321 `/rounds/${roundId}/applications/${applicationId}`, 322 'GET', 323 undefined, 324 f, 325 ); 326 327 return applicationSchema.parse(await res.json()); 328 } 329 330 export async function getApplicationHistory( 331 f = fetch, 332 roundId: string, 333 applicationId: string, 334 ): Promise<ApplicationVersion[]> { 335 const res = await authenticatedRpgfServerCall( 336 `/rounds/${roundId}/applications/${applicationId}/history`, 337 'GET', 338 undefined, 339 f, 340 ); 341 342 return applicationVersionSchema.array().parse(await res.json()); 343 } 344 345 export async function submitApplicationReview( 346 f = fetch, 347 roundId: string, 348 decisions: ApplicationReviewDto, 349 ): Promise<void> { 350 await authenticatedRpgfServerCall(`/rounds/${roundId}/applications/review`, 'POST', decisions, f); 351 } 352 353 export async function castBallot( 354 f = fetch, 355 roundSlug: string, 356 ballot: Ballot, 357 ): Promise<WrappedBallot> { 358 const { signature, chainId } = await signBallot(ballot); 359 360 const res = await authenticatedRpgfServerCall( 361 `/rounds/${roundSlug}/ballots`, 362 'PUT', 363 { 364 ballot, 365 signature, 366 chainId, 367 } satisfies SubmitBallotDto, 368 f, 369 ); 370 371 const parsed = wrappedBallotSchema.parse(await res.json()); 372 return parsed; 373 } 374 375 type SpreadsheetFormat = 'csv' | 'xlsx'; 376 type SpreadsheetBody = string | ArrayBuffer; 377 378 const SPREADSHEET_CONTENT_TYPE: Record< 379 SpreadsheetFormat, 380 Parameters<typeof authenticatedRpgfServerCall>[6] 381 > = { 382 csv: 'text/csv', 383 xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 384 }; 385 386 export async function parseBallotSpreadsheet( 387 f = fetch, 388 roundSlug: string, 389 data: SpreadsheetBody, 390 format: SpreadsheetFormat, 391 ): Promise<Ballot> { 392 const res = await authenticatedRpgfServerCall( 393 `/rounds/${roundSlug}/ballots/parse-spreadsheet?format=${format}`, 394 'POST', 395 data, 396 f, 397 true, 398 false, 399 SPREADSHEET_CONTENT_TYPE[format], 400 ); 401 402 return z.object({ ballot: ballotSchema }).parse(await res.json()).ballot; 403 } 404 405 async function submitSpreadsheetBallot( 406 f = fetch, 407 roundSlug: string, 408 data: SpreadsheetBody, 409 format: SpreadsheetFormat, 410 addressOverride?: string, 411 ): Promise<WrappedBallot> { 412 const ballot = await parseBallotSpreadsheet(f, roundSlug, data, format); 413 const { signature, chainId } = await signBallot(ballot); 414 415 const query = new URLSearchParams({ 416 format, 417 signature, 418 chainId: chainId.toString(), 419 }); 420 421 if (addressOverride) { 422 query.set('addressOverride', addressOverride); 423 } 424 425 const res = await authenticatedRpgfServerCall( 426 `/rounds/${roundSlug}/ballots/spreadsheet?${query.toString()}`, 427 'POST', 428 data, 429 f, 430 true, 431 false, 432 SPREADSHEET_CONTENT_TYPE[format], 433 ); 434 435 const parsed = wrappedBallotSchema.parse(await res.json()); 436 return parsed; 437 } 438 439 export async function castBallotAsCsv( 440 f = fetch, 441 roundSlug: string, 442 data: string, 443 addressOverride?: string, 444 ): Promise<WrappedBallot> { 445 return submitSpreadsheetBallot(f, roundSlug, data, 'csv', addressOverride); 446 } 447 448 export async function castBallotAsXlsx( 449 f = fetch, 450 roundSlug: string, 451 data: ArrayBuffer, 452 addressOverride?: string, 453 ): Promise<WrappedBallot> { 454 return submitSpreadsheetBallot(f, roundSlug, data, 'xlsx', addressOverride); 455 } 456 457 async function importResultsSpreadsheet( 458 f = fetch, 459 roundId: string, 460 data: SpreadsheetBody, 461 format: SpreadsheetFormat, 462 ): Promise<void> { 463 await authenticatedRpgfServerCall( 464 `/rounds/${roundId}/results/import?format=${format}`, 465 'POST', 466 data, 467 f, 468 true, 469 false, 470 SPREADSHEET_CONTENT_TYPE[format], 471 ); 472 } 473 474 export async function importResultsAsCsv(f = fetch, roundId: string, data: string): Promise<void> { 475 return importResultsSpreadsheet(f, roundId, data, 'csv'); 476 } 477 478 export async function importResultsAsXlsx( 479 f = fetch, 480 roundId: string, 481 data: ArrayBuffer, 482 ): Promise<void> { 483 return importResultsSpreadsheet(f, roundId, data, 'xlsx'); 484 } 485 486 export async function getOwnBallot(f = fetch, roundSlug: string): Promise<WrappedBallot | null> { 487 const res = await authenticatedRpgfServerCall( 488 `/rounds/${roundSlug}/ballots/own`, 489 'GET', 490 undefined, 491 f, 492 ); 493 494 if (res.status === 404) { 495 return null; 496 } 497 498 const parsed = wrappedBallotSchema.parse(await res.json()); 499 return parsed; 500 } 501 502 export async function getBallots(f = fetch, roundSlug: string): Promise<WrappedBallot[]> { 503 const res = await authenticatedRpgfServerCall( 504 // TODO(rpgf) pagination 505 `/rounds/${roundSlug}/ballots?limit=1000`, 506 'GET', 507 undefined, 508 f, 509 ); 510 511 const parsed = wrappedBallotSchema.array().parse(await res.json()); 512 return parsed; 513 } 514 515 export async function getBallotsCsv(f = fetch, roundSlug: string): Promise<string> { 516 const res = await authenticatedRpgfServerCall( 517 `/rounds/${roundSlug}/ballots?format=csv&limit=1000`, 518 'GET', 519 undefined, 520 f, 521 ); 522 523 if (!res.ok) { 524 throw new Error(`${res.status} - Failed to fetch ballots CSV: ${res.statusText}`); 525 } 526 527 return await res.text(); 528 } 529 530 export async function getBallotStats( 531 f = fetch, 532 roundSlug: string, 533 ): Promise<{ 534 numberOfVoters: number; 535 numberOfBallots: number; 536 }> { 537 const res = await authenticatedRpgfServerCall( 538 `/rounds/${roundSlug}/ballots/stats`, 539 'GET', 540 undefined, 541 f, 542 ); 543 544 if (!res.ok) { 545 throw new Error(`${res.status} - Failed to fetch ballot stats: ${res.statusText}`); 546 } 547 548 const data = await res.json(); 549 550 return z 551 .object({ 552 numberOfVoters: z.number(), 553 numberOfBallots: z.number(), 554 }) 555 .parse(data); 556 } 557 558 export async function recalculateResults( 559 f = fetch, 560 roundId: string, 561 method: 'avg' | 'median' | 'sum', 562 ): Promise<void> { 563 await authenticatedRpgfServerCall( 564 `/rounds/${roundId}/results/recalculate?method=${method}`, 565 'POST', 566 undefined, 567 f, 568 ); 569 570 return; 571 } 572 573 export async function publishResults(f = fetch, roundId: string): Promise<void> { 574 await authenticatedRpgfServerCall(`/rounds/${roundId}/results/publish`, 'POST', undefined, f); 575 576 return; 577 } 578 579 export async function getDripListWeightsForRound( 580 f = fetch, 581 roundId: string, 582 ): Promise<Record<string, number>> { 583 const res = await authenticatedRpgfServerCall( 584 `/rounds/${roundId}/results/drip-list-weights`, 585 'GET', 586 undefined, 587 f, 588 ); 589 590 const parsed = z.record(z.string(), z.number()).parse(await res.json()); 591 return parsed; 592 } 593 594 export async function linkDripListsToRound( 595 f = fetch, 596 roundId: string, 597 dripListAccountIds: string[], 598 ): Promise<void> { 599 await authenticatedRpgfServerCall( 600 `/rounds/${roundId}/drip-lists`, 601 'PATCH', 602 { 603 dripListAccountIds, 604 }, 605 f, 606 ); 607 608 return; 609 } 610 611 export async function getOwnUserData(f = fetch): Promise<{ 612 walletAddress: string; 613 id: string; 614 whitelisted: boolean; 615 } | null> { 616 const res = await authenticatedRpgfServerCall( 617 `/users/me?chainId=${network.chainId}`, 618 'GET', 619 undefined, 620 f, 621 ); 622 623 if (res.status === 401) { 624 return null; 625 } 626 627 return z 628 .object({ 629 walletAddress: z.string(), 630 id: z.string(), 631 whitelisted: z.boolean(), 632 }) 633 .parse(await res.json()); 634 } 635 636 export async function getRoundAdmins(f = fetch, roundId: string): Promise<RpgfUser[]> { 637 const res = await authenticatedRpgfServerCall(`/rounds/${roundId}/admins`, 'GET', undefined, f); 638 639 return userSchema.array().parse(await res.json()); 640 } 641 642 export async function setRoundAdmins( 643 f = fetch, 644 roundId: string, 645 walletAddresses: string[], 646 ): Promise<RpgfUser[]> { 647 const res = await authenticatedRpgfServerCall( 648 `/rounds/${roundId}/admins`, 649 'PUT', 650 { 651 walletAddresses, 652 }, 653 f, 654 ); 655 656 return userSchema.array().parse(await res.json()); 657 } 658 659 export async function getRoundVoters(f = fetch, roundId: string): Promise<RpgfUser[]> { 660 const res = await authenticatedRpgfServerCall(`/rounds/${roundId}/voters`, 'GET', undefined, f); 661 662 return userSchema.array().parse(await res.json()); 663 } 664 665 export async function setRoundVoters( 666 f = fetch, 667 roundId: string, 668 walletAddresses: string[], 669 ): Promise<RpgfUser[]> { 670 const res = await authenticatedRpgfServerCall( 671 `/rounds/${roundId}/voters`, 672 'PUT', 673 { 674 walletAddresses, 675 }, 676 f, 677 ); 678 679 return userSchema.array().parse(await res.json()); 680 } 681 682 export async function getApplicationCategories( 683 f = fetch, 684 roundId: string, 685 ): Promise<ApplicationCategory[]> { 686 const res = await authenticatedRpgfServerCall( 687 `/rounds/${roundId}/application-categories`, 688 'GET', 689 undefined, 690 f, 691 ); 692 693 return applicationCategorySchema.array().parse(await res.json()); 694 } 695 696 export async function createApplicationCategory( 697 f = fetch, 698 roundId: string, 699 dto: CreateApplicationCategoryDto, 700 ): Promise<ApplicationCategory> { 701 const res = await authenticatedRpgfServerCall( 702 `/rounds/${roundId}/application-categories`, 703 'PUT', 704 dto, 705 f, 706 ); 707 708 return applicationCategorySchema.parse(await res.json()); 709 } 710 711 export async function deleteApplicationCategory( 712 f = fetch, 713 roundId: string, 714 categoryId: string, 715 ): Promise<void> { 716 await authenticatedRpgfServerCall( 717 `/rounds/${roundId}/application-categories/${categoryId}`, 718 'DELETE', 719 undefined, 720 f, 721 ); 722 723 return; 724 } 725 726 export async function getApplicationForms(f = fetch, roundId: string): Promise<ApplicationForm[]> { 727 const res = await authenticatedRpgfServerCall( 728 `/rounds/${roundId}/application-forms`, 729 'GET', 730 undefined, 731 f, 732 ); 733 734 return applicationFormSchema.array().parse(await res.json()); 735 } 736 737 export async function getApplicationForm( 738 f = fetch, 739 roundId: string, 740 formId: string, 741 ): Promise<ApplicationForm | null> { 742 const res = await authenticatedRpgfServerCall( 743 `/rounds/${roundId}/application-forms/${formId}`, 744 'GET', 745 undefined, 746 f, 747 ); 748 749 if (res.status === 404) { 750 return null; 751 } 752 753 return applicationFormSchema.parse(await res.json()); 754 } 755 756 export async function createApplicationForm( 757 f = fetch, 758 roundId: string, 759 dto: CreateApplicationFormDto, 760 ): Promise<ApplicationForm> { 761 const res = await authenticatedRpgfServerCall( 762 `/rounds/${roundId}/application-forms`, 763 'PUT', 764 dto, 765 f, 766 ); 767 768 return applicationFormSchema.parse(await res.json()); 769 } 770 771 export async function deleteApplicationForm( 772 f = fetch, 773 roundId: string, 774 formId: string, 775 ): Promise<void> { 776 await authenticatedRpgfServerCall( 777 `/rounds/${roundId}/application-forms/${formId}`, 778 'DELETE', 779 undefined, 780 f, 781 ); 782 783 return; 784 } 785 786 export async function updateApplicationForm( 787 f = fetch, 788 roundId: string, 789 formId: string, 790 dto: CreateApplicationFormDto, 791 ): Promise<ApplicationForm> { 792 const res = await authenticatedRpgfServerCall( 793 `/rounds/${roundId}/application-forms/${formId}`, 794 'PATCH', 795 dto, 796 f, 797 ); 798 799 return applicationFormSchema.parse(await res.json()); 800 } 801 802 export async function getAuditLog( 803 f = fetch, 804 roundId: string, 805 ): Promise<{ 806 logs: AuditLog[]; 807 }> { 808 const res = await authenticatedRpgfServerCall( 809 // No pagination for now, fetch up to 1000 entries 810 `/rounds/${roundId}/audit-logs?limit=1000`, 811 'GET', 812 undefined, 813 f, 814 ); 815 816 return z 817 .object({ 818 logs: auditLogSchema.array(), 819 }) 820 .parse(await res.json()); 821 } 822 823 export async function createKycRequestForApplication( 824 f = fetch, 825 applicationId: string, 826 dto: CreateKycRequestForApplicationDto, 827 ): Promise<KycRequest> { 828 const res = await authenticatedRpgfServerCall( 829 `/kyc/applications/${applicationId}/request`, 830 'POST', 831 dto, 832 f, 833 ); 834 835 return kycRequestSchema.parse(await res.json()); 836 } 837 838 export async function getKycRequestForApplication( 839 f = fetch, 840 applicationId: string, 841 ): Promise<KycRequest | null> { 842 const res = await authenticatedRpgfServerCall( 843 `/kyc/applications/${applicationId}/request`, 844 'GET', 845 undefined, 846 f, 847 ); 848 849 if (res.status === 404) { 850 return null; 851 } 852 853 return kycRequestSchema.parse(await res.json()); 854 } 855 856 export async function getKycRequestsForRound(f = fetch, roundId: string): Promise<KycRequest[]> { 857 const res = await authenticatedRpgfServerCall( 858 `/kyc/rounds/${roundId}/requests`, 859 'GET', 860 undefined, 861 f, 862 ); 863 864 return kycRequestSchema.array().parse(await res.json()); 865 } 866 867 export async function linkExistingKycRequestToApplication( 868 f = fetch, 869 applicationId: string, 870 kycRequestId: string, 871 ): Promise<void> { 872 await authenticatedRpgfServerCall( 873 `/kyc/applications/${applicationId}/link-existing`, 874 'POST', 875 { kycRequestId }, 876 f, 877 ); 878 879 return; 880 } 881 882 export async function getCustomDatasetsForRound( 883 f = fetch, 884 roundId: string, 885 ): Promise<CustomDataset[]> { 886 const res = await authenticatedRpgfServerCall( 887 `/rounds/${roundId}/custom-datasets`, 888 'GET', 889 undefined, 890 f, 891 ); 892 893 return customDatasetSchema.array().parse(await res.json()); 894 } 895 896 export async function createCustomDataset( 897 f = fetch, 898 roundId: string, 899 name: string, 900 ): Promise<CustomDataset> { 901 const res = await authenticatedRpgfServerCall( 902 `/rounds/${roundId}/custom-datasets`, 903 'PUT', 904 { name, isPublic: false }, 905 f, 906 ); 907 908 return customDatasetSchema.parse(await res.json()); 909 } 910 911 export async function updateCustomDataset( 912 f = fetch, 913 roundId: string, 914 datasetId: string, 915 name: string, 916 isPublic: boolean, 917 ): Promise<CustomDataset> { 918 const res = await authenticatedRpgfServerCall( 919 `/rounds/${roundId}/custom-datasets/${datasetId}`, 920 'PATCH', 921 { name, isPublic }, 922 f, 923 ); 924 925 return customDatasetSchema.parse(await res.json()); 926 } 927 928 export async function deleteCustomDataset( 929 f = fetch, 930 roundId: string, 931 datasetId: string, 932 ): Promise<void> { 933 await authenticatedRpgfServerCall( 934 `/rounds/${roundId}/custom-datasets/${datasetId}`, 935 'DELETE', 936 undefined, 937 f, 938 ); 939 940 return; 941 } 942 943 export async function uploadCustomDatasetCsv( 944 f = fetch, 945 roundId: string, 946 datasetId: string, 947 csvData: string, 948 ): Promise<CustomDataset | { error: string }> { 949 const res = await authenticatedRpgfServerCall( 950 `/rounds/${roundId}/custom-datasets/${datasetId}/upload`, 951 'POST', 952 csvData, 953 f, 954 true, 955 true, 956 'text/csv', 957 ); 958 959 return customDatasetSchema 960 .or( 961 z.object({ 962 error: z.string(), 963 }), 964 ) 965 .parse(await res.json()); 966 }