Skip to content

Commit

Permalink
Multiple Porter URIs (#553)
Browse files Browse the repository at this point in the history
  • Loading branch information
derekpierre committed Aug 9, 2024
2 parents 3f15a7f + 1977815 commit 142a509
Show file tree
Hide file tree
Showing 17 changed files with 191 additions and 73 deletions.
3 changes: 1 addition & 2 deletions demos/taco-demo/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
decrypt,
domains,
encrypt,
getPorterUri,
initialize,
ThresholdMessageKit,
toHexString,
Expand Down Expand Up @@ -89,7 +88,7 @@ export default function App() {
provider,
domain,
encryptedMessage,
getPorterUri(domain),
undefined,
provider.getSigner(),
);

Expand Down
5 changes: 2 additions & 3 deletions demos/taco-nft-demo/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
decrypt,
domains,
encrypt,
getPorterUri,
initialize,
ThresholdMessageKit,
} from '@nucypher/taco';
Expand Down Expand Up @@ -78,8 +77,8 @@ export default function App() {
provider,
domain,
encryptedMessage,
getPorterUri(domain),
provider.getSigner(),
undefined,
provider.getSigner()
);

setDecryptedMessage(new TextDecoder().decode(decryptedMessage));
Expand Down
2 changes: 0 additions & 2 deletions examples/taco/nextjs/src/hooks/useTaco.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
Domain,
EIP4361AuthProvider,
encrypt,
getPorterUri,
initialize,
ThresholdMessageKit,
} from '@nucypher/taco';
Expand Down Expand Up @@ -38,7 +37,6 @@ export default function useTaco({
domain,
messageKit,
authProvider,
getPorterUri(domain),
);
},
[isInit, provider, domain],
Expand Down
2 changes: 0 additions & 2 deletions examples/taco/nodejs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
EIP4361AuthProvider,
encrypt,
fromBytes,
getPorterUri,
initialize,
isAuthorized,
ThresholdMessageKit,
Expand Down Expand Up @@ -119,7 +118,6 @@ const decryptFromBytes = async (encryptedBytes: Uint8Array) => {
domain,
messageKit,
authProvider,
getPorterUri(domain),
);
};

Expand Down
2 changes: 0 additions & 2 deletions examples/taco/react/src/hooks/useTaco.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
Domain,
EIP4361AuthProvider,
encrypt,
getPorterUri,
initialize,
ThresholdMessageKit,
} from '@nucypher/taco';
Expand Down Expand Up @@ -38,7 +37,6 @@ export default function useTaco({
domain,
messageKit,
authProvider,
getPorterUri(domain),
);
},
[isInit, provider, domain],
Expand Down
3 changes: 1 addition & 2 deletions examples/taco/webpack-5/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
EIP4361AuthProvider,
encrypt,
fromBytes,
getPorterUri,
getPorterUris,
initialize,
toBytes,
} from '@nucypher/taco';
Expand Down Expand Up @@ -67,7 +67,6 @@ const runExample = async () => {
domain,
messageKit,
authProvider,
getPorterUri(domain),
);
const decryptedMessage = fromBytes(decryptedBytes);
console.log('Decrypted message:', decryptedMessage);
Expand Down
1 change: 1 addition & 0 deletions packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
93 changes: 68 additions & 25 deletions packages/shared/src/porter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,23 @@ 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';
import { fromBase64, fromHexString, toBase64, toHexString } from './utils';

const porterUri: Record<string, string> = {
const defaultPorterUri: Record<string, string> = {
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<string, Domain> = {
DEVNET: 'lynx',
Expand All @@ -28,11 +31,20 @@ export const domains: Record<string, Domain> = {
};

export const getPorterUri = (domain: Domain): string => {
const uri = porterUri[domain];
return getPorterUris(domain)[0];
};

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}`);
}
return porterUri[domain];
fullList.push(uri);
return fullList;
};

// /get_ursulas
Expand Down Expand Up @@ -120,10 +132,39 @@ export type TacoDecryptResult = {
};

export class PorterClient {
readonly porterUrl: URL;
readonly porterUrls: URL[];

constructor(porterUri: string) {
this.porterUrl = new URL(porterUri);
constructor(porterUris: string | string[]) {
if (porterUris instanceof Array) {
this.porterUrls = porterUris.map((uri) => new URL(uri));
} else {
this.porterUrls = [new URL(porterUris)];
}
}

protected async tryAndCall<T, D>(
config: AxiosRequestConfig<D>,
): Promise<AxiosResponse<T>> {
let resp!: AxiosResponse<T>;
let lastError = undefined;
for (const porterUrl of this.porterUrls) {
const localConfig = { ...config, baseURL: porterUrl.toString() };
try {
resp = await axios.request(localConfig);
} catch (e) {
lastError = e;
continue;
}
if (resp.status === HttpStatusCode.Ok) {
return resp;
}
}
if (lastError !== undefined) {
throw lastError;
}
throw new Error(
'Porter returns bad response: ${resp.status} - ${resp.data}',
);
}

public async getUrsulas(
Expand All @@ -136,15 +177,14 @@ export class PorterClient {
exclude_ursulas: excludeUrsulas,
include_ursulas: includeUrsulas,
};
const resp: AxiosResponse<GetUrsulasResult> = await axios.get(
new URL('/get_ursulas', this.porterUrl).toString(),
{
params,
paramsSerializer: (params) => {
return qs.stringify(params, { arrayFormat: 'comma' });
},
const resp: AxiosResponse<GetUrsulasResult> = 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,
uri: u.uri,
Expand All @@ -170,10 +210,12 @@ export class PorterClient {
bob_verifying_key: toHexString(bobVerifyingKey.toCompressedBytes()),
context: conditionContextJSON,
};
const resp: AxiosResponse<PostRetrieveCFragsResponse> = await axios.post(
new URL('/retrieve_cfrags', this.porterUrl).toString(),
data,
);
const resp: AxiosResponse<PostRetrieveCFragsResponse> =
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) => [
Expand All @@ -198,10 +240,11 @@ export class PorterClient {
),
threshold,
};
const resp: AxiosResponse<PostTacoDecryptResponse> = await axios.post(
new URL('/decrypt', this.porterUrl).toString(),
data,
);
const resp: AxiosResponse<PostTacoDecryptResponse> = await this.tryAndCall({
url: '/decrypt',
method: 'post',
data: data,
});

const { encrypted_decryption_responses, errors } =
resp.data.result.decryption_results;
Expand Down
97 changes: 97 additions & 0 deletions packages/shared/test/porter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { fakeUrsulas } from '@nucypher/test-utils';
import axios, { HttpStatusCode } from 'axios';
import { SpyInstance, beforeAll, describe, expect, it, vi } from 'vitest';
import {
GetUrsulasResult,
PorterClient,
Ursula,
initialize,
toHexString,
} from '../src';

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('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();
const params = {
method: 'get',
url: '/get_ursulas',
params: {
exclude_ursulas: [],
include_ursulas: [],
quantity: ursulas.length,
},
};

expect(getUrsulasSpy).toBeCalledTimes(fakePorterUris.length);
fakePorterUris.forEach((value, index) => {
expect(getUrsulasSpy).toHaveBeenNthCalledWith(
index + 1,
expect.objectContaining({ ...params, baseURL: value }),
);
});
});

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();
});
});
5 changes: 5 additions & 0 deletions packages/shared/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,9 @@
"skipLibCheck": true,
"resolveJsonModule": true,
},
"references": [
{
"path": "../test-utils/tsconfig.es.json",
},
],
}
2 changes: 0 additions & 2 deletions packages/taco/examples/encrypt-decrypt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
domains,
EIP4361AuthProvider,
encrypt,
getPorterUri,
initialize,
ThresholdMessageKit,
toBytes,
Expand Down Expand Up @@ -55,7 +54,6 @@ const run = async () => {
domains.TESTNET,
messageKit,
authProvider,
getPorterUri(domains.TESTNET),
);
return decryptedMessage;
};
Expand Down
2 changes: 1 addition & 1 deletion packages/taco/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export {
Domain,
domains,
fromBytes,
getPorterUri,
getPorterUris,
initialize,
toBytes,
toHexString,
Expand Down
Loading

0 comments on commit 142a509

Please sign in to comment.