Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cardano-services-client): add KoraLabsHandleProvider
- Loading branch information
Showing
6 changed files
with
284 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
102 changes: 102 additions & 0 deletions
102
packages/cardano-services-client/src/HandleProvider/KoraLabsHandleProvider.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 }; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './KoraLabsHandleProvider'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
104 changes: 104 additions & 0 deletions
104
packages/cardano-services-client/test/HandleProvider/KoraLabsHandleProvider.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters