Skip to content

Commit

Permalink
Merge branch 'main' into kai/SDK-1719-max-age
Browse files Browse the repository at this point in the history
  • Loading branch information
dfx-json authored Oct 21, 2024
2 parents cf742f6 + 69a92f7 commit 04c7bc3
Show file tree
Hide file tree
Showing 90 changed files with 12,967 additions and 10,580 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/e2e-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ jobs:
run: |
dfx start --background
- name: deploy canisters
run: |
dfx deploy counter
dfx deploy trap
- name: Node.js e2e tests
run: npm run e2e --workspace e2e/node
env:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ dist
docs/agent
docs/assets
docs/auth-client
docs/use-auth-client
docs/authentication
docs/identity
docs/identity-secp256k1
Expand Down
2 changes: 1 addition & 1 deletion bin/version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ workspaces.forEach(async workspace => {
json.devDependencies = updateDeps(json.devDependencies);
}

const formatted = prettier.format(JSON.stringify(json), {
const formatted = await prettier.format(JSON.stringify(json), {
parser: 'json-stringify',
});

Expand Down
4 changes: 4 additions & 0 deletions dfx.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
"counter": {
"type": "motoko",
"main": "e2e/node/canisters/counter.mo"
},
"trap": {
"type": "motoko",
"main": "e2e/node/canisters/trap.mo"
}
}
}
55 changes: 55 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,65 @@

- feat: allow for setting HttpAgent ingress expiry using `ingressExpiryInMinutes` option

### Changed

- test: automatically deploys trap canister if it doesn't exist yet during e2e
- fix: handle v3 traps correctly, pulling the reject_code and message from the certificate in the error response like v2.
Example trap error message:
```txt
AgentError: Call failed:
Canister: hbrpn-74aaa-aaaaa-qaaxq-cai
Method: Throw (update)
"Request ID": "ae107dfd7c9be168a8ebc122d904900a95e3f15312111d9e0c08f136573c5f13"
"Error code": "IC0406"
"Reject code": "4"
"Reject message": "foo"
```
- feat: the `UpdateCallRejected` error now exposes `reject_code: ReplicaRejectCode`, `reject_message: string`, and `error_code?: string` properties directly on the error object.
- fix: recalculates body to use a fresh `Expiry` when polling for `read_state` requests. This prevents the request from exceeding the `maximum_ingress_expiry` when the replica is slow to respond.

## [2.1.2] - 2024-09-30
- fix: revert https://github.com/dfinity/agent-js/pull/923 allow option to set agent replica time

## [2.1.1] - 2024-09-13

### Added

- fix: support for headers during upload with `@dfinity/assets`

## [2.1.0] - 2024-09-12

### Added

- chore: awaits prettier formatting in release automation
- feat: expose inner certificate in `Certificate` for inspection or use in raw calls. `Certificate.cert` is now a public property
- feat: allow creation of multiple Actors in `useAuthClient` by passing a record to `actorOptions` with the actor name as the key, and `CreateActorOptions` as the value
- feat: sync_call support in HttpAgent and Actor
- Skips polling if the sync call succeeds and provides a certificate
- Falls back to v2 api if the v3 endpoint 404's
- Adds certificate to SubmitResponse endpoint
- adds callSync option to `HttpAgent.call`, which defaults to `true`
- feat: management canister interface updates for schnorr signatures
- feat: ensure that identity-secp256k1 seed phrase must produce a 64 byte seed
- docs: documentation and metadata for use-auth-client
- feat: adds optional `rootKey` to `HttpAgentOptions` to allow for a custom root key to be used for verifying signatures from other networks
- chore: npm audit bumping micromatch
- feat: exports polling utilities from `@dfinity/agent` for use in other packages
- `pollForResponse` now uses the default strategy by default
- Updated the `bls-verify` jsdoc comment to accurately reflect that the default strategy now uses @noble/curves
- docs: clarifies meaning of `effectiveCanisterId` in `CallOptions`

### Changed
- feat: replaces hdkey and bip32 implementations with `@scure/bip39` and `@scure/bip32` due to vulnerability and lack of maintenance for `elliptic`
- chore: bumps dev dependency versions to remove warnings
- chore: addresses eslint errors uncovered by bumping eslint version

## [2.0.0] - 2024-07-16

### Changed

- fix: passing `request` correctly during pollForResponse `Processing` status
- credit: [Senior Joinu](https://forum.dfinity.org/t/timestamp-failed-to-pass-the-watermark-after-retrying-the-configured-3-times/29180/11?)
- ci: removing headless browser tests pending a rewrite
- ci: changing token for creating release

Expand Down
1 change: 1 addition & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<a href="/principal/index.html">Principal</a>
<a href="/candid/index.html">Candid</a>
<a href="/auth-client/index.html">Auth-Client</a>
<a href="/use-auth-client/index.html">useAuthClient (React Hook)</a>
<a href="/assets/index.html">Asset Manager</a>
<a href="/identity/index.html">Identity</a>
<a href="/identity-secp256k1/index.html">Secp256k1 Identity</a>
Expand Down
48 changes: 36 additions & 12 deletions e2e/node/basic/counter.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import counterCanister, { createActor } from '../canisters/counter';
import { Actor, HttpAgent } from '@dfinity/agent';
import counterCanister, { idl } from '../canisters/counter';
import { it, expect, describe, vi } from 'vitest';

describe('counter', () => {
Expand Down Expand Up @@ -37,26 +38,49 @@ describe('counter', () => {
describe('retrytimes', () => {
it('should retry after a failure', async () => {
let count = 0;
const { canisterId } = await counterCanister();
const fetchMock = vi.fn(function (...args) {
if (count <= 1) {
count += 1;
count += 1;
// let the first 3 requests pass, then throw an error on the call
if (count === 3) {
return new Response('Test error - ignore', {
status: 500,
statusText: 'Internal Server Error',
});
}

// eslint-disable-next-line prefer-spread
return fetch.apply(
null,
args as [input: string | Request, init?: RequestInit | CMRequestInit | undefined],
);
return fetch.apply(null, args as [input: string | Request, init?: RequestInit | undefined]);
});

const counter = await createActor({ fetch: fetchMock as typeof fetch, retryTimes: 3 });
try {
expect(await counter.greet('counter')).toEqual('Hello, counter!');
} catch (error) {
console.error(error);
const counter = await Actor.createActor(idl, {
canisterId,
agent: await HttpAgent.create({
fetch: fetchMock as typeof fetch,
retryTimes: 3,
host: 'http://localhost:4943',
shouldFetchRootKey: true,
}),
});

const result = await counter.greet('counter');
expect(result).toEqual('Hello, counter!');

// The number of calls should be 4 or more, depending on whether the test environment is using v3 or v2
if (findV2inCalls(fetchMock.mock.calls as [string, Request][]) === -1) {
// TODO - pin to 4 once dfx v0.23.0 is released
expect(fetchMock.mock.calls.length).toBe(4);
} else {
expect(fetchMock.mock.calls.length).toBeGreaterThanOrEqual(4);
}
}, 40000);
});

const findV2inCalls = (calls: [string, Request][]) => {
for (let i = 0; i < calls.length; i++) {
if (calls[i][0].includes('v2')) {
return i;
}
}
return -1;
};
10 changes: 4 additions & 6 deletions e2e/node/basic/identity.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Actor, SignIdentity } from '@dfinity/agent';
import { IDL } from '@dfinity/candid';
import { Principal } from '@dfinity/principal';
import {
DelegationChain,
Expand Down Expand Up @@ -28,7 +27,7 @@ function createSecpIdentity(seed: number): SignIdentity {
async function createIdentityActor(
seed: number,
canisterId: Principal,
idl: IDL.InterfaceFactory,
idl,
): Promise<any> {
const identity = createIdentity(seed);
const agent1 = await makeAgent({ identity });
Expand All @@ -40,7 +39,7 @@ async function createIdentityActor(

async function createSecp256k1IdentityActor(
canisterId: Principal,
idl: IDL.InterfaceFactory,
idl,
seed?: number,
): Promise<any> {
let seed1: Uint8Array | undefined;
Expand All @@ -59,10 +58,9 @@ async function createSecp256k1IdentityActor(

async function createEcdsaIdentityActor(
canisterId: Principal,
idl: IDL.InterfaceFactory,
idl,
identity?: SignIdentity,
): Promise<any> {
global.crypto;
let effectiveIdentity: SignIdentity;
if (identity) {
effectiveIdentity = identity;
Expand All @@ -80,7 +78,7 @@ async function createEcdsaIdentityActor(

async function installIdentityCanister(): Promise<{
canisterId: Principal;
idl: IDL.InterfaceFactory;
idl;
}> {
const { canisterId, idl } = await identityCanister();
return {
Expand Down
23 changes: 20 additions & 3 deletions e2e/node/basic/mainnet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ const createWhoamiActor = async (identity: Identity) => {
const idlFactory = () => {
return IDL.Service({
whoami: IDL.Func([], [IDL.Principal], ['query']),
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as unknown as any;
};
vi.useFakeTimers();
new Date(Date.now());
Expand Down Expand Up @@ -136,8 +137,8 @@ describe('call forwarding', () => {
requestId,
defaultStrategy(),
);
certificate; // Certificate
reply; // ArrayBuffer
expect(certificate).toBeTruthy();
expect(reply).toBeTruthy();
}, 15_000);
});

Expand Down Expand Up @@ -185,4 +186,20 @@ test('it should fail when setting an expiry in the far future', async () => {
ingressExpiryInMinutes: 100,
}),
).rejects.toThrowError(`The maximum ingress expiry time is 5 minutes`);

test('it should allow you to set an incorrect root key', async () => {
const agent = HttpAgent.createSync({
rootKey: new Uint8Array(31),
});
const idlFactory = ({ IDL }) =>
IDL.Service({
whoami: IDL.Func([], [IDL.Principal], ['query']),
});

const actor = Actor.createActor(idlFactory, {
agent,
canisterId: Principal.fromText('rrkah-fqaaa-aaaaa-aaaaq-cai'),
});

expect(actor.whoami).rejects.toThrowError(`Invalid certificate:`);
});
56 changes: 56 additions & 0 deletions e2e/node/basic/trap.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { describe, it, expect } from 'vitest';
import { ActorMethod, Actor, HttpAgent } from '@dfinity/agent';
import util from 'util';
import exec from 'child_process';
const execAsync = util.promisify(exec.exec);

// eslint-disable-next-line prefer-const
let stdout;
try {
({ stdout } = await execAsync('dfx canister id trap'));
} catch {
await execAsync('dfx deploy trap');
({ stdout } = await execAsync('dfx canister id trap'));
}

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

export interface _SERVICE {
Throw: ActorMethod<[], undefined>;
test: ActorMethod<[], undefined>;
}

describe('trap', () => {
it('should trap', async () => {
const canisterId = stdout.trim();
const agent = await HttpAgent.create({
host: 'http://localhost:4943',
shouldFetchRootKey: true,
});
const actor = Actor.createActor<_SERVICE>(idlFactory, { canisterId, agent });
try {
await actor.Throw();
} catch (error) {
console.log(error);
expect(error.reject_message).toBe('foo');
}
});
it('should trap', async () => {
const canisterId = stdout.trim();
const agent = await HttpAgent.create({
host: 'http://localhost:4943',
shouldFetchRootKey: true,
});
const actor = Actor.createActor<_SERVICE>(idlFactory, { canisterId, agent });
try {
await actor.test();
} catch (error) {
expect(error.reject_message).toContain('Canister called `ic0.trap` with message: trapping');
}
});
});
39 changes: 32 additions & 7 deletions e2e/node/basic/watermark.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ function indexOfQueryResponse(history: Response[]) {

test('basic', async () => {
const fetchProxy = new FetchProxy();
global.fetch;

const actor = await createActor({
fetch: fetchProxy.fetch.bind(fetchProxy),
Expand All @@ -60,7 +59,6 @@ test('basic', async () => {

test('replay queries only', async () => {
const fetchProxy = new FetchProxy();
global.fetch;

const actor = await createActor({
fetch: fetchProxy.fetch.bind(fetchProxy),
Expand All @@ -83,7 +81,6 @@ test('replay queries only', async () => {

test('replay attack', async () => {
const fetchProxy = new FetchProxy();
global.fetch;

const actor = await createActor({
verifyQuerySignatures: true,
Expand Down Expand Up @@ -117,16 +114,44 @@ test('replay attack', async () => {
expect(startValue3).toBe(1n);

const queryResponseIndex = indexOfQueryResponse(fetchProxy.history);
console.log(queryResponseIndex);

fetchProxy.replayFromHistory(queryResponseIndex);

// the replayed request should throw an error
expect(fetchProxy.calls).toBe(7);
// The number of calls should be 4 or more, depending on whether the test environment is using v3 or v2
const usingV2 =
findV2inCalls(
fetchProxy.history.map(response => {
return [response.url];
}),
) !== -1;
if (usingV2) {
// TODO - pin to 5 once dfx v0.23.0 is released
// the replayed request should throw an error
expect(fetchProxy.calls).toBe(5);
} else {
expect(fetchProxy.calls).toBeGreaterThanOrEqual(5);
}

await expect(actor.read()).rejects.toThrowError(
'Timestamp failed to pass the watermark after retrying the configured 3 times. We cannot guarantee the integrity of the response since it could be a replay attack.',
);

// The agent should should have made 4 additional requests (3 retries + 1 original request)
expect(fetchProxy.calls).toBe(11);
// TODO - pin to 9 once dfx v0.23.0 is released
if (usingV2) {
// the replayed request should throw an error
// The agent should should have made 4 additional requests (3 retries + 1 original request)
expect(fetchProxy.calls).toBe(9);
} else {
expect(fetchProxy.calls).toBeGreaterThanOrEqual(9);
}
}, 10_000);

const findV2inCalls = (calls: [string][]) => {
for (let i = 0; i < calls.length; i++) {
if (calls[i][0].includes('v2')) {
return i;
}
}
return -1;
};
Loading

0 comments on commit 04c7bc3

Please sign in to comment.