Skip to content

Commit 761d1ad

Browse files
authored
feat: allows httpagent.call to be provided with a nonce (#983)
* feat: refactor nonce logic to prioritize options and ensure compatibility with ArrayBuffer and Uint8Array * docs: jsdoc comments for call
1 parent b623f0d commit 761d1ad

File tree

3 files changed

+65
-5
lines changed

3 files changed

+65
-5
lines changed

docs/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
## [Unreleased]
55

6+
### Changed
7+
68
- fix: Bring Candid decoding of `opt` types up to Candid spec:
79
In particular, when decoding at an `opt` type:
810
- If the wire type is an `opt` type, decode its payload at the expected content type
@@ -19,6 +21,7 @@
1921

2022
### Added
2123

24+
- feat: refactor nonce logic to prioritize options and ensure compatibility with ArrayBuffer and Uint8Array
2225
- test: added e2e test for CanisterStatus requesting a subnet path, as a reference for getting the subnet id of a given canister id
2326

2427
## [2.3.0] - 2025-02-07

packages/agent/src/agent/http/http.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,32 @@ test('readState should not call transformers if request is passed', async () =>
240240
expect(transformMock).toBeCalledTimes(1);
241241
});
242242

243+
test('use provided nonce for call', async () => {
244+
const mockFetch: jest.Mock = jest.fn(() => {
245+
return Promise.resolve(
246+
new Response(null, {
247+
status: 200,
248+
}),
249+
);
250+
});
251+
252+
const canisterId: Principal = Principal.fromText('2chl6-4hpzw-vqaaa-aaaaa-c');
253+
const nonce = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]) as Nonce;
254+
255+
const httpAgent = new HttpAgent({ fetch: mockFetch, host: 'http://localhost' });
256+
257+
const methodName = 'greet';
258+
const arg = new ArrayBuffer(32);
259+
260+
const callResponse = await httpAgent.call(canisterId, {
261+
methodName,
262+
arg,
263+
nonce,
264+
});
265+
266+
expect(callResponse?.requestDetails?.nonce).toEqual(nonce);
267+
});
268+
243269
test('redirect avoid', async () => {
244270
function checkUrl(base: string, result: string) {
245271
const httpAgent = new HttpAgent({ host: base });

packages/agent/src/agent/http/index.ts

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -447,20 +447,33 @@ export class HttpAgent implements Agent {
447447
return (await this.#identity).getPrincipal();
448448
}
449449

450+
/**
451+
* Makes a call to a canister method.
452+
* @param canisterId - The ID of the canister to call. Can be a Principal or a string.
453+
* @param options - Options for the call.
454+
* @param options.methodName - The name of the method to call.
455+
* @param options.arg - The argument to pass to the method, as an ArrayBuffer.
456+
* @param options.effectiveCanisterId - (Optional) The effective canister ID, if different from the target canister ID.
457+
* @param options.callSync - (Optional) Whether to use synchronous call mode. Defaults to true.
458+
* @param options.nonce - (Optional) A unique nonce for the request. If provided, it will override any nonce set by transforms.
459+
* @param identity - (Optional) The identity to use for the call. If not provided, the agent's current identity will be used.
460+
* @returns A promise that resolves to the response of the call, including the request ID and response details.
461+
*/
450462
public async call(
451463
canisterId: Principal | string,
452464
options: {
453465
methodName: string;
454466
arg: ArrayBuffer;
455467
effectiveCanisterId?: Principal | string;
456468
callSync?: boolean;
469+
nonce?: Uint8Array | Nonce;
457470
},
458471
identity?: Identity | Promise<Identity>,
459472
): Promise<SubmitResponse> {
460473
await this.#rootKeyGuard();
461474
// TODO - restore this value
462475
const callSync = options.callSync ?? true;
463-
const id = await (identity !== undefined ? await identity : await this.#identity);
476+
const id = await(identity !== undefined ? await identity : await this.#identity);
464477
if (!id) {
465478
throw new IdentityInvalidError(
466479
"This identity has expired due this application's security policy. Please refresh your authentication.",
@@ -504,13 +517,31 @@ export class HttpAgent implements Agent {
504517
body: submit,
505518
})) as HttpAgentSubmitRequest;
506519

507-
const nonce: Nonce | undefined = transformedRequest.body.nonce
508-
? toNonce(transformedRequest.body.nonce)
509-
: undefined;
520+
// Determine the nonce to use for the request
521+
let nonce: Nonce | undefined;
522+
523+
// Check if a nonce is provided in the options and convert it to the correct type
524+
if (options?.nonce) {
525+
nonce = toNonce(options.nonce);
526+
}
527+
// If no nonce is provided in the options, check the transformedRequest body
528+
else if (transformedRequest.body.nonce) {
529+
nonce = toNonce(transformedRequest.body.nonce);
530+
}
531+
// If no nonce is found, set it to undefined
532+
else {
533+
nonce = undefined;
534+
}
510535

536+
// Assign the determined nonce to the submit object
511537
submit.nonce = nonce;
512538

513-
function toNonce(buf: ArrayBuffer): Nonce {
539+
/**
540+
* Converts an ArrayBuffer or Uint8Array to a Nonce type.
541+
* @param buf - The buffer to convert.
542+
* @returns The buffer as a Nonce.
543+
*/
544+
function toNonce(buf: ArrayBuffer | Uint8Array): Nonce {
514545
return new Uint8Array(buf) as Nonce;
515546
}
516547

0 commit comments

Comments
 (0)