Skip to content

Commit ede3547

Browse files
committed
feat(cardano-services-client): restore KoraLabsHandleProvider
Does not re-engage in e2e tests
1 parent 62010d2 commit ede3547

File tree

7 files changed

+261
-0
lines changed

7 files changed

+261
-0
lines changed

packages/cardano-services-client/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"@cardano-sdk/core": "workspace:~",
6161
"@cardano-sdk/crypto": "workspace:~",
6262
"@cardano-sdk/util": "workspace:~",
63+
"@koralabs/handles-public-api-interfaces": "^2.13.0",
6364
"axios": "^1.7.4",
6465
"class-validator": "^0.14.0",
6566
"isomorphic-ws": "^5.0.0",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import {
2+
Asset,
3+
Cardano,
4+
HandleProvider,
5+
HandleResolution,
6+
HealthCheckResponse,
7+
ProviderError,
8+
ProviderFailure,
9+
ResolveHandlesArgs
10+
} from '@cardano-sdk/core';
11+
12+
// eslint-disable-next-line import/no-extraneous-dependencies
13+
import { IHandle } from '@koralabs/handles-public-api-interfaces';
14+
import axios, { AxiosAdapter, AxiosInstance } from 'axios';
15+
16+
/** The KoraLabsHandleProvider endpoint paths. */
17+
const paths = {
18+
handles: '/handles',
19+
healthCheck: '/health'
20+
};
21+
22+
export interface KoraLabsHandleProviderDeps {
23+
serverUrl: string;
24+
adapter?: AxiosAdapter;
25+
policyId: Cardano.PolicyId;
26+
}
27+
28+
export const toHandleResolution = ({
29+
apiResponse,
30+
policyId
31+
}: {
32+
apiResponse: IHandle;
33+
policyId: Cardano.PolicyId;
34+
}): HandleResolution => ({
35+
backgroundImage: apiResponse.bg_image ? Asset.Uri(apiResponse.bg_image) : undefined,
36+
cardanoAddress: Cardano.PaymentAddress(apiResponse.resolved_addresses.ada),
37+
handle: apiResponse.name,
38+
hasDatum: apiResponse.has_datum,
39+
image: apiResponse.image ? Asset.Uri(apiResponse.image) : undefined,
40+
policyId,
41+
profilePic: apiResponse.pfp_image ? Asset.Uri(apiResponse.pfp_image) : undefined
42+
});
43+
44+
/**
45+
* Creates a KoraLabs Provider instance to resolve Standard Handles
46+
*
47+
* @param KoraLabsHandleProviderDeps The configuration object fot the KoraLabs Handle Provider.
48+
*/
49+
export class KoraLabsHandleProvider implements HandleProvider {
50+
private axiosClient: AxiosInstance;
51+
policyId: Cardano.PolicyId;
52+
53+
constructor({ serverUrl, adapter, policyId }: KoraLabsHandleProviderDeps) {
54+
this.axiosClient = axios.create({
55+
adapter,
56+
baseURL: serverUrl
57+
});
58+
this.policyId = policyId;
59+
}
60+
61+
async resolveHandles(args: ResolveHandlesArgs): Promise<Array<HandleResolution | null>> {
62+
try {
63+
const results = await Promise.all(
64+
args.handles.map((handle) => this.axiosClient.get<IHandle>(`${paths.handles}/${handle}`))
65+
);
66+
return results.map(({ data: apiResponse }) => toHandleResolution({ apiResponse, policyId: this.policyId }));
67+
} catch (error) {
68+
if (axios.isAxiosError(error)) {
69+
if (error.request) {
70+
throw new ProviderError(ProviderFailure.ConnectionFailure, error, error.code);
71+
}
72+
73+
if (error.response?.status === 404) {
74+
return [null];
75+
}
76+
77+
throw new ProviderError(ProviderFailure.Unhealthy, error, `Failed to resolve handles due to: ${error.message}`);
78+
}
79+
if (error instanceof ProviderError) throw error;
80+
throw new ProviderError(ProviderFailure.Unknown, error, 'Failed to resolve handles');
81+
}
82+
}
83+
84+
async healthCheck(): Promise<HealthCheckResponse> {
85+
try {
86+
await this.axiosClient.get(`${paths.healthCheck}`);
87+
return { ok: true };
88+
} catch {
89+
return { ok: false };
90+
}
91+
}
92+
93+
async getPolicyIds(): Promise<Cardano.PolicyId[]> {
94+
return [this.policyId];
95+
}
96+
}
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
export * from './KoraLabsHandleProvider';
12
export * from './handleHttpProvider';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/* eslint-disable no-magic-numbers */
2+
/* eslint-disable camelcase */
3+
import { Cardano, ProviderError } from '@cardano-sdk/core';
4+
import { KoraLabsHandleProvider } from '../../src';
5+
import {
6+
getAliceHandleAPIResponse,
7+
getAliceHandleProviderResponse,
8+
getBobHandleAPIResponse,
9+
getBobHandleProviderResponse
10+
} from '../util';
11+
import MockAdapter from 'axios-mock-adapter';
12+
import axios from 'axios';
13+
14+
const config = {
15+
policyId: Cardano.PolicyId('50fdcdbfa3154db86a87e4b5697ae30d272e0bbcfa8122efd3e301cb'),
16+
serverUrl: 'http://some-hostname:3000'
17+
};
18+
19+
describe('KoraLabsHandleProvider', () => {
20+
let axiosMock: MockAdapter;
21+
let provider: KoraLabsHandleProvider;
22+
23+
beforeAll(() => {
24+
axiosMock = new MockAdapter(axios);
25+
provider = new KoraLabsHandleProvider(config);
26+
});
27+
28+
afterEach(() => {
29+
axiosMock.reset();
30+
});
31+
32+
afterAll(() => {
33+
axiosMock.restore();
34+
});
35+
36+
describe('resolveHandles', () => {
37+
test('HandlesProvider should resolve a single handle', async () => {
38+
axiosMock.onGet().replyOnce(200, getAliceHandleAPIResponse);
39+
const args = {
40+
handles: ['alice']
41+
};
42+
await expect(provider.resolveHandles(args)).resolves.toEqual([getAliceHandleProviderResponse]);
43+
});
44+
45+
test('HandleProvider should resolve multiple handles', async () => {
46+
axiosMock.onGet().replyOnce(200, getAliceHandleAPIResponse).onGet().replyOnce(200, getBobHandleAPIResponse);
47+
const args = {
48+
handles: ['alice', 'bob']
49+
};
50+
await expect(provider.resolveHandles(args)).resolves.toEqual([
51+
getAliceHandleProviderResponse,
52+
getBobHandleProviderResponse
53+
]);
54+
});
55+
});
56+
57+
describe('error checks', () => {
58+
test('HandleProvider should throw ProviderError with ConnectionFailure on request error', async () => {
59+
axiosMock.onGet('/handles/alice').networkError();
60+
const args = { handles: ['alice'] };
61+
await expect(provider.resolveHandles(args)).rejects.toThrowError(ProviderError);
62+
});
63+
test('HandleProvider should return null for 404 response from API', async () => {
64+
axiosMock.onGet('/handles/alice').reply(404);
65+
const args = { handles: ['alice'] };
66+
await expect(provider.resolveHandles(args)).resolves.toEqual([null]);
67+
});
68+
test('HandleProvider should throw ProviderError with Unhealthy on other Axios error', async () => {
69+
axiosMock.onGet('/handles/bob').reply(500);
70+
const args = { handles: ['bob'] };
71+
await expect(provider.resolveHandles(args)).rejects.toThrowError(ProviderError);
72+
});
73+
test('HandleProvider should throw ProviderError', async () => {
74+
axiosMock.onGet('/handles/bob').networkError();
75+
const args = { handles: ['bob'] };
76+
await expect(provider.resolveHandles(args)).rejects.toThrowError(ProviderError);
77+
});
78+
test('HandleProvider should throw ProviderError with Unknown, unable to resolve handle', async () => {
79+
axiosMock.onGet().replyOnce(304, getAliceHandleAPIResponse);
80+
const args = { handles: ['bob'] };
81+
await expect(provider.resolveHandles(args)).rejects.toThrowError(ProviderError);
82+
});
83+
});
84+
85+
describe('health checks', () => {
86+
test('HandleProvider should get ok health check', async () => {
87+
axiosMock.onGet().replyOnce(200, {});
88+
const result = await provider.healthCheck();
89+
expect(result.ok).toEqual(true);
90+
});
91+
92+
test('HandleProvider should get not ok health check', async () => {
93+
const providerWithBadConfig = new KoraLabsHandleProvider({
94+
policyId: Cardano.PolicyId('50fdcdbfa3154db86a87e4b5697ae30d272e0bbcfa8122efd3e301cb'),
95+
serverUrl: ''
96+
});
97+
const result = await providerWithBadConfig.healthCheck();
98+
expect(result.ok).toEqual(false);
99+
});
100+
});
101+
102+
describe('get policy ids', () => {
103+
test('HandleProvider should get handle policy ids', async () => {
104+
const policyIds = await provider.getPolicyIds();
105+
await expect(policyIds.length).toEqual(1);
106+
await expect(policyIds).toEqual([config.policyId]);
107+
});
108+
});
109+
});

packages/cardano-services-client/test/util.ts

+45
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Asset, Cardano, ProviderError, ProviderFailure } from '@cardano-sdk/core';
22
import { AxiosError, AxiosResponse } from 'axios';
3+
import { IHandle, Rarity } from '@koralabs/handles-public-api-interfaces';
34
import { logger } from '@cardano-sdk/util-dev';
45
import { toSerializableObject } from '@cardano-sdk/util';
56

@@ -61,6 +62,50 @@ export const getBobHandleProviderResponse = {
6162
profilePic: Asset.Uri('ipfs://zrljm7nskakjydxlr450ktsj08zuw6aktvgfkmmyw9semrkrezryq3yd1')
6263
};
6364

65+
export const getAliceHandleAPIResponse: Partial<IHandle> = {
66+
characters: 'rljm7n/23455',
67+
created_slot_number: 33,
68+
default_in_wallet: 'alice_default_hndle',
69+
has_datum: false,
70+
hex: '0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d5',
71+
holder: 'stake1uyehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gh6ffgw',
72+
image: 'ipfs://c8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56feasd',
73+
length: 123,
74+
name: 'alice',
75+
numeric_modifiers: '-12.9',
76+
og_number: 5,
77+
rarity: Rarity.rare,
78+
resolved_addresses: {
79+
ada: 'addr_test1qqk4sr4f7vtqzd2w90d5nfu3n59jhhpawyphnek2y7er02nkrezryq3ydtmkg0e7e2jvzg443h0ffzfwd09wpcxy2fuqmcnecd'
80+
},
81+
standard_image: 'ipfs://c8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56feasdfasd',
82+
updated_slot_number: 22,
83+
utxo: 'rljm7nskakjydxlr450ktsj08zuw6aktvgfkmmyw9semrkrezryq3ydtmkg0'
84+
};
85+
86+
export const getBobHandleAPIResponse: Partial<IHandle> = {
87+
bg_image: 'ipfs://zrljm7nskakjydxlr450ktsj08zuw6aktvgfkmmyw9semrkrezryq3yd',
88+
characters: 'rljm7n/23455',
89+
created_slot_number: 33,
90+
default_in_wallet: 'bob_default_handle',
91+
has_datum: false,
92+
hex: '0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d5',
93+
holder: 'stake1uyehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gh6ffgw',
94+
image: 'ipfs://c8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe',
95+
length: 123,
96+
name: 'bob',
97+
numeric_modifiers: '-12.9',
98+
og_number: 5,
99+
pfp_image: 'ipfs://zrljm7nskakjydxlr450ktsj08zuw6aktvgfkmmyw9semrkrezryq3yd1',
100+
rarity: Rarity.rare,
101+
resolved_addresses: {
102+
ada: 'addr_test1qzrljm7nskakjydxlr450ktsj08zuw6aktvgfkmmyw9semrkrezryq3ydtmkg0e7e2jvzg443h0ffzfwd09wpcxy2fuql9tk0g'
103+
},
104+
standard_image: 'ipfs://c8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56feasdfasd',
105+
updated_slot_number: 22,
106+
utxo: 'rljm7nskakjydxlr450ktsj08zuw6aktvgfkmmyw9semrkrezryq3ydtmkg0'
107+
};
108+
64109
export const mockResponses = (request: jest.Mock, responses: [string | RegExp, unknown][]) => {
65110
request.mockImplementation(async (endpoint: string) => {
66111
for (const [match, response] of responses) {

yarn-project.nix

+1
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,7 @@ cacheEntries = {
493493
"@jridgewell/trace-mapping@npm:0.3.25" = { filename = "@jridgewell-trace-mapping-npm-0.3.25-c076fd2279-9d3c40d225.zip"; sha512 = "9d3c40d225e139987b50c48988f8717a54a8c994d8a948ee42e1412e08988761d0754d7d10b803061cc3aebf35f92a5dbbab493bd0e1a9ef9e89a2130e83ba34"; };
494494
"@jridgewell/trace-mapping@npm:0.3.9" = { filename = "@jridgewell-trace-mapping-npm-0.3.9-91625cd7fb-d89597752f.zip"; sha512 = "d89597752fd88d3f3480845691a05a44bd21faac18e2185b6f436c3b0fd0c5a859fbbd9aaa92050c4052caf325ad3e10e2e1d1b64327517471b7d51babc0ddef"; };
495495
"@jsdevtools/ono@npm:7.1.3" = { filename = "@jsdevtools-ono-npm-7.1.3-cb2313543b-2297fcd472.zip"; sha512 = "2297fcd472ba810bffe8519d2249171132844c7174f3a16634f9260761c8c78bc0428a4190b5b6d72d45673c13918ab9844d706c3ed4ef8f62ab11a2627a08ad"; };
496+
"@koralabs/handles-public-api-interfaces@npm:2.13.0" = { filename = "@koralabs-handles-public-api-interfaces-npm-2.13.0-33d8afa484-b28895a70e.zip"; sha512 = "b28895a70ecdb1b4643953dd0af11b406970a5388b79889050f19ef01335c7da29fb8696d26e80f645fd435ca8cc90f13b5fed0c4a5ceb0ac0d2447aad5e3e06"; };
496497
"@ledgerhq/devices@npm:8.4.4" = { filename = "@ledgerhq-devices-npm-8.4.4-9331403bf0-370fb38d48.zip"; sha512 = "370fb38d484665c92165580e285cc792e7af0bf114a5d1e855aec602c6e39592090d0de7a43addeb4c13622f734ddd4f25be82f07507e14550753ce5473eea66"; };
497498
"@ledgerhq/errors@npm:6.19.1" = { filename = "@ledgerhq-errors-npm-6.19.1-4837ba7170-f4e1cf0d6a.zip"; sha512 = "f4e1cf0d6a5808c58235c54e2a1565556de8e683552bd94d97609f790e8c25226114bfa2af9914bb329c3e7c314585b60c66ddac8d8f8f1d072b7e791a889ad8"; };
498499
"@ledgerhq/hw-transport-node-hid-noevents@npm:6.30.5" = { filename = "@ledgerhq-hw-transport-node-hid-noevents-npm-6.30.5-19d7e6c1d0-013d7d6745.zip"; sha512 = "013d7d674573b838b3e2d6a750a8e48a1f1722d986c272513de28f68c5a203576e2b39beea1f0ea7ca5e348421b50f79d8af1a30774068479b1fa6526662eae1"; };

yarn.lock

+8
Original file line numberDiff line numberDiff line change
@@ -3391,6 +3391,7 @@ __metadata:
33913391
"@cardano-sdk/crypto": "workspace:~"
33923392
"@cardano-sdk/util": "workspace:~"
33933393
"@cardano-sdk/util-dev": "workspace:~"
3394+
"@koralabs/handles-public-api-interfaces": ^2.13.0
33943395
"@types/lodash": ^4.14.182
33953396
"@types/node-fetch": ^2.6.12
33963397
"@types/validator": ^13.7.1
@@ -5173,6 +5174,13 @@ __metadata:
51735174
languageName: node
51745175
linkType: hard
51755176

5177+
"@koralabs/handles-public-api-interfaces@npm:^2.13.0":
5178+
version: 2.13.0
5179+
resolution: "@koralabs/handles-public-api-interfaces@npm:2.13.0"
5180+
checksum: b28895a70ecdb1b4643953dd0af11b406970a5388b79889050f19ef01335c7da29fb8696d26e80f645fd435ca8cc90f13b5fed0c4a5ceb0ac0d2447aad5e3e06
5181+
languageName: node
5182+
linkType: hard
5183+
51765184
"@ledgerhq/devices@npm:^8.4.4":
51775185
version: 8.4.4
51785186
resolution: "@ledgerhq/devices@npm:8.4.4"

0 commit comments

Comments
 (0)