Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,41 @@

## [Unreleased]

### Added

* feat: adds support for verified queries against management canister
* includes support for `fetch_canister_logs` in the actor provided by `getManagementCanister`
* also includes support for bitcoin queries

Logging

```ts
// Agent should not use an anonymous identity for this call, and should ideally be a canister controller
const management = await getManagementCanister({ agent });
const logs = await management.fetch_canister_logs({ canister_id: canisterId });
```

Bitcoin

```ts
// For now, the verifyQuerySignatures option must be set to false
const agent = await makeAgent({ host: 'https://icp-api.io', verifyQuerySignatures: false });
const management = getManagementCanister({
agent
});

const result = await management.bitcoin_get_balance_query({
address: 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh',
network: { mainnet: null },
min_confirmations: [6],
});
```

### Changed

* fix: pads date numbers in changelog automation. E.G. 2024-3-1 -> 2024-03-01
* feat: allow passing `DBCreateOptions` to `IdbStorage` constructor
* updated management canister interface

## [1.1.1] - 2024-03-19

Expand Down
31 changes: 31 additions & 0 deletions e2e/node/basic/logging.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { bufFromBufLike, getManagementCanister } from '@dfinity/agent';
import { describe, it, expect } from 'vitest';
import logsActor from '../canisters/logs';
import { makeAgent } from '../utils/agent';

describe('canister logs', () => {
it('should make requests to the management canister', async () => {
const { canisterId } = await logsActor();

const management = await getManagementCanister({ agent: await makeAgent() });
const logs = await management.fetch_canister_logs({ canister_id: canisterId });

expect(logs.canister_log_records.length).toBe(1);
const content = bufFromBufLike(logs.canister_log_records[0].content);

expect(new TextDecoder().decode(content).trim()).toBe('Hello, first call!');
});
it('should show additional logs', async () => {
const { canisterId, actor } = await logsActor();

await actor.hello('second call');

const management = await getManagementCanister({ agent: await makeAgent() });
const logs = await management.fetch_canister_logs({ canister_id: canisterId });

expect(logs.canister_log_records.length).toBe(2);
const content = bufFromBufLike(logs.canister_log_records[1].content);

expect(new TextDecoder().decode(content).trim()).toBe('Hello, second call!');
});
}, 10_000);
27 changes: 26 additions & 1 deletion e2e/node/basic/mainnet.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { Actor, AnonymousIdentity, HttpAgent, Identity, CanisterStatus } from '@dfinity/agent';
import {
Actor,
AnonymousIdentity,
HttpAgent,
Identity,
CanisterStatus,
getManagementCanister,
} from '@dfinity/agent';
import { IDL } from '@dfinity/candid';
import { Ed25519KeyIdentity } from '@dfinity/identity';
import { Principal } from '@dfinity/principal';
Expand Down Expand Up @@ -161,3 +168,21 @@ describe('controllers', () => {
`);
});
});

describe('bitcoin query', async () => {
it('should return the balance of a bitcoin address', async () => {
// TODO - verify node signature for bitcoin once supported
const agent = await makeAgent({ host: 'https://icp-api.io', verifyQuerySignatures: false });
const management = getManagementCanister({
agent,
});

const result = await management.bitcoin_get_balance_query({
address: 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh',
network: { mainnet: null },
min_confirmations: [6],
});
console.log(`balance for address: ${result}`);
expect(result).toBeGreaterThan(0n);
});
});
1 change: 1 addition & 0 deletions e2e/node/basic/mitm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ mitmTest(
const counter = await createActor('tnnnb-2yaaa-aaaab-qaiiq-cai', {
agent: await makeAgent({
host: 'http://127.0.0.1:8888',
verifyQuerySignatures: false,
}),
});
await expect(counter.greet('counter')).rejects.toThrow(/Invalid certificate/);
Expand Down
52 changes: 52 additions & 0 deletions e2e/node/canisters/logs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Principal } from '@dfinity/principal';
import agent from '../utils/agent';
import { readFileSync } from 'fs';
import path from 'path';
import { Actor, ActorMethod, ActorSubclass } from '@dfinity/agent';
import { IDL } from '@dfinity/candid';

export interface _SERVICE {
hello: ActorMethod<[string], undefined>;
}
export declare const init: (args: { IDL: typeof IDL }) => IDL.Type[];

export const idlFactory = ({ IDL }) => {
return IDL.Service({ hello: IDL.Func([IDL.Text], [], []) });
};

let cache: {
canisterId: Principal;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
actor: any;
} | null = null;

/**
* Create a counter Actor + canisterId
*/
export default async function (): Promise<{
canisterId: Principal;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
actor: any;
}> {
if (!cache) {
const module = readFileSync(path.join(__dirname, 'logs.wasm'));

const canisterId = await Actor.createCanister({ agent: await agent });
await Actor.install({ module }, { canisterId, agent: await agent });

const actor = Actor.createActor(idlFactory, {
canisterId,
agent: await agent,
}) as ActorSubclass<_SERVICE>;

await actor.hello('first call');

cache = {
canisterId,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
actor,
};
}

return cache;
}
Binary file added e2e/node/canisters/logs.wasm
Binary file not shown.
2 changes: 0 additions & 2 deletions e2e/node/utils/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ if (Number.isNaN(port)) {
export const makeAgent = async (options?: HttpAgentOptions) => {
const agent = new HttpAgent({
host: `http://127.0.0.1:${process.env.REPLICA_PORT ?? 4943}`,
// TODO - remove this when the dfx replica supports it
verifyQuerySignatures: false,
...options,
});
try {
Expand Down
11 changes: 10 additions & 1 deletion packages/agent/src/actor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,8 @@ export class Actor {
compute_allocation: settings.compute_allocation ? [settings.compute_allocation] : [],
freezing_threshold: settings.freezing_threshold ? [settings.freezing_threshold] : [],
memory_allocation: settings.memory_allocation ? [settings.memory_allocation] : [],
reserved_cycles_limit: [],
log_visibility: [],
},
];
}
Expand Down Expand Up @@ -429,7 +431,11 @@ function _createActorMethod(
const cid = Principal.from(options.canisterId || actor[metadataSymbol].config.canisterId);
const arg = IDL.encode(func.argTypes, args);

const result = await agent.query(cid, { methodName, arg });
const result = await agent.query(cid, {
methodName,
arg,
effectiveCanisterId: options.effectiveCanisterId,
});

switch (result.status) {
case QueryResponseStatus.Rejected:
Expand Down Expand Up @@ -517,6 +523,9 @@ export function getManagementCanister(config: CallConfig): ActorSubclass<Managem
_methodName: string,
args: Record<string, unknown> & { canister_id: string }[],
) {
if (config.effectiveCanisterId) {
return { effectiveCanisterId: Principal.from(config.effectiveCanisterId) };
}
const first = args[0];
let effectiveCanisterId = Principal.fromHex('');
if (first && typeof first === 'object' && first.canister_id) {
Expand Down
5 changes: 5 additions & 0 deletions packages/agent/src/agent/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ export interface QueryFields {
* A binary encoded argument. This is already encoded and will be sent as is.
*/
arg: ArrayBuffer;

/**
* Overrides canister id for path to fetch. This is used for management canister calls.
*/
effectiveCanisterId?: Principal;
}

/**
Expand Down
69 changes: 49 additions & 20 deletions packages/agent/src/agent/http/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,14 @@ export enum RequestStatusResponseStatus {
const DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS = 5 * 60 * 1000;

// Root public key for the IC, encoded as hex
const IC_ROOT_KEY =
export const IC_ROOT_KEY =
'308182301d060d2b0601040182dc7c0503010201060c2b0601040182dc7c05030201036100814' +
'c0e6ec71fab583b08bd81373c255c3c371b2e84863c98a4f1e08b74235d14fb5d9c0cd546d968' +
'5f913a0c0b2cc5341583bf4b4392e467db96d65b9bb4cb717112f8472e0d5a4d14505ffd7484' +
'b01291091c5f87b98883463f98091a0baaae';

export const MANAGEMENT_CANISTER_ID = 'aaaaa-aa';

// IC0 domain info
const IC0_DOMAIN = 'ic0.app';
const IC0_SUB_DOMAIN = '.ic0.app';
Expand Down Expand Up @@ -396,6 +398,8 @@ export class HttpAgent implements Agent {

const body = cbor.encode(transformedRequest.body);

this.log(`fetching "/api/v2/canister/${ecid.toText()}/call" with request:`, transformedRequest);

// Run both in parallel. The fetch is quite expensive, so we have plenty of time to
// calculate the requestId locally.
const request = this._requestAndRetry(() =>
Expand Down Expand Up @@ -427,36 +431,50 @@ export class HttpAgent implements Agent {

async #requestAndRetryQuery(
args: {
canister: string;
ecid: Principal;
transformedRequest: HttpAgentRequest;
body: ArrayBuffer;
requestId: RequestId;
},
tries = 0,
): Promise<ApiQueryResponse> {
const { canister, transformedRequest, body, requestId } = args;
const { ecid, transformedRequest, body, requestId } = args;
let response: ApiQueryResponse;
// Make the request and retry if it throws an error
try {
const fetchResponse = await this._fetch(
'' + new URL(`/api/v2/canister/${canister}/query`, this._host),
'' + new URL(`/api/v2/canister/${ecid.toString()}/query`, this._host),
{
...this._fetchOptions,
...transformedRequest.request,
body,
},
);
const queryResponse: QueryResponse = cbor.decode(await fetchResponse.arrayBuffer());
response = {
...queryResponse,
httpDetails: {
ok: fetchResponse.ok,
status: fetchResponse.status,
statusText: fetchResponse.statusText,
headers: httpHeadersTransform(fetchResponse.headers),
},
requestId,
};
if (fetchResponse.status === 200) {
const queryResponse: QueryResponse = cbor.decode(await fetchResponse.arrayBuffer());
response = {
...queryResponse,
httpDetails: {
ok: fetchResponse.ok,
status: fetchResponse.status,
statusText: fetchResponse.statusText,
headers: httpHeadersTransform(fetchResponse.headers),
},
requestId,
};
} else {
throw new AgentHTTPResponseError(
`Server returned an error:\n` +
` Code: ${fetchResponse.status} (${fetchResponse.statusText})\n` +
` Body: ${await fetchResponse.text()}\n`,
{
ok: fetchResponse.ok,
status: fetchResponse.status,
statusText: fetchResponse.statusText,
headers: httpHeadersTransform(fetchResponse.headers),
},
);
}
} catch (error) {
if (tries < this._retryTimes) {
this.log.warn(
Expand Down Expand Up @@ -553,7 +571,12 @@ export class HttpAgent implements Agent {
fields: QueryFields,
identity?: Identity | Promise<Identity>,
): Promise<ApiQueryResponse> {
this.log(`making query to canister ${canisterId} with fields:`, fields);
const ecid = fields.effectiveCanisterId
? Principal.from(fields.effectiveCanisterId)
: Principal.from(canisterId);

this.log(`ecid ${ecid.toString()}`);
this.log(`canisterId ${canisterId.toString()}`);
const makeQuery = async () => {
const id = await (identity !== undefined ? await identity : await this._identity);
if (!id) {
Expand Down Expand Up @@ -597,6 +620,7 @@ export class HttpAgent implements Agent {

const args = {
canister: canister.toText(),
ecid,
transformedRequest,
body,
requestId,
Expand All @@ -609,12 +633,12 @@ export class HttpAgent implements Agent {
if (!this.#verifyQuerySignatures) {
return undefined;
}
const subnetStatus = this.#subnetKeys.get(canisterId.toString());
const subnetStatus = this.#subnetKeys.get(ecid.toString());
if (subnetStatus) {
return subnetStatus;
}
await this.fetchSubnetKeys(canisterId.toString());
return this.#subnetKeys.get(canisterId.toString());
await this.fetchSubnetKeys(ecid.toString());
return this.#subnetKeys.get(ecid.toString());
};
// Attempt to make the query i=retryTimes times
// eslint-disable-next-line @typescript-eslint/no-unused-vars
Expand All @@ -633,7 +657,7 @@ export class HttpAgent implements Agent {
// In case the node signatures have changed, refresh the subnet keys and try again
this.log.warn('Query response verification failed. Retrying with fresh subnet keys.');
this.#subnetKeys.delete(canisterId.toString());
await this.fetchSubnetKeys(canisterId.toString());
await this.fetchSubnetKeys(ecid.toString());

const updatedSubnetStatus = this.#subnetKeys.get(canisterId.toString());
if (!updatedSubnetStatus) {
Expand Down Expand Up @@ -767,6 +791,10 @@ export class HttpAgent implements Agent {
const transformedRequest = request ?? (await this.createReadStateRequest(fields, identity));
const body = cbor.encode(transformedRequest.body);

this.log(
`fetching "/api/v2/canister/${canister}/read_state" with request:`,
transformedRequest,
);
// TODO - https://dfinity.atlassian.net/browse/SDK-1092
const response = await this._requestAndRetry(() =>
this._fetch('' + new URL(`/api/v2/canister/${canister}/read_state`, this._host), {
Expand Down Expand Up @@ -858,6 +886,7 @@ export class HttpAgent implements Agent {
}
: {};

this.log(`fetching "/api/v2/status"`);
const response = await this._requestAndRetry(() =>
this._fetch('' + new URL(`/api/v2/status`, this._host), { headers, ...this._fetchOptions }),
);
Expand Down
Loading