Skip to content

Commit

Permalink
feat(cardano-services-client): add KoraLabsHandleProvider
Browse files Browse the repository at this point in the history
  • Loading branch information
VanessaPC committed May 31, 2023
1 parent f209095 commit 746e311
Show file tree
Hide file tree
Showing 6 changed files with 284 additions and 1 deletion.
1 change: 1 addition & 0 deletions packages/cardano-services-client/package.json
Expand Up @@ -54,6 +54,7 @@
},
"devDependencies": {
"@cardano-sdk/util-dev": "workspace:~",
"@koralabs/handles-public-api-interfaces": "1.6.6",
"@types/validator": "^13.7.1",
"axios-mock-adapter": "^1.20.0",
"eslint": "^7.32.0",
Expand Down
@@ -0,0 +1,102 @@
import {
Cardano,
HandleProvider,
HandleResolution,
HealthCheckResponse,
NetworkInfoProvider,
ProviderError,
ProviderFailure,
ResolveHandlesArgs
} from '@cardano-sdk/core';

// eslint-disable-next-line import/no-extraneous-dependencies
import { IHandle } from '@koralabs/handles-public-api-interfaces';
import axios, { AxiosAdapter, AxiosInstance } from 'axios';

/**
* The KoraLabsHandleProvider endpoint paths.
*/
const paths = {
handles: '/handles',
healthCheck: '/health'
};

export interface KoraLabsHandleProviderDeps {
serverUrl: string;
networkInfoProvider: NetworkInfoProvider;
adapter?: AxiosAdapter;
policyId: Cardano.PolicyId;
}

export const toHandleResolution = ({
apiResponse,
tip,
policyId
}: {
apiResponse: IHandle;
tip: Cardano.Tip;
policyId: Cardano.PolicyId;
}): HandleResolution => ({
handle: apiResponse.name,
hasDatum: apiResponse.hasDatum,
policyId,
resolvedAddresses: {
cardano: Cardano.PaymentAddress(apiResponse.resolved_addresses.ada)
},
resolvedAt: {
hash: tip.hash,
slot: tip.slot
}
});

/**
* Creates a KoraLabs Provider instance to resolve Standard Handles
*
* @param KoraLabsHandleProviderDeps The configuration object fot the KoraLabs Handle Provider.
*/
export class KoraLabsHandleProvider implements HandleProvider {
private axiosClient: AxiosInstance;
private networkInfoProvider: NetworkInfoProvider;
policyId: Cardano.PolicyId;

constructor({ serverUrl, networkInfoProvider, adapter, policyId }: KoraLabsHandleProviderDeps) {
this.networkInfoProvider = networkInfoProvider;
this.axiosClient = axios.create({
adapter,
baseURL: serverUrl
});
this.policyId = policyId;
}

async resolveHandles(args: ResolveHandlesArgs): Promise<Array<HandleResolution | null>> {
try {
const tip = await this.networkInfoProvider.ledgerTip();
const results = await Promise.all(
args.handles.map((handle) => this.axiosClient.get<IHandle>(`${paths.handles}/${handle}`))
);
return results.map(({ data: apiResponse }) => toHandleResolution({ apiResponse, policyId: this.policyId, tip }));
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.request) {
throw new ProviderError(ProviderFailure.ConnectionFailure, error, error.code);
}

if (error.response?.status === 404) {
return [null];
}

throw new ProviderError(ProviderFailure.Unhealthy, error, `Failed to resolve handles due to: ${error.message}`);
}
if (error instanceof ProviderError) throw error;
throw new ProviderError(ProviderFailure.Unknown, error, 'Failed to resolve handles');
}
}
async healthCheck(): Promise<HealthCheckResponse> {
try {
await this.axiosClient.get(`${paths.healthCheck}`);
return { ok: true };
} catch {
return { ok: false };
}
}
}
@@ -0,0 +1 @@
export * from './KoraLabsHandleProvider';
1 change: 1 addition & 0 deletions packages/cardano-services-client/src/index.ts
Expand Up @@ -6,3 +6,4 @@ export * from './UtxoProvider';
export * from './ChainHistoryProvider';
export * from './NetworkInfoProvider';
export * from './RewardsProvider';
export * from './HandleProvider';
@@ -0,0 +1,104 @@
/* eslint-disable no-magic-numbers */
/* eslint-disable camelcase */
import { Cardano, ProviderError } from '@cardano-sdk/core';
import { KoraLabsHandleProvider } from '../../src';
import {
getAliceHandleAPIResponse,
getAliceHandleProviderResponse,
getBobHandleAPIResponse,
getBobHandleProviderResponse
} from '../util';
import { mockProviders as mocks } from '@cardano-sdk/util-dev';
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios';

const config = {
networkInfoProvider: mocks.mockNetworkInfoProvider2(100),
policyId: Cardano.PolicyId('50fdcdbfa3154db86a87e4b5697ae30d272e0bbcfa8122efd3e301cb'),
serverUrl: 'http://some-hostname:3000'
};

describe('KoraLabsHandleProvider', () => {
let axiosMock: MockAdapter;
let provider: KoraLabsHandleProvider;

beforeAll(() => {
axiosMock = new MockAdapter(axios);
provider = new KoraLabsHandleProvider(config);
});

afterEach(() => {
axiosMock.reset();
});

afterAll(() => {
axiosMock.restore();
});

describe('resolveHandles', () => {
test('HandlesProvider should resolve a single handle', async () => {
axiosMock.onGet().replyOnce(200, getAliceHandleAPIResponse);
const args = {
handles: ['alice']
};
await expect(provider.resolveHandles(args)).resolves.toEqual([getAliceHandleProviderResponse]);
});

test('HandleProvider should resolve multiple handles', async () => {
axiosMock.onGet().replyOnce(200, getAliceHandleAPIResponse).onGet().replyOnce(200, getBobHandleAPIResponse);
const args = {
handles: ['alice', 'bob']
};
await expect(provider.resolveHandles(args)).resolves.toEqual([
getAliceHandleProviderResponse,
getBobHandleProviderResponse
]);
});
});

describe('error checks', () => {
test('HandleProvider should throw ProviderError with ConnectionFailure on request error', async () => {
axiosMock.onGet('/handles/alice').networkError();
const args = { handles: ['alice'] };
await expect(provider.resolveHandles(args)).rejects.toThrowError(ProviderError);
});
test('HandleProvider should return null for 404 response from API', async () => {
axiosMock.onGet('/handles/alice').reply(404);
const args = { handles: ['alice'] };
await expect(provider.resolveHandles(args)).resolves.toEqual([null]);
});
test('HandleProvider should throw ProviderError with Unhealthy on other Axios error', async () => {
axiosMock.onGet('/handles/bob').reply(500);
const args = { handles: ['bob'] };
await expect(provider.resolveHandles(args)).rejects.toThrowError(ProviderError);
});
test('HandleProvider should throw ProviderError', async () => {
axiosMock.onGet('/handles/bob').networkError();
const args = { handles: ['bob'] };
await expect(provider.resolveHandles(args)).rejects.toThrowError(ProviderError);
});
test('HandleProvider should throw ProviderError with Unknown, unable to resolve handle', async () => {
axiosMock.onGet().replyOnce(304, getAliceHandleAPIResponse);
const args = { handles: ['bob'] };
await expect(provider.resolveHandles(args)).rejects.toThrowError(ProviderError);
});
});

describe('health checks', () => {
test('HandleProvider should get ok health check', async () => {
axiosMock.onGet().replyOnce(200, {});
const result = await provider.healthCheck();
expect(result.ok).toEqual(true);
});

test('HandleProvider should get not ok health check', async () => {
const providerWithBadConfig = new KoraLabsHandleProvider({
networkInfoProvider: mocks.mockNetworkInfoProvider2(100),
policyId: Cardano.PolicyId('50fdcdbfa3154db86a87e4b5697ae30d272e0bbcfa8122efd3e301cb'),
serverUrl: ''
});
const result = await providerWithBadConfig.healthCheck();
expect(result.ok).toEqual(false);
});
});
});
76 changes: 75 additions & 1 deletion packages/cardano-services-client/test/util.ts
@@ -1,5 +1,5 @@
import { AxiosError, AxiosResponse } from 'axios';
import { ProviderError, ProviderFailure } from '@cardano-sdk/core';
import { Cardano, ProviderError, ProviderFailure } from '@cardano-sdk/core';
import { toSerializableObject } from '@cardano-sdk/util';

export const axiosError = (bodyError = new Error('error')) => {
Expand All @@ -22,3 +22,77 @@ export const healthCheckResponseWithState = {
},
ok: true
};

export const getAliceHandleProviderResponse = {
handle: 'alice',
hasDatum: false,
policyId: Cardano.PolicyId('50fdcdbfa3154db86a87e4b5697ae30d272e0bbcfa8122efd3e301cb'),
resolvedAddresses: {
cardano:
'addr_test1qqk4sr4f7vtqzd2w90d5nfu3n59jhhpawyphnek2y7er02nkrezryq3ydtmkg0e7e2jvzg443h0ffzfwd09wpcxy2fuqmcnecd'
},
resolvedAt: {
hash: Cardano.BlockId('10d64cc11e9b20e15b6c46aa7b1fed11246f437e62225655a30ea47bf8cc22d0'),
slot: Cardano.Slot(37_834_496)
}
};

export const getBobHandleProviderResponse = {
handle: 'bob',
hasDatum: false,
policyId: Cardano.PolicyId('50fdcdbfa3154db86a87e4b5697ae30d272e0bbcfa8122efd3e301cb'),
resolvedAddresses: {
cardano:
'addr_test1qzrljm7nskakjydxlr450ktsj08zuw6aktvgfkmmyw9semrkrezryq3ydtmkg0e7e2jvzg443h0ffzfwd09wpcxy2fuql9tk0g'
},
resolvedAt: {
hash: Cardano.BlockId('10d64cc11e9b20e15b6c46aa7b1fed11246f437e62225655a30ea47bf8cc22d0'),
slot: Cardano.Slot(37_834_496)
}
};

export const getAliceHandleAPIResponse = {
background: 'zrljm7nskakjydxlr450ktsj08zuw6aktvgfkmmyw9semrkrezryq3yd',
characters: 'rljm7n/23455',
created_slot_number: 33,
default_in_wallet: 'alice_default_hndle',
hasDatum: false,
hex: '0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d5',
holder_address: 'stake1uyehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gh6ffgw',
length: 123,
name: 'alice',
nft_image: 'c8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe',
numeric_modifiers: '-12.9',
og: 5,
original_nft_image: 'c8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56feasdfasd',
profile_pic: 'zrljm7nskakjydxlr450ktsj08zuw6aktvgfkmmyw9semrkrezryq3yd',
rarity: 'rare',
resolved_addresses: {
ada: 'addr_test1qqk4sr4f7vtqzd2w90d5nfu3n59jhhpawyphnek2y7er02nkrezryq3ydtmkg0e7e2jvzg443h0ffzfwd09wpcxy2fuqmcnecd'
},
updated_slot_number: 22,
utxo: 'rljm7nskakjydxlr450ktsj08zuw6aktvgfkmmyw9semrkrezryq3ydtmkg0'
};

export const getBobHandleAPIResponse = {
background: 'zrljm7nskakjydxlr450ktsj08zuw6aktvgfkmmyw9semrkrezryq3yd',
characters: 'rljm7n/23455',
created_slot_number: 33,
default_in_wallet: 'bob_default_handle',
hasDatum: false,
hex: '0f3abbc8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe0e78f19d9d5',
holder_address: 'stake1uyehkck0lajq8gr28t9uxnuvgcqrc6070x3k9r8048z8y5gh6ffgw',
length: 123,
name: 'bob',
nft_image: 'c8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56fe',
numeric_modifiers: '-12.9',
og: 5,
original_nft_image: 'c8fc19c2e61bab6059bf8a466e6e754833a08a62a6c56feasdfasd',
profile_pic: 'zrljm7nskakjydxlr450ktsj08zuw6aktvgfkmmyw9semrkrezryq3yd',
rarity: 'rare',
resolved_addresses: {
ada: 'addr_test1qzrljm7nskakjydxlr450ktsj08zuw6aktvgfkmmyw9semrkrezryq3ydtmkg0e7e2jvzg443h0ffzfwd09wpcxy2fuql9tk0g'
},
updated_slot_number: 22,
utxo: 'rljm7nskakjydxlr450ktsj08zuw6aktvgfkmmyw9semrkrezryq3ydtmkg0'
};

0 comments on commit 746e311

Please sign in to comment.