Skip to content

Commit

Permalink
fix: effective target canister ID for mgmt call (#773)
Browse files Browse the repository at this point in the history
# Motivation

Using the IC mgmt function `install_chunked_code` currently fails when
executed against mainnet.

```
e: Server returned an error:
  Code: 400 (Bad Request)
  Body: error: canister_not_found
details: The specified canister does not exist.
```

The feature works locally but, fails on mainnet or the call is being
rejected by the BN because it does not comply with the specification.

> The <effective_canister_id> in the URL paths of requests is the
effective destination of the request. It must be contained in the
canister ranges of a subnet, otherwise the corresponding HTTP request is
rejected.
> If the request is an update call to the Management Canister
(aaaaa-aa), then:
>
> If the call is to the install_chunked_code method and the arg is a
Candid-encoded record with a target_canister field of type principal,
then the effective canister id must be that principal.

Source:
https://internetcomputer.org/docs/current/references/ic-interface-spec/#http-effective-canister-id

# Changes

- If the `tranform` function is called for method `install_chunked_code`
with `target_canister`, then `effectiveCanisterId` is the
`target_canister` else same as before.

---------

Signed-off-by: David Dal Busco <david.dalbusco@dfinity.org>
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
peterpeterparker and github-actions[bot] authored Nov 27, 2024
1 parent 230ebe2 commit 98d0d33
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 51 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# 2024.11.22-1600Z

## Fix

- The IC management function `install_chunked_code` failed on mainnet because the `target_canister` was not mapped as `effective_canister_id`, as defined by the IC specification.

# 2024.11.22-1600Z

## Overview

The current status of the libraries at the time of the release is as follows:
Expand Down
172 changes: 128 additions & 44 deletions packages/ic-management/src/utils/transform.utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,71 +4,155 @@ import { mockCanisterId } from "../ic-management.mock";
import { transform } from "./transform.utils";

describe("transform", () => {
it("should map the effectiveCanisterId when a valid canister_id is provided as principal in the request", () => {
const methodName = "someMethod";
const args = [{ canister_id: mockCanisterId }];
const callConfig: CallConfig = {};
describe("canister_id", () => {
it("should map the effectiveCanisterId when a valid canister_id is provided as principal in the request", () => {
const methodName = "someMethod";
const args = [{ canister_id: mockCanisterId }];
const callConfig: CallConfig = {};

const result = transform(methodName, args, callConfig);
const result = transform(methodName, args, callConfig);

expect(result).toEqual({
effectiveCanisterId: mockCanisterId,
expect(result).toEqual({
effectiveCanisterId: mockCanisterId,
});
});
});

it("should map the effectiveCanisterId when a valid canister_id is provided as string in the request", () => {
const methodName = "someMethod";
const args = [{ canister_id: mockCanisterId.toText() }];
const callConfig: CallConfig = {};
it("should map the effectiveCanisterId when a valid canister_id is provided as string in the request", () => {
const methodName = "someMethod";
const args = [{ canister_id: mockCanisterId.toText() }];
const callConfig: CallConfig = {};

const result = transform(methodName, args, callConfig);
const result = transform(methodName, args, callConfig);

expect(result).toEqual({
effectiveCanisterId: mockCanisterId,
expect(result).toEqual({
effectiveCanisterId: mockCanisterId,
});
});
});

it("should return effectiveCanisterId aaaaa-aa when args is empty", () => {
const methodName = "someMethod";
const args: unknown[] = [];
const callConfig: CallConfig = {};
it("should throw an error if canister_id is provided in the request but is not a valid principal or representation", () => {
const methodName = "someMethod";
const args = [{ canister_id: 12345 }];
const callConfig: CallConfig = {};

expect(() => transform(methodName, args, callConfig)).toThrow();
});
});

const result = transform(methodName, args, callConfig);
describe("target_canister", () => {
describe("with method install_chunked_code", () => {
it("should map the effectiveCanisterId when a valid target_canister is provided as principal with method install_chunked_code in the request", () => {
const methodName = "install_chunked_code";
const args = [{ target_canister: mockCanisterId }];
const callConfig: CallConfig = {};

const result = transform(methodName, args, callConfig);

expect(result).toEqual({
effectiveCanisterId: mockCanisterId,
});
});

it("should map the effectiveCanisterId when a valid target_canister is provided as string with method install_chunked_code in the request", () => {
const methodName = "install_chunked_code";
const args = [{ target_canister: mockCanisterId.toText() }];
const callConfig: CallConfig = {};

const result = transform(methodName, args, callConfig);

expect(result).toEqual({
effectiveCanisterId: mockCanisterId,
});
});

it("should throw an error if target_canister is provided in the request with method install_chunked_code but is not a valid principal or representation", () => {
const methodName = "install_chunked_code";
const args = [{ target_canister: 12345 }];
const callConfig: CallConfig = {};

expect(() => transform(methodName, args, callConfig)).toThrow();
});

it("should map the effectiveCanisterId to target_canister if a valid target_canister and canister_id are provided with the method install_chunked_code in the request", () => {
const methodName = "install_chunked_code";
const canisterId = Principal.fromText("7hfb6-caaaa-aaaar-qadga-cai");
const args = [
{ target_canister: mockCanisterId, canister_id: canisterId },
];
const callConfig: CallConfig = {};

const result = transform(methodName, args, callConfig);

expect(result).toEqual({
effectiveCanisterId: mockCanisterId,
});
});
});

expect(result).toEqual({
effectiveCanisterId: Principal.fromHex(""),
describe("without method install_chunked_code", () => {
it("should not map the effectiveCanisterId when a valid target_canister is provided but not the method install_chunked_code in the request", () => {
const methodName = "someMethod";
const args = [{ target_canister: mockCanisterId }];
const callConfig: CallConfig = {};

const result = transform(methodName, args, callConfig);

expect(result).toEqual({
effectiveCanisterId: Principal.fromHex(""),
});
});

it("should map the effectiveCanisterId to canister_id if a valid target_canister and canister_id are provided but not the method install_chunked_code in the request", () => {
const methodName = "someMethod";
const canisterId = Principal.fromText("7hfb6-caaaa-aaaar-qadga-cai");
const args = [
{ target_canister: mockCanisterId, canister_id: canisterId },
];
const callConfig: CallConfig = {};

const result = transform(methodName, args, callConfig);

expect(result).toEqual({
effectiveCanisterId: canisterId,
});
});
});
});

it("should return effectiveCanisterId aaaaa-aa when canister_id is missing in the first argument", () => {
const methodName = "someMethod";
const args = [{}];
const callConfig: CallConfig = {};
describe("no ids", () => {
it("should return effectiveCanisterId aaaaa-aa when args is empty", () => {
const methodName = "someMethod";
const args: unknown[] = [];
const callConfig: CallConfig = {};

const result = transform(methodName, args, callConfig);
const result = transform(methodName, args, callConfig);

expect(result).toEqual({
effectiveCanisterId: Principal.fromHex(""),
expect(result).toEqual({
effectiveCanisterId: Principal.fromHex(""),
});
});
});

it("should return effectiveCanisterId aaaaa-aa when the first argument is not an object", () => {
const methodName = "someMethod";
const args = [42];
const callConfig: CallConfig = {};
it("should return effectiveCanisterId aaaaa-aa when canister_id is missing in the first argument", () => {
const methodName = "someMethod";
const args = [{}];
const callConfig: CallConfig = {};

const result = transform(methodName, args, callConfig);
const result = transform(methodName, args, callConfig);

expect(result).toEqual({
effectiveCanisterId: Principal.fromHex(""),
expect(result).toEqual({
effectiveCanisterId: Principal.fromHex(""),
});
});
});

it("should throw an error if canister_id is provided in the request but is not a valid principal or representation", () => {
const methodName = "someMethod";
const args = [{ canister_id: 12345 }];
const callConfig: CallConfig = {};
it("should return effectiveCanisterId aaaaa-aa when the first argument is not an object", () => {
const methodName = "someMethod";
const args = [42];
const callConfig: CallConfig = {};

expect(() => transform(methodName, args, callConfig)).toThrow();
const result = transform(methodName, args, callConfig);

expect(result).toEqual({
effectiveCanisterId: Principal.fromHex(""),
});
});
});
});
18 changes: 11 additions & 7 deletions packages/ic-management/src/utils/transform.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,24 @@ type QueryTransform = Required<ActorConfig>["queryTransform"];
* @link https://internetcomputer.org/docs/current/references/ic-interface-spec/#http-effective-canister-id
**/
export const transform: CallTransform | QueryTransform = (
_methodName: string,
methodName: string,
args: (Record<string, unknown> & {
canister_id?: unknown;
target_canister?: unknown;
})[],
_callConfig: CallConfig,
): { effectiveCanisterId: Principal } => {
const first = args[0];

if (
nonNullish(first) &&
typeof first === "object" &&
nonNullish(first.canister_id)
) {
return { effectiveCanisterId: Principal.from(first.canister_id) };
if (nonNullish(first) && typeof first === "object") {
if (
methodName === "install_chunked_code" &&
nonNullish(first.target_canister)
) {
return { effectiveCanisterId: Principal.from(first.target_canister) };
} else if (nonNullish(first.canister_id)) {
return { effectiveCanisterId: Principal.from(first.canister_id) };
}
}

return { effectiveCanisterId: Principal.fromHex("") };
Expand Down

0 comments on commit 98d0d33

Please sign in to comment.