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!: support getting certificate back from call #892

Merged
merged 36 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
3f825b8
wip
krpeacock Jun 10, 2024
8fc63ab
working call with certificate annotation
krpeacock Jun 10, 2024
6417bb9
feat: adds createActorWithExtendedDetails method
krpeacock Jun 10, 2024
bebafdd
feat: ingress_expiry and nonce inside of requestDetails in extended c…
krpeacock Jun 10, 2024
e0a33c7
skipping a test
krpeacock Jun 11, 2024
c815b2c
running call and polling for demo test
krpeacock Jun 12, 2024
a2867a9
wip
krpeacock Jun 12, 2024
b801c46
test: moves call forwarding test to mainnet suite
krpeacock Jun 14, 2024
24c0a76
chore: cleaning up comments and logs
krpeacock Jun 14, 2024
f0e4f68
more comprehensive return types
krpeacock Jun 14, 2024
3f0e1f0
test for request detail totality
krpeacock Jun 14, 2024
fcee464
Merge branch 'main' into kai/SDK-1717-raw-call
krpeacock Jun 14, 2024
38cb045
Merge branch 'main' into kai/SDK-1717-raw-call
krpeacock Jun 14, 2024
0f3377f
Merge branch 'main' into kai/SDK-1717-raw-call
dfx-json Jun 18, 2024
473ff04
restoring use fake timers
krpeacock Jun 20, 2024
28868a4
test: restoring skipped test
krpeacock Jun 20, 2024
2fbc8ba
Merge branch 'kai/SDK-1717-raw-call' of github.com:dfinity/agent-js i…
krpeacock Jun 20, 2024
3a55832
replacing localhost for cypress
krpeacock Jun 20, 2024
6ed5949
upgrading dependencies
krpeacock Jun 20, 2024
12ab426
package lock
krpeacock Jun 20, 2024
82ccbcc
trying cypress action
krpeacock Jun 20, 2024
6120d2e
Merge branch 'main' into kai/SDK-1717-raw-call
krpeacock Jun 21, 2024
8f9d202
Merge branch 'main' into kai/SDK-1717-raw-call
krpeacock Jun 21, 2024
54c2fde
Merge branch 'kai/SDK-1717-raw-call' of github.com:dfinity/agent-js i…
krpeacock Jun 24, 2024
175d950
reverting ecdsa.cy.js changes
krpeacock Jun 24, 2024
4d07e46
fixing cypress test
krpeacock Jun 24, 2024
c46b258
Merge branch 'main' into kai/SDK-1717-raw-call
krpeacock Jun 24, 2024
615d645
reverting more changes for consistency with main
krpeacock Jun 24, 2024
bee999e
Update packages/agent/src/actor.test.ts
krpeacock Jun 24, 2024
28e9a4e
package lock
krpeacock Jun 24, 2024
46df80a
Merge branch 'kai/SDK-1717-raw-call' of github.com:dfinity/agent-js i…
krpeacock Jun 24, 2024
5c687ca
switching to vite
krpeacock Jun 24, 2024
b43f472
vite port
krpeacock Jun 24, 2024
f9302d1
Merge branch 'main' into kai/SDK-1717-raw-call
krpeacock Jun 26, 2024
a1e9851
using test.each for the repeated queries
krpeacock Jun 26, 2024
d2ade5e
package lock
krpeacock Jun 26, 2024
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
45 changes: 44 additions & 1 deletion e2e/node/basic/mainnet.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import { Actor, AnonymousIdentity, HttpAgent, Identity, CanisterStatus } from '@dfinity/agent';
import {
Actor,
AnonymousIdentity,
HttpAgent,
Identity,
CanisterStatus,
fromHex,
polling,
requestIdOf,
} from '@dfinity/agent';
import { IDL } from '@dfinity/candid';
import { Ed25519KeyIdentity } from '@dfinity/identity';
import { Principal } from '@dfinity/principal';
import { describe, it, expect, vi } from 'vitest';
import { makeAgent } from '../utils/agent';

const { defaultStrategy, pollForResponse } = polling;

const createWhoamiActor = async (identity: Identity) => {
const canisterId = 'ivcos-eqaaa-aaaab-qablq-cai';
const idlFactory = () => {
Expand Down Expand Up @@ -161,3 +172,35 @@ describe('controllers', () => {
});
});

describe('call forwarding', () => {
it('should handle call forwarding', async () => {
vi.useRealTimers();
const forwardedOptions = {
canisterId: 'tnnnb-2yaaa-aaaab-qaiiq-cai',
methodName: 'inc_read',
arg: '4449444c0000',
effectiveCanisterId: 'tnnnb-2yaaa-aaaab-qaiiq-cai',
};

const agent = new HttpAgent({ host: 'https://icp-api.io' });
const { requestId, requestDetails } = await agent.call(
Principal.fromText(forwardedOptions.canisterId),
{
methodName: forwardedOptions.methodName,
arg: fromHex(forwardedOptions.arg),
effectiveCanisterId: Principal.fromText(forwardedOptions.effectiveCanisterId),
},
);

expect(requestIdOf(requestDetails!)).toStrictEqual(requestId);

const { certificate, reply } = await pollForResponse(
agent,
Principal.fromText(forwardedOptions.effectiveCanisterId),
requestId,
defaultStrategy(),
);
certificate; // Certificate
reply; // ArrayBuffer
}, 15_000);
});
5 changes: 4 additions & 1 deletion packages/agent/src/actor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@ describe('makeActor', () => {
body: cbor.encode(expectedErrorCallRequest),
});
});
it('should enrich actor interface with httpDetails', async () => {
// TODO: fix this test
it.skip('should enrich actor interface with httpDetails', async () => {
const canisterDecodedReturnValue = 'Hello, World!';
const expectedReplyArg = IDL.encode([IDL.Text], [canisterDecodedReturnValue]);
const { Actor } = await importActor(() =>
Expand Down Expand Up @@ -342,3 +343,5 @@ describe('makeActor', () => {
});
});
// TODO: tests for rejected, unknown time out

jest.setTimeout(20000);
108 changes: 96 additions & 12 deletions packages/agent/src/actor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { pollForResponse, PollStrategyFactory, strategy } from './polling';
import { Principal } from '@dfinity/principal';
import { RequestId } from './request_id';
import { toHex } from './utils/buffer';
import { CreateCertificateOptions } from './certificate';
import { Certificate, CreateCertificateOptions } from './certificate';
import managementCanisterIdl from './canisters/management_idl';
import _SERVICE, { canister_install_mode, canister_settings } from './canisters/management_service';

Expand Down Expand Up @@ -159,6 +159,18 @@ export interface ActorMethodWithHttpDetails<Args extends unknown[] = unknown[],
(...args: Args): Promise<{ httpDetails: HttpDetailsResponse; result: Ret }>;
}

/**
* An actor method type, defined for each methods of the actor service.
*/
export interface ActorMethodExtended<Args extends unknown[] = unknown[], Ret = unknown>
extends ActorMethod {
(...args: Args): Promise<{
certificate?: Certificate;
httpDetails?: HttpDetailsResponse;
result: Ret;
}>;
}

export type FunctionWithArgsAndReturn<Args extends unknown[] = unknown[], Ret = unknown> = (
...args: Args
) => Ret;
Expand All @@ -170,6 +182,13 @@ export type ActorMethodMappedWithHttpDetails<T> = {
: never;
};

// Update all entries of T with the extra information from ActorMethodWithInfo
export type ActorMethodMappedExtended<T> = {
[K in keyof T]: T[K] extends FunctionWithArgsAndReturn<infer Args, infer Ret>
? ActorMethodExtended<Args, Ret>
: never;
};

/**
* The mode used when installing a canister.
*/
Expand Down Expand Up @@ -205,6 +224,7 @@ const metadataSymbol = Symbol.for('ic-agent-metadata');

export interface CreateActorClassOpts {
httpDetails?: boolean;
certificate?: boolean;
}

interface CreateCanisterSettings {
Expand Down Expand Up @@ -348,6 +368,9 @@ export class Actor {
if (options?.httpDetails) {
func.annotations.push(ACTOR_METHOD_WITH_HTTP_DETAILS);
}
if (options?.certificate) {
func.annotations.push(ACTOR_METHOD_WITH_CERTIFICATE);
}

this[methodName] = _createActorMethod(this, methodName, func, config.blsVerify);
}
Expand All @@ -371,6 +394,12 @@ export class Actor {
) as unknown as ActorSubclass<T>;
}

/**
* Returns an actor with methods that return the http response details along with the result
* @param interfaceFactory - the interface factory for the actor
* @param configuration - the configuration for the actor
* @deprecated - use createActor with actorClassOptions instead
*/
public static createActorWithHttpDetails<T = Record<string, ActorMethod>>(
interfaceFactory: IDL.InterfaceFactory,
configuration: ActorConfig,
Expand All @@ -380,6 +409,25 @@ export class Actor {
) as unknown as ActorSubclass<ActorMethodMappedWithHttpDetails<T>>;
}

/**
* Returns an actor with methods that return the http response details along with the result
* @param interfaceFactory - the interface factory for the actor
* @param configuration - the configuration for the actor
* @param actorClassOptions - options for the actor class extended details to return with the result
*/
public static createActorWithExtendedDetails<T = Record<string, ActorMethod>>(
interfaceFactory: IDL.InterfaceFactory,
configuration: ActorConfig,
actorClassOptions: CreateActorClassOpts = {
httpDetails: true,
certificate: true,
},
): ActorSubclass<ActorMethodMappedExtended<T>> {
return new (this.createActorClass(interfaceFactory, actorClassOptions))(
configuration,
) as unknown as ActorSubclass<ActorMethodMappedExtended<T>>;
}

private [metadataSymbol]: ActorMetadata;

protected constructor(metadata: ActorMetadata) {
Expand Down Expand Up @@ -409,6 +457,7 @@ const DEFAULT_ACTOR_CONFIG = {
export type ActorConstructor = new (config: ActorConfig) => ActorSubclass;

export const ACTOR_METHOD_WITH_HTTP_DETAILS = 'http-details';
export const ACTOR_METHOD_WITH_CERTIFICATE = 'certificate';

function _createActorMethod(
actor: Actor,
Expand Down Expand Up @@ -437,6 +486,10 @@ function _createActorMethod(
arg,
effectiveCanisterId: options.effectiveCanisterId,
});
const httpDetails = {
...result.httpDetails,
requestDetails: result.requestDetails,
} as HttpDetailsResponse;

switch (result.status) {
case QueryResponseStatus.Rejected:
Expand All @@ -445,7 +498,7 @@ function _createActorMethod(
case QueryResponseStatus.Replied:
return func.annotations.includes(ACTOR_METHOD_WITH_HTTP_DETAILS)
? {
httpDetails: result.httpDetails,
httpDetails,
result: decodeReturnValue(func.retTypes, result.reply.arg),
}
: decodeReturnValue(func.retTypes, result.reply.arg);
Expand All @@ -471,7 +524,8 @@ function _createActorMethod(
const cid = Principal.from(canisterId);
const ecid = effectiveCanisterId !== undefined ? Principal.from(effectiveCanisterId) : cid;
const arg = IDL.encode(func.argTypes, args);
const { requestId, response } = await agent.call(cid, {

const { requestId, response, requestDetails } = await agent.call(cid, {
methodName,
arg,
effectiveCanisterId: ecid,
Expand All @@ -482,16 +536,40 @@ function _createActorMethod(
}

const pollStrategy = pollingStrategyFactory();
const responseBytes = await pollForResponse(agent, ecid, requestId, pollStrategy, blsVerify);
// Contains the certificate and the reply from the boundary node
const { certificate, reply } = await pollForResponse(
agent,
ecid,
requestId,
pollStrategy,
blsVerify,
);
const shouldIncludeHttpDetails = func.annotations.includes(ACTOR_METHOD_WITH_HTTP_DETAILS);

if (responseBytes !== undefined) {
return shouldIncludeHttpDetails
? {
httpDetails: response,
result: decodeReturnValue(func.retTypes, responseBytes),
}
: decodeReturnValue(func.retTypes, responseBytes);
const shouldIncludeCertificate = func.annotations.includes(ACTOR_METHOD_WITH_CERTIFICATE);

const httpDetails = { ...response, requestDetails } as HttpDetailsResponse;

reply;

if (reply !== undefined) {
if (shouldIncludeHttpDetails && shouldIncludeCertificate) {
return {
httpDetails,
certificate,
result: decodeReturnValue(func.retTypes, reply),
};
} else if (shouldIncludeCertificate) {
return {
certificate,
result: decodeReturnValue(func.retTypes, reply),
};
} else if (shouldIncludeHttpDetails) {
return {
httpDetails,
result: decodeReturnValue(func.retTypes, reply),
};
}
return decodeReturnValue(func.retTypes, reply);
} else if (func.retTypes.length === 0) {
return shouldIncludeHttpDetails
? {
Expand Down Expand Up @@ -544,3 +622,9 @@ export function getManagementCanister(config: CallConfig): ActorSubclass<Managem
},
});
}

export class AdvancedActor extends Actor {
constructor(metadata: ActorMetadata) {
super(metadata);
}
}
4 changes: 3 additions & 1 deletion packages/agent/src/agent/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Principal } from '@dfinity/principal';
import { RequestId } from '../request_id';
import { JsonObject } from '@dfinity/candid';
import { Identity } from '../auth';
import { HttpHeaderField } from './http/types';
import { CallRequest, HttpHeaderField, QueryRequest } from './http/types';

/**
* Codes used by the replica for rejecting a message.
Expand Down Expand Up @@ -50,6 +50,7 @@ export type ApiQueryResponse = QueryResponse & {

export interface QueryResponseBase {
status: QueryResponseStatus;
requestDetails?: QueryRequest;
}

export type NodeSignature = {
Expand Down Expand Up @@ -133,6 +134,7 @@ export interface SubmitResponse {
} | null;
headers: HttpHeaderField[];
};
requestDetails?: CallRequest;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

exports[`retry failures should succeed after multiple failures within the configured limit 1`] = `
{
"requestDetails": undefined,
"requestId": ArrayBuffer [],
"response": {
"body": null,
Expand Down
4 changes: 3 additions & 1 deletion packages/agent/src/agent/http/http.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -588,12 +588,14 @@ describe('retry failures', () => {
methodName: 'test',
arg: new Uint8Array().buffer,
});
// Remove the request details to make the snapshot consistent
result.requestDetails = undefined;
expect(result).toMatchSnapshot();
// One try + three retries
expect(mockFetch.mock.calls.length).toBe(4);
});
});
jest.useFakeTimers({ legacyFakeTimers: true });
// jest.useFakeTimers({ legacyFakeTimers: true });
krpeacock marked this conversation as resolved.
Show resolved Hide resolved

test('should adjust the Expiry if the clock is more than 30 seconds behind', async () => {
const mockFetch = jest.fn();
Expand Down
33 changes: 27 additions & 6 deletions packages/agent/src/agent/http/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,16 @@ export class HttpAgent implements Agent {
body: submit,
})) as HttpAgentSubmitRequest;

const nonce: Nonce | undefined = transformedRequest.body.nonce
? toNonce(transformedRequest.body.nonce)
: undefined;

submit.nonce = nonce;

function toNonce(buf: ArrayBuffer): Nonce {
return new Uint8Array(buf) as Nonce;
}

// Apply transform for identity.
transformedRequest = await id.transformRequest(transformedRequest);

Expand Down Expand Up @@ -449,6 +459,7 @@ export class HttpAgent implements Agent {
body: responseBody,
headers: httpHeadersTransform(response.headers),
},
requestDetails: submit,
};
}

Expand Down Expand Up @@ -696,7 +707,10 @@ export class HttpAgent implements Agent {
tries: 0,
};

return await this.#requestAndRetryQuery(args);
return {
requestDetails: request,
query: await this.#requestAndRetryQuery(args),
};
};

const getSubnetStatus = async (): Promise<SubnetStatus | void> => {
Expand All @@ -712,16 +726,22 @@ export class HttpAgent implements Agent {
};
// Attempt to make the query i=retryTimes times
// Make query and fetch subnet keys in parallel
const [query, subnetStatus] = await Promise.all([makeQuery(), getSubnetStatus()]);
const [queryResult, subnetStatus] = await Promise.all([makeQuery(), getSubnetStatus()]);
const { requestDetails, query } = queryResult;

const queryWithDetails = {
...query,
requestDetails,
};

this.log.print('Query response:', query);
this.log.print('Query response:', queryWithDetails);
// Skip verification if the user has disabled it
if (!this.#verifyQuerySignatures) {
return query;
return queryWithDetails;
}

try {
return this.#verifyQueryResponse(query, subnetStatus);
return this.#verifyQueryResponse(queryWithDetails, subnetStatus);
} catch (_) {
// 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.');
Expand All @@ -734,7 +754,7 @@ export class HttpAgent implements Agent {
'Invalid signature from replica signed query: no matching node key found.',
);
}
return this.#verifyQueryResponse(query, updatedSubnetStatus);
return this.#verifyQueryResponse(queryWithDetails, updatedSubnetStatus);
}
}

Expand Down Expand Up @@ -1023,3 +1043,4 @@ export class HttpAgent implements Agent {
return p;
}
}

Loading
Loading