diff --git a/docs/generated/changelog.html b/docs/generated/changelog.html
index aee071bf8..c23d4d2c0 100644
--- a/docs/generated/changelog.html
+++ b/docs/generated/changelog.html
@@ -16,6 +16,10 @@
Version 0.14.0
Adds retry logic to HttpAgent. By default, retries three times before throwing an error,
to offer a more cohesive workflow
+
+ Adds a top-level fetchCandid()
function which retrieves the Candid interface
+ for a given canister id.
+
Version 0.13.4
diff --git a/packages/agent/src/agent/http/index.ts b/packages/agent/src/agent/http/index.ts
index 839c72c80..399a9a888 100644
--- a/packages/agent/src/agent/http/index.ts
+++ b/packages/agent/src/agent/http/index.ts
@@ -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);
diff --git a/packages/agent/src/fetch_candid.test.ts b/packages/agent/src/fetch_candid.test.ts
new file mode 100644
index 000000000..5f6ae1335
--- /dev/null
+++ b/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/);
+});
diff --git a/packages/agent/src/fetch_candid.ts b/packages/agent/src/fetch_candid.ts
new file mode 100644
index 000000000..a6db5bc36
--- /dev/null
+++ b/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 {
+ 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;
+}
diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts
index 35bfa4770..f784eecb0 100644
--- a/packages/agent/src/index.ts
+++ b/packages/agent/src/index.ts
@@ -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';