Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add fetchCandid() function to @dfinity/agent #630

Merged
merged 9 commits into from Sep 29, 2022
4 changes: 4 additions & 0 deletions docs/generated/changelog.html
Expand Up @@ -16,6 +16,10 @@ <h2>Version 0.14.0</h2>
Adds retry logic to HttpAgent. By default, retries three times before throwing an error,
to offer a more cohesive workflow
</li>
<li>
Adds a top-level <code>fetchCandid()</code> function which retrieves the Candid interface
for a given canister id.
</li>
</ul>
<h2>Version 0.13.4</h2>
<ul>
Expand Down
5 changes: 5 additions & 0 deletions packages/agent/src/agent/http/index.ts
Expand Up @@ -219,6 +219,11 @@ export class HttpAgent implements Agent {
}
}

public isLocal(): boolean {
const hostname = this._host.hostname;
return hostname === '127.0.0.1' || hostname.endsWith('localhost');
}

public addTransform(fn: HttpAgentRequestTransformFn, priority = fn.priority || 0): void {
// Keep the pipeline sorted at all time, by priority.
const i = this._pipeline.findIndex(x => (x.priority || 0) < priority);
Expand Down
27 changes: 27 additions & 0 deletions packages/agent/src/fetch_candid.test.ts
@@ -0,0 +1,27 @@
import { fetchCandid, HttpAgent } from '.';
import { IDL } from '@dfinity/candid';
import * as cbor from './cbor';

test('simulate fetching a Candid interface', async () => {
const mockFetch = jest.fn().mockImplementation((/*resource, init*/) => {
return Promise.resolve(
new Response(
cbor.encode({
status: 'replied',
reply: {
arg: IDL.encode([IDL.Text], ['service {}']),
},
}),
{
status: 200,
},
),
);
});

const agent = new HttpAgent({ fetch: mockFetch, host: 'http://localhost' });

const candid = await fetchCandid('ryjl3-tyaaa-aaaaa-aaaba-cai', agent);

expect(candid).toMatch(/service/);
});
39 changes: 39 additions & 0 deletions packages/agent/src/fetch_candid.ts
@@ -0,0 +1,39 @@
import { Actor, ActorSubclass, CanisterStatus, HttpAgent } from '.';
import { Principal } from '@dfinity/principal';
import { IDL } from '@dfinity/candid';

/**
* Retrieves the Candid interface for the specified canister.
*
* @param agent The agent to use for the request (usually an `HttpAgent`)
* @param canisterId A string corresponding to the canister ID
* @returns Candid source code
*/
export async function fetchCandid(canisterId: string, agent?: HttpAgent): Promise<string> {
if (!agent) {
// Create an anonymous `HttpAgent` (adapted from Candid UI)
agent = new HttpAgent();
if (agent.isLocal()) {
agent.fetchRootKey();
}
}

// Attempt to use canister metadata
const status = await CanisterStatus.request({
agent,
canisterId: Principal.fromText(canisterId),
paths: ['candid'],
});
const candid = status.get('candid') as string | undefined;
if (candid) {
return candid;
}

// Use `__get_candid_interface_tmp_hack` for canisters without Candid metadata
const tmpHackInterface: IDL.InterfaceFactory = ({ IDL }) =>
IDL.Service({
__get_candid_interface_tmp_hack: IDL.Func([], [IDL.Text], ['query']),
});
const actor: ActorSubclass = Actor.createActor(tmpHackInterface, { agent, canisterId });
return (await actor.__get_candid_interface_tmp_hack()) as string;
}
1 change: 1 addition & 0 deletions packages/agent/src/index.ts
Expand Up @@ -8,6 +8,7 @@ export * from './agent/http/transforms';
export * from './agent/http/types';
export * from './canisters/asset';
export * from './canisters/management';
export * from './fetch_candid';
export * from './request_id';
export * from './utils/bls';
export * from './utils/buffer';
Expand Down