From ad3827c45b081174b9a059ab949cf77a9430ac4a Mon Sep 17 00:00:00 2001 From: Victoria Zotova Date: Thu, 18 Jul 2024 13:48:02 -0400 Subject: [PATCH 1/6] PorterClient: multiple porter uris with check --- packages/shared/src/porter.ts | 63 ++++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 12 deletions(-) diff --git a/packages/shared/src/porter.ts b/packages/shared/src/porter.ts index 7d78b5e65..9c3f5f456 100644 --- a/packages/shared/src/porter.ts +++ b/packages/shared/src/porter.ts @@ -6,7 +6,11 @@ import { RetrievalKit, TreasureMap, } from '@nucypher/nucypher-core'; -import axios, { AxiosResponse } from 'axios'; +import axios, { + AxiosRequestConfig, + AxiosResponse, + HttpStatusCode, +} from 'axios'; import qs from 'qs'; import { Base64EncodedBytes, ChecksumAddress, HexEncodedBytes } from './types'; @@ -120,10 +124,47 @@ export type TacoDecryptResult = { }; export class PorterClient { - readonly porterUrl: URL; + readonly porterUrls: URL[]; - constructor(porterUri: string) { - this.porterUrl = new URL(porterUri); + constructor(porterUris: string | [string, ...[string]]) { + if (porterUris instanceof Array) { + this.porterUrls = porterUris.map((uri) => new URL(uri)); + } else { + this.porterUrls = [new URL(porterUris)]; + } + } + + protected async tryAndGet( + path: string, + config?: AxiosRequestConfig, + ): Promise> { + let resp!: AxiosResponse; + for (const porterUrl of this.porterUrls) { + resp = await axios.get(new URL(path, porterUrl).toString(), config); + if (resp.status === HttpStatusCode.Ok) { + return resp; + } + } + return resp; + } + + protected async tryAndPost( + path: string, + data?: D, + config?: AxiosRequestConfig, + ): Promise> { + let resp!: AxiosResponse; + for (const porterUrl of this.porterUrls) { + resp = await axios.post( + new URL(path, porterUrl).toString(), + data, + config, + ); + if (resp.status === HttpStatusCode.Ok) { + return resp; + } + } + return resp; } public async getUrsulas( @@ -136,8 +177,8 @@ export class PorterClient { exclude_ursulas: excludeUrsulas, include_ursulas: includeUrsulas, }; - const resp: AxiosResponse = await axios.get( - new URL('/get_ursulas', this.porterUrl).toString(), + const resp: AxiosResponse = await this.tryAndGet( + '/get_ursulas', { params, paramsSerializer: (params) => { @@ -170,10 +211,8 @@ export class PorterClient { bob_verifying_key: toHexString(bobVerifyingKey.toCompressedBytes()), context: conditionContextJSON, }; - const resp: AxiosResponse = await axios.post( - new URL('/retrieve_cfrags', this.porterUrl).toString(), - data, - ); + const resp: AxiosResponse = + await this.tryAndPost('/retrieve_cfrags', data); return resp.data.result.retrieval_results.map(({ cfrags, errors }) => { const parsed = Object.keys(cfrags).map((address) => [ @@ -198,8 +237,8 @@ export class PorterClient { ), threshold, }; - const resp: AxiosResponse = await axios.post( - new URL('/decrypt', this.porterUrl).toString(), + const resp: AxiosResponse = await this.tryAndPost( + '/decrypt', data, ); From b3d80cfe5165df4c279c65527cfb3a2c47385de8 Mon Sep 17 00:00:00 2001 From: Victoria Zotova Date: Thu, 18 Jul 2024 15:07:09 -0400 Subject: [PATCH 2/6] PorterClient: combine default and specified porter URI --- demos/taco-demo/src/App.tsx | 3 +-- demos/taco-nft-demo/src/App.tsx | 5 ++--- examples/taco/nextjs/src/hooks/useTaco.ts | 2 -- examples/taco/nodejs/src/index.ts | 2 -- examples/taco/react/src/hooks/useTaco.ts | 2 -- examples/taco/webpack-5/src/index.ts | 3 +-- packages/shared/src/porter.ts | 19 ++++++++++++++----- packages/taco/examples/encrypt-decrypt.ts | 2 -- packages/taco/src/index.ts | 2 +- packages/taco/src/taco.ts | 8 +++----- packages/taco/src/tdec.ts | 8 ++++---- 11 files changed, 26 insertions(+), 30 deletions(-) diff --git a/demos/taco-demo/src/App.tsx b/demos/taco-demo/src/App.tsx index 2aa5f07a8..b13b8497e 100644 --- a/demos/taco-demo/src/App.tsx +++ b/demos/taco-demo/src/App.tsx @@ -3,7 +3,6 @@ import { decrypt, domains, encrypt, - getPorterUri, initialize, ThresholdMessageKit, toHexString, @@ -89,7 +88,7 @@ export default function App() { provider, domain, encryptedMessage, - getPorterUri(domain), + undefined, provider.getSigner(), ); diff --git a/demos/taco-nft-demo/src/App.tsx b/demos/taco-nft-demo/src/App.tsx index 48f934dc0..191276709 100644 --- a/demos/taco-nft-demo/src/App.tsx +++ b/demos/taco-nft-demo/src/App.tsx @@ -3,7 +3,6 @@ import { decrypt, domains, encrypt, - getPorterUri, initialize, ThresholdMessageKit, } from '@nucypher/taco'; @@ -78,8 +77,8 @@ export default function App() { provider, domain, encryptedMessage, - getPorterUri(domain), - provider.getSigner(), + undefined, + provider.getSigner() ); setDecryptedMessage(new TextDecoder().decode(decryptedMessage)); diff --git a/examples/taco/nextjs/src/hooks/useTaco.ts b/examples/taco/nextjs/src/hooks/useTaco.ts index 81ff0dc5d..cc2996bf8 100644 --- a/examples/taco/nextjs/src/hooks/useTaco.ts +++ b/examples/taco/nextjs/src/hooks/useTaco.ts @@ -4,7 +4,6 @@ import { Domain, EIP4361AuthProvider, encrypt, - getPorterUri, initialize, ThresholdMessageKit, } from '@nucypher/taco'; @@ -38,7 +37,6 @@ export default function useTaco({ domain, messageKit, authProvider, - getPorterUri(domain), ); }, [isInit, provider, domain], diff --git a/examples/taco/nodejs/src/index.ts b/examples/taco/nodejs/src/index.ts index 212906102..89b8bf25d 100644 --- a/examples/taco/nodejs/src/index.ts +++ b/examples/taco/nodejs/src/index.ts @@ -7,7 +7,6 @@ import { EIP4361AuthProvider, encrypt, fromBytes, - getPorterUri, initialize, isAuthorized, ThresholdMessageKit, @@ -119,7 +118,6 @@ const decryptFromBytes = async (encryptedBytes: Uint8Array) => { domain, messageKit, authProvider, - getPorterUri(domain), ); }; diff --git a/examples/taco/react/src/hooks/useTaco.ts b/examples/taco/react/src/hooks/useTaco.ts index 81ff0dc5d..cc2996bf8 100644 --- a/examples/taco/react/src/hooks/useTaco.ts +++ b/examples/taco/react/src/hooks/useTaco.ts @@ -4,7 +4,6 @@ import { Domain, EIP4361AuthProvider, encrypt, - getPorterUri, initialize, ThresholdMessageKit, } from '@nucypher/taco'; @@ -38,7 +37,6 @@ export default function useTaco({ domain, messageKit, authProvider, - getPorterUri(domain), ); }, [isInit, provider, domain], diff --git a/examples/taco/webpack-5/src/index.ts b/examples/taco/webpack-5/src/index.ts index 740153f5d..e372053ab 100644 --- a/examples/taco/webpack-5/src/index.ts +++ b/examples/taco/webpack-5/src/index.ts @@ -5,7 +5,7 @@ import { EIP4361AuthProvider, encrypt, fromBytes, - getPorterUri, + getPorterUris, initialize, toBytes, } from '@nucypher/taco'; @@ -67,7 +67,6 @@ const runExample = async () => { domain, messageKit, authProvider, - getPorterUri(domain), ); const decryptedMessage = fromBytes(decryptedBytes); console.log('Decrypted message:', decryptedMessage); diff --git a/packages/shared/src/porter.ts b/packages/shared/src/porter.ts index 9c3f5f456..f1b0cf3ad 100644 --- a/packages/shared/src/porter.ts +++ b/packages/shared/src/porter.ts @@ -16,14 +16,14 @@ import qs from 'qs'; import { Base64EncodedBytes, ChecksumAddress, HexEncodedBytes } from './types'; import { fromBase64, fromHexString, toBase64, toHexString } from './utils'; -const porterUri: Record = { +const defaultPorterUri: Record = { mainnet: 'https://porter.nucypher.community', tapir: 'https://porter-tapir.nucypher.community', oryx: 'https://porter-oryx.nucypher.community', lynx: 'https://porter-lynx.nucypher.community', }; -export type Domain = keyof typeof porterUri; +export type Domain = keyof typeof defaultPorterUri; export const domains: Record = { DEVNET: 'lynx', @@ -32,11 +32,20 @@ export const domains: Record = { }; export const getPorterUri = (domain: Domain): string => { - const uri = porterUri[domain]; + return getPorterUris(domain)[0]; +}; + +export const getPorterUris = (domain: Domain, porterUri?: string): string[] => { + const porterUris: string[] = []; + if (porterUri) { + porterUris.push(porterUri); + } + const uri = defaultPorterUri[domain]; if (!uri) { throw new Error(`No default Porter URI found for domain: ${domain}`); } - return porterUri[domain]; + porterUris.push(uri); + return porterUris; }; // /get_ursulas @@ -126,7 +135,7 @@ export type TacoDecryptResult = { export class PorterClient { readonly porterUrls: URL[]; - constructor(porterUris: string | [string, ...[string]]) { + constructor(porterUris: string | string[]) { if (porterUris instanceof Array) { this.porterUrls = porterUris.map((uri) => new URL(uri)); } else { diff --git a/packages/taco/examples/encrypt-decrypt.ts b/packages/taco/examples/encrypt-decrypt.ts index 8d6b7f0f6..a039efe7b 100644 --- a/packages/taco/examples/encrypt-decrypt.ts +++ b/packages/taco/examples/encrypt-decrypt.ts @@ -7,7 +7,6 @@ import { domains, EIP4361AuthProvider, encrypt, - getPorterUri, initialize, ThresholdMessageKit, toBytes, @@ -55,7 +54,6 @@ const run = async () => { domains.TESTNET, messageKit, authProvider, - getPorterUri(domains.TESTNET), ); return decryptedMessage; }; diff --git a/packages/taco/src/index.ts b/packages/taco/src/index.ts index 8baccdad7..315229937 100644 --- a/packages/taco/src/index.ts +++ b/packages/taco/src/index.ts @@ -3,7 +3,7 @@ export { Domain, domains, fromBytes, - getPorterUri, + getPorterUris, initialize, toBytes, toHexString, diff --git a/packages/taco/src/taco.ts b/packages/taco/src/taco.ts index 28f629567..7641dfe50 100644 --- a/packages/taco/src/taco.ts +++ b/packages/taco/src/taco.ts @@ -9,7 +9,7 @@ import { DkgCoordinatorAgent, Domain, fromHexString, - getPorterUri, + getPorterUris, GlobalAllowListAgent, toBytes, } from '@nucypher/shared'; @@ -148,9 +148,7 @@ export const decrypt = async ( porterUri?: string, customParameters?: Record, ): Promise => { - if (!porterUri) { - porterUri = getPorterUri(domain); - } + const porterUris: string[] = getPorterUris(domain, porterUri); const ritualId = await DkgCoordinatorAgent.getRitualIdFromPublicKey( provider, @@ -166,7 +164,7 @@ export const decrypt = async ( return retrieveAndDecrypt( provider, domain, - porterUri, + porterUris, messageKit, ritualId, ritual.sharesNum, diff --git a/packages/taco/src/tdec.ts b/packages/taco/src/tdec.ts index cfe516519..8c7008b71 100644 --- a/packages/taco/src/tdec.ts +++ b/packages/taco/src/tdec.ts @@ -61,7 +61,7 @@ export const encryptMessage = async ( export const retrieveAndDecrypt = async ( provider: ethers.providers.Provider, domain: Domain, - porterUri: string, + porterUris: string[], thresholdMessageKit: ThresholdMessageKit, ritualId: number, sharesNum: number, @@ -72,7 +72,7 @@ export const retrieveAndDecrypt = async ( const decryptionShares = await retrieve( provider, domain, - porterUri, + porterUris, thresholdMessageKit, ritualId, sharesNum, @@ -88,7 +88,7 @@ export const retrieveAndDecrypt = async ( const retrieve = async ( provider: ethers.providers.Provider, domain: Domain, - porterUri: string, + porterUris: string[], thresholdMessageKit: ThresholdMessageKit, ritualId: number, sharesNum: number, @@ -114,7 +114,7 @@ const retrieve = async ( thresholdMessageKit, ); - const porter = new PorterClient(porterUri); + const porter = new PorterClient(porterUris); const { encryptedResponses, errors } = await porter.tacoDecrypt( encryptedRequests, threshold, From 57f20c31b401e4a86953896e7bc4b5763c6d594a Mon Sep 17 00:00:00 2001 From: Victoria Zotova Date: Fri, 19 Jul 2024 14:37:41 -0400 Subject: [PATCH 3/6] PorterClient: more generic way to use axios --- packages/shared/src/porter.ts | 57 ++++++++++++-------------------- packages/test-utils/src/utils.ts | 35 ++++++++++---------- 2 files changed, 38 insertions(+), 54 deletions(-) diff --git a/packages/shared/src/porter.ts b/packages/shared/src/porter.ts index f1b0cf3ad..c9432d0db 100644 --- a/packages/shared/src/porter.ts +++ b/packages/shared/src/porter.ts @@ -143,32 +143,11 @@ export class PorterClient { } } - protected async tryAndGet( - path: string, - config?: AxiosRequestConfig, - ): Promise> { + protected async tryAndCall(config: AxiosRequestConfig): Promise> { let resp!: AxiosResponse; for (const porterUrl of this.porterUrls) { - resp = await axios.get(new URL(path, porterUrl).toString(), config); - if (resp.status === HttpStatusCode.Ok) { - return resp; - } - } - return resp; - } - - protected async tryAndPost( - path: string, - data?: D, - config?: AxiosRequestConfig, - ): Promise> { - let resp!: AxiosResponse; - for (const porterUrl of this.porterUrls) { - resp = await axios.post( - new URL(path, porterUrl).toString(), - data, - config, - ); + config.baseURL = porterUrl.toString(); + resp = await axios.request(config); if (resp.status === HttpStatusCode.Ok) { return resp; } @@ -186,14 +165,14 @@ export class PorterClient { exclude_ursulas: excludeUrsulas, include_ursulas: includeUrsulas, }; - const resp: AxiosResponse = await this.tryAndGet( - '/get_ursulas', - { - params, - paramsSerializer: (params) => { - return qs.stringify(params, { arrayFormat: 'comma' }); - }, + const resp: AxiosResponse = await this.tryAndCall({ + url: '/get_ursulas', + method: 'get', + params: params, + paramsSerializer: (params) => { + return qs.stringify(params, { arrayFormat: 'comma' }); }, + }, ); return resp.data.result.ursulas.map((u: UrsulaResponse) => ({ checksumAddress: u.checksum_address, @@ -221,7 +200,11 @@ export class PorterClient { context: conditionContextJSON, }; const resp: AxiosResponse = - await this.tryAndPost('/retrieve_cfrags', data); + await this.tryAndCall({ + url: '/retrieve_cfrags', + method: 'post', + data: data + }); return resp.data.result.retrieval_results.map(({ cfrags, errors }) => { const parsed = Object.keys(cfrags).map((address) => [ @@ -246,10 +229,12 @@ export class PorterClient { ), threshold, }; - const resp: AxiosResponse = await this.tryAndPost( - '/decrypt', - data, - ); + const resp: AxiosResponse = + await this.tryAndCall({ + url: '/decrypt', + method: 'post', + data: data + }); const { encrypted_decryption_responses, errors } = resp.data.result.decryption_results; diff --git a/packages/test-utils/src/utils.ts b/packages/test-utils/src/utils.ts index 9a52848eb..06095b125 100644 --- a/packages/test-utils/src/utils.ts +++ b/packages/test-utils/src/utils.ts @@ -40,7 +40,6 @@ import { zip, } from '@nucypher/shared'; import { EIP4361_AUTH_METHOD, EIP4361AuthProvider } from '@nucypher/taco-auth'; -import axios from 'axios'; import { ethers, providers, Wallet } from 'ethers'; import { expect, SpyInstance, vi } from 'vitest'; @@ -133,23 +132,23 @@ export const fakeUrsulas = (n = 4): Ursula[] => export const mockGetUrsulas = ( ursulas: Ursula[] = fakeUrsulas(), ): SpyInstance => { - const fakePorterUrsulas = ( - mockUrsulas: readonly Ursula[], - ): GetUrsulasResult => { - return { - result: { - ursulas: mockUrsulas.map(({ encryptingKey, uri, checksumAddress }) => ({ - encrypting_key: toHexString(encryptingKey.toCompressedBytes()), - uri: uri, - checksum_address: checksumAddress, - })), - }, - version: '5.2.0', - }; - }; - - return vi.spyOn(axios, 'get').mockImplementation(async () => { - return Promise.resolve({ data: fakePorterUrsulas(ursulas) }); + // const fakePorterUrsulas = ( + // mockUrsulas: readonly Ursula[], + // ): GetUrsulasResult => { + // return { + // result: { + // ursulas: mockUrsulas.map(({ encryptingKey, uri, checksumAddress }) => ({ + // encrypting_key: toHexString(encryptingKey.toCompressedBytes()), + // uri: uri, + // checksum_address: checksumAddress, + // })), + // }, + // version: '5.2.0', + // }; + // }; + + return vi.spyOn(PorterClient.prototype, 'getUrsulas').mockImplementation(async () => { + return Promise.resolve(ursulas); }); }; From db8eb2f6d1a389723df4b0b84c1ec8f3e68af8b4 Mon Sep 17 00:00:00 2001 From: Victoria Zotova Date: Wed, 24 Jul 2024 16:52:47 -0400 Subject: [PATCH 4/6] PorterClient: test and fix for 'tryAndCall' --- packages/shared/package.json | 1 + packages/shared/src/porter.ts | 15 ++++- packages/shared/test/porter.test.ts | 91 +++++++++++++++++++++++++++++ packages/shared/tsconfig.json | 5 ++ packages/test-utils/src/utils.ts | 17 ------ pnpm-lock.yaml | 3 + 6 files changed, 112 insertions(+), 20 deletions(-) create mode 100644 packages/shared/test/porter.test.ts diff --git a/packages/shared/package.json b/packages/shared/package.json index 58ca8c0e0..f08d2401d 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -51,6 +51,7 @@ "zod": "*" }, "devDependencies": { + "@nucypher/test-utils": "workspace:*", "@typechain/ethers-v5": "^11.1.2", "@types/deep-equal": "^1.0.3", "@types/qs": "^6.9.15", diff --git a/packages/shared/src/porter.ts b/packages/shared/src/porter.ts index c9432d0db..0d6d5d16c 100644 --- a/packages/shared/src/porter.ts +++ b/packages/shared/src/porter.ts @@ -145,14 +145,23 @@ export class PorterClient { protected async tryAndCall(config: AxiosRequestConfig): Promise> { let resp!: AxiosResponse; + let lastError = undefined; for (const porterUrl of this.porterUrls) { - config.baseURL = porterUrl.toString(); - resp = await axios.request(config); + const localConfig = { ...config, baseURL: porterUrl.toString() } + try { + resp = await axios.request(localConfig); + } catch (e) { + lastError = e; + continue; + } if (resp.status === HttpStatusCode.Ok) { return resp; } } - return resp; + if (lastError !== undefined) { + throw lastError; + } + throw new Error("Porter returns bad response"); } public async getUrsulas( diff --git a/packages/shared/test/porter.test.ts b/packages/shared/test/porter.test.ts new file mode 100644 index 000000000..113f3012a --- /dev/null +++ b/packages/shared/test/porter.test.ts @@ -0,0 +1,91 @@ +import { + initialize, + GetUrsulasResult, + PorterClient, + toHexString, + Ursula, +} from '../src'; +import { fakeUrsulas } from '@nucypher/test-utils'; +import { beforeAll, describe, expect, SpyInstance, it, vi } from 'vitest'; +import axios, { HttpStatusCode } from 'axios'; + +const fakePorterUris = ['https://_this_should_crash.com/', 'https://2_this_should_crash.com/', 'https://_this_should_work.com/']; + +const mockGetUrsulas = ( + ursulas: Ursula[] = fakeUrsulas(), +): SpyInstance => { + const fakePorterUrsulas = ( + mockUrsulas: readonly Ursula[], + ): GetUrsulasResult => { + return { + result: { + ursulas: mockUrsulas.map(({ encryptingKey, uri, checksumAddress }) => ({ + encrypting_key: toHexString(encryptingKey.toCompressedBytes()), + uri: uri, + checksum_address: checksumAddress, + })), + }, + version: '5.2.0', + }; + }; + + return vi.spyOn(axios, 'request').mockImplementation(async (config) => { + switch (config.baseURL) { + case fakePorterUris[2]: + return Promise.resolve({ status: HttpStatusCode.Ok, data: fakePorterUrsulas(ursulas) }); + case fakePorterUris[0]: + throw new Error(); + default: + throw Promise.resolve({ status: HttpStatusCode.BadRequest }); + } + }); +}; + +describe('PorterClient', () => { + beforeAll(async () => { + await initialize(); + }); + + it('Get Ursulas', async () => { + const ursulas = fakeUrsulas(); + const getUrsulasSpy = mockGetUrsulas(ursulas); + const porterClient = new PorterClient(fakePorterUris); + const result = await porterClient.getUrsulas(ursulas.length); + + expect(result.every((u: Ursula, index: number) => { + const expectedUrsula = ursulas[index]; + return u.checksumAddress === expectedUrsula.checksumAddress && + u.uri === expectedUrsula.uri && + u.encryptingKey.equals(expectedUrsula.encryptingKey); + })).toBeTruthy(); + const params = { + method: 'get', + url: "/get_ursulas", + params: { + exclude_ursulas: [], + include_ursulas: [], + quantity: ursulas.length, + } + }; + + expect(getUrsulasSpy).toBeCalledTimes(fakePorterUris.length); + expect(getUrsulasSpy).toHaveBeenNthCalledWith(1, expect.objectContaining( + { ...params, baseURL: fakePorterUris[0] }) + ); + expect(getUrsulasSpy).toHaveBeenNthCalledWith(2, expect.objectContaining( + { ...params, baseURL: fakePorterUris[1] }) + ); + expect(getUrsulasSpy).toHaveBeenNthCalledWith(3, expect.objectContaining( + { ...params, baseURL: fakePorterUris[2] })); + }); + + it('returns error in case all porters fail', async () => { + const ursulas = fakeUrsulas(); + mockGetUrsulas(ursulas); + let porterClient = new PorterClient([fakePorterUris[0], fakePorterUris[1]]); + expect(porterClient.getUrsulas(ursulas.length)).rejects.toThrowError(); + porterClient = new PorterClient([fakePorterUris[1], fakePorterUris[0]]); + expect(porterClient.getUrsulas(ursulas.length)).rejects.toThrowError(); + }); + +}); diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json index 0c3f3ec10..e17152bcf 100644 --- a/packages/shared/tsconfig.json +++ b/packages/shared/tsconfig.json @@ -6,4 +6,9 @@ "skipLibCheck": true, "resolveJsonModule": true, }, + "references": [ + { + "path": "../test-utils/tsconfig.es.json", + }, + ], } diff --git a/packages/test-utils/src/utils.ts b/packages/test-utils/src/utils.ts index 06095b125..07ed9dc1c 100644 --- a/packages/test-utils/src/utils.ts +++ b/packages/test-utils/src/utils.ts @@ -31,11 +31,9 @@ import { import { ChecksumAddress, DkgCoordinatorAgent, - GetUrsulasResult, PorterClient, RetrieveCFragsResult, TacoDecryptResult, - toHexString, Ursula, zip, } from '@nucypher/shared'; @@ -132,21 +130,6 @@ export const fakeUrsulas = (n = 4): Ursula[] => export const mockGetUrsulas = ( ursulas: Ursula[] = fakeUrsulas(), ): SpyInstance => { - // const fakePorterUrsulas = ( - // mockUrsulas: readonly Ursula[], - // ): GetUrsulasResult => { - // return { - // result: { - // ursulas: mockUrsulas.map(({ encryptingKey, uri, checksumAddress }) => ({ - // encrypting_key: toHexString(encryptingKey.toCompressedBytes()), - // uri: uri, - // checksum_address: checksumAddress, - // })), - // }, - // version: '5.2.0', - // }; - // }; - return vi.spyOn(PorterClient.prototype, 'getUrsulas').mockImplementation(async () => { return Promise.resolve(ursulas); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a714b2441..3ba5006ed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -516,6 +516,9 @@ importers: specifier: '*' version: 3.23.8 devDependencies: + '@nucypher/test-utils': + specifier: workspace:* + version: link:../test-utils '@typechain/ethers-v5': specifier: ^11.1.2 version: 11.1.2(@ethersproject/abi@5.7.0)(@ethersproject/providers@5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.10))(ethers@5.7.2(bufferutil@4.0.8)(utf-8-validate@5.0.10))(typechain@8.3.2(typescript@5.4.5))(typescript@5.4.5) From cc62c2bce9701bacfd59ea6ec1f6b7444cacaac0 Mon Sep 17 00:00:00 2001 From: Victoria Zotova Date: Thu, 25 Jul 2024 12:09:07 -0400 Subject: [PATCH 5/6] Expand getPorterUris() --- packages/shared/src/porter.ts | 8 ++++++-- packages/taco/src/taco.ts | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/shared/src/porter.ts b/packages/shared/src/porter.ts index 0d6d5d16c..4dc054924 100644 --- a/packages/shared/src/porter.ts +++ b/packages/shared/src/porter.ts @@ -35,10 +35,14 @@ export const getPorterUri = (domain: Domain): string => { return getPorterUris(domain)[0]; }; -export const getPorterUris = (domain: Domain, porterUri?: string): string[] => { +export const getPorterUris = (domain: Domain, porterUri?: string | string[]): string[] => { const porterUris: string[] = []; if (porterUri) { - porterUris.push(porterUri); + if (porterUri instanceof Array) { + porterUris.push(...porterUri); + } else { + porterUris.push(porterUri); + } } const uri = defaultPorterUri[domain]; if (!uri) { diff --git a/packages/taco/src/taco.ts b/packages/taco/src/taco.ts index 7641dfe50..93d29b764 100644 --- a/packages/taco/src/taco.ts +++ b/packages/taco/src/taco.ts @@ -145,7 +145,7 @@ export const decrypt = async ( domain: Domain, messageKit: ThresholdMessageKit, authProvider?: EIP4361AuthProvider, - porterUri?: string, + porterUri?: string | string[], customParameters?: Record, ): Promise => { const porterUris: string[] = getPorterUris(domain, porterUri); From 197781548eae34b969eda2e283e08540c47baa3f Mon Sep 17 00:00:00 2001 From: Victoria Zotova Date: Thu, 25 Jul 2024 13:25:11 -0400 Subject: [PATCH 6/6] apply pr suggestions --- packages/shared/src/porter.ts | 45 ++++++++++----------- packages/shared/test/porter.test.ts | 62 ++++++++++++++++------------- packages/taco/src/taco.ts | 8 ++-- packages/taco/test/taco.test.ts | 2 +- 4 files changed, 60 insertions(+), 57 deletions(-) diff --git a/packages/shared/src/porter.ts b/packages/shared/src/porter.ts index 4dc054924..388b8f5c0 100644 --- a/packages/shared/src/porter.ts +++ b/packages/shared/src/porter.ts @@ -19,7 +19,6 @@ import { fromBase64, fromHexString, toBase64, toHexString } from './utils'; const defaultPorterUri: Record = { mainnet: 'https://porter.nucypher.community', tapir: 'https://porter-tapir.nucypher.community', - oryx: 'https://porter-oryx.nucypher.community', lynx: 'https://porter-lynx.nucypher.community', }; @@ -35,21 +34,17 @@ export const getPorterUri = (domain: Domain): string => { return getPorterUris(domain)[0]; }; -export const getPorterUris = (domain: Domain, porterUri?: string | string[]): string[] => { - const porterUris: string[] = []; - if (porterUri) { - if (porterUri instanceof Array) { - porterUris.push(...porterUri); - } else { - porterUris.push(porterUri); - } - } +export const getPorterUris = ( + domain: Domain, + porterUris: string[] = [], +): string[] => { + const fullList = [...porterUris]; const uri = defaultPorterUri[domain]; if (!uri) { throw new Error(`No default Porter URI found for domain: ${domain}`); } - porterUris.push(uri); - return porterUris; + fullList.push(uri); + return fullList; }; // /get_ursulas @@ -147,11 +142,13 @@ export class PorterClient { } } - protected async tryAndCall(config: AxiosRequestConfig): Promise> { + protected async tryAndCall( + config: AxiosRequestConfig, + ): Promise> { let resp!: AxiosResponse; let lastError = undefined; for (const porterUrl of this.porterUrls) { - const localConfig = { ...config, baseURL: porterUrl.toString() } + const localConfig = { ...config, baseURL: porterUrl.toString() }; try { resp = await axios.request(localConfig); } catch (e) { @@ -165,7 +162,9 @@ export class PorterClient { if (lastError !== undefined) { throw lastError; } - throw new Error("Porter returns bad response"); + throw new Error( + 'Porter returns bad response: ${resp.status} - ${resp.data}', + ); } public async getUrsulas( @@ -185,8 +184,7 @@ export class PorterClient { paramsSerializer: (params) => { return qs.stringify(params, { arrayFormat: 'comma' }); }, - }, - ); + }); return resp.data.result.ursulas.map((u: UrsulaResponse) => ({ checksumAddress: u.checksum_address, uri: u.uri, @@ -216,7 +214,7 @@ export class PorterClient { await this.tryAndCall({ url: '/retrieve_cfrags', method: 'post', - data: data + data: data, }); return resp.data.result.retrieval_results.map(({ cfrags, errors }) => { @@ -242,12 +240,11 @@ export class PorterClient { ), threshold, }; - const resp: AxiosResponse = - await this.tryAndCall({ - url: '/decrypt', - method: 'post', - data: data - }); + const resp: AxiosResponse = await this.tryAndCall({ + url: '/decrypt', + method: 'post', + data: data, + }); const { encrypted_decryption_responses, errors } = resp.data.result.decryption_results; diff --git a/packages/shared/test/porter.test.ts b/packages/shared/test/porter.test.ts index 113f3012a..83d65b252 100644 --- a/packages/shared/test/porter.test.ts +++ b/packages/shared/test/porter.test.ts @@ -1,19 +1,21 @@ +import { fakeUrsulas } from '@nucypher/test-utils'; +import axios, { HttpStatusCode } from 'axios'; +import { SpyInstance, beforeAll, describe, expect, it, vi } from 'vitest'; import { - initialize, GetUrsulasResult, PorterClient, - toHexString, Ursula, + initialize, + toHexString, } from '../src'; -import { fakeUrsulas } from '@nucypher/test-utils'; -import { beforeAll, describe, expect, SpyInstance, it, vi } from 'vitest'; -import axios, { HttpStatusCode } from 'axios'; -const fakePorterUris = ['https://_this_should_crash.com/', 'https://2_this_should_crash.com/', 'https://_this_should_work.com/']; +const fakePorterUris = [ + 'https://_this_should_crash.com/', + 'https://2_this_should_crash.com/', + 'https://_this_should_work.com/', +]; -const mockGetUrsulas = ( - ursulas: Ursula[] = fakeUrsulas(), -): SpyInstance => { +const mockGetUrsulas = (ursulas: Ursula[] = fakeUrsulas()): SpyInstance => { const fakePorterUrsulas = ( mockUrsulas: readonly Ursula[], ): GetUrsulasResult => { @@ -32,7 +34,10 @@ const mockGetUrsulas = ( return vi.spyOn(axios, 'request').mockImplementation(async (config) => { switch (config.baseURL) { case fakePorterUris[2]: - return Promise.resolve({ status: HttpStatusCode.Ok, data: fakePorterUrsulas(ursulas) }); + return Promise.resolve({ + status: HttpStatusCode.Ok, + data: fakePorterUrsulas(ursulas), + }); case fakePorterUris[0]: throw new Error(); default: @@ -46,37 +51,39 @@ describe('PorterClient', () => { await initialize(); }); - it('Get Ursulas', async () => { + it('should work when at least one ursula URI is valid', async () => { const ursulas = fakeUrsulas(); const getUrsulasSpy = mockGetUrsulas(ursulas); const porterClient = new PorterClient(fakePorterUris); const result = await porterClient.getUrsulas(ursulas.length); - expect(result.every((u: Ursula, index: number) => { - const expectedUrsula = ursulas[index]; - return u.checksumAddress === expectedUrsula.checksumAddress && - u.uri === expectedUrsula.uri && - u.encryptingKey.equals(expectedUrsula.encryptingKey); - })).toBeTruthy(); + expect( + result.every((u: Ursula, index: number) => { + const expectedUrsula = ursulas[index]; + return ( + u.checksumAddress === expectedUrsula.checksumAddress && + u.uri === expectedUrsula.uri && + u.encryptingKey.equals(expectedUrsula.encryptingKey) + ); + }), + ).toBeTruthy(); const params = { method: 'get', - url: "/get_ursulas", + url: '/get_ursulas', params: { exclude_ursulas: [], include_ursulas: [], quantity: ursulas.length, - } + }, }; expect(getUrsulasSpy).toBeCalledTimes(fakePorterUris.length); - expect(getUrsulasSpy).toHaveBeenNthCalledWith(1, expect.objectContaining( - { ...params, baseURL: fakePorterUris[0] }) - ); - expect(getUrsulasSpy).toHaveBeenNthCalledWith(2, expect.objectContaining( - { ...params, baseURL: fakePorterUris[1] }) - ); - expect(getUrsulasSpy).toHaveBeenNthCalledWith(3, expect.objectContaining( - { ...params, baseURL: fakePorterUris[2] })); + fakePorterUris.forEach((value, index) => { + expect(getUrsulasSpy).toHaveBeenNthCalledWith( + index + 1, + expect.objectContaining({ ...params, baseURL: value }), + ); + }); }); it('returns error in case all porters fail', async () => { @@ -87,5 +94,4 @@ describe('PorterClient', () => { porterClient = new PorterClient([fakePorterUris[1], fakePorterUris[0]]); expect(porterClient.getUrsulas(ursulas.length)).rejects.toThrowError(); }); - }); diff --git a/packages/taco/src/taco.ts b/packages/taco/src/taco.ts index 93d29b764..ce6572bf1 100644 --- a/packages/taco/src/taco.ts +++ b/packages/taco/src/taco.ts @@ -130,7 +130,7 @@ export const encryptWithPublicKey = async ( * Must match the `ritualId`. * @param {ThresholdMessageKit} messageKit - The kit containing the message to be decrypted * @param authProvider - The authentication provider that will be used to provide the authorization - * @param {string} [porterUri] - The URI for the Porter service. If not provided, a value will be obtained + * @param {string} [porterUri] - The URI(s) for the Porter service. If not provided, a value will be obtained * from the Domain * @param {Record} [customParameters] - Optional custom parameters that may be required * depending on the condition used @@ -145,10 +145,10 @@ export const decrypt = async ( domain: Domain, messageKit: ThresholdMessageKit, authProvider?: EIP4361AuthProvider, - porterUri?: string | string[], + porterUris: string[] = [], customParameters?: Record, ): Promise => { - const porterUris: string[] = getPorterUris(domain, porterUri); + const porterUrisFull: string[] = getPorterUris(domain, porterUris); const ritualId = await DkgCoordinatorAgent.getRitualIdFromPublicKey( provider, @@ -164,7 +164,7 @@ export const decrypt = async ( return retrieveAndDecrypt( provider, domain, - porterUris, + porterUrisFull, messageKit, ritualId, ritual.sharesNum, diff --git a/packages/taco/test/taco.test.ts b/packages/taco/test/taco.test.ts index f965fa80c..8fd2545a0 100644 --- a/packages/taco/test/taco.test.ts +++ b/packages/taco/test/taco.test.ts @@ -93,7 +93,7 @@ describe('taco', () => { domains.DEVNET, messageKit, authProvider, - fakePorterUri, + [fakePorterUri], ); expect(decryptedMessage).toEqual(toBytes(message)); expect(getParticipantsSpy).toHaveBeenCalled();