From 32babe57e5a55b18190b5254f63c58db828ec58f Mon Sep 17 00:00:00 2001 From: Quint Daenen Date: Mon, 6 May 2024 10:42:32 +0200 Subject: [PATCH] Add instance endpoints. --- pocketic/README.md | 24 +++--- pocketic/blobstore.go | 5 +- pocketic/endpoints_test.go | 155 ++++++++++++++++++++++++++++++++- pocketic/http.go | 2 +- pocketic/instances.go | 171 +++++++++++++++++++++++++++++++++++++ pocketic/management.go | 49 ++++++++++- pocketic/pocketic.go | 44 +++------- pocketic/pocketic_test.go | 13 +-- pocketic/raw.go | 34 ++++++-- pocketic/request.go | 57 +++++++++++-- 10 files changed, 487 insertions(+), 67 deletions(-) create mode 100644 pocketic/instances.go diff --git a/pocketic/README.md b/pocketic/README.md index c6431aa..f30566f 100644 --- a/pocketic/README.md +++ b/pocketic/README.md @@ -15,24 +15,24 @@ The client is not yet stable and is subject to change. | ✅ | GET | /status | | ✅ | POST | /blobstore | | ✅ | GET | /blobstore/{id} | -| ❌ | POST | /verify_signature | +| ✅ | POST | /verify_signature | | ❌ | GET | /read_graph/{state_label}/{op_id} | -| ❌ | GET | /instances/ | -| ❌ | POST | /instances/ | -| ❌ | DELETE | /instances/{id} | +| ✅ | GET | /instances/ | +| ✅ | POST | /instances/ | +| ✅ | DELETE | /instances/{id} | | ✅ | POST | /instances/{id}/read/query | -| ❌ | GET | /instances/{id}/read/get_time | -| ❌ | POST | /instances/{id}/read/get_cycles | -| ❌ | POST | /instances/{id}/read/get_stable_memory | -| ❌ | POST | /instances/{id}/read/get_subnet | -| ❌ | POST | /instances/{id}/read/pub_key | +| ✅ | GET | /instances/{id}/read/get_time | +| ✅ | POST | /instances/{id}/read/get_cycles | +| ✅ | POST | /instances/{id}/read/get_stable_memory | +| ✅ | POST | /instances/{id}/read/get_subnet | +| ✅ | POST | /instances/{id}/read/pub_key | | ✅ | POST | /instances/{id}/update/submit_ingress_message | | ✅ | POST | /instances/{id}/update/await_ingress_message | -| ❌ | POST | /instances/{id}/update/execute_ingress_message | +| ✅ | POST | /instances/{id}/update/execute_ingress_message | | ✅ | POST | /instances/{id}/update/set_time | | ✅ | POST | /instances/{id}/update/add_cycles | -| ❌ | POST | /instances/{id}/update/set_stable_memory | -| ❌ | POST | /instances/{id}/update/tick | +| ✅ | POST | /instances/{id}/update/set_stable_memory | +| ✅ | POST | /instances/{id}/update/tick | | ❌ | GET | /instances/{id}/api/v2/status | | ❌ | POST | /instances/{id}/api/v2/canister/{ecid}/call | | ❌ | POST | /instances/{id}/api/v2/canister/{ecid}/query | diff --git a/pocketic/blobstore.go b/pocketic/blobstore.go index 17e25f4..de64580 100644 --- a/pocketic/blobstore.go +++ b/pocketic/blobstore.go @@ -23,7 +23,7 @@ func (pic PocketIC) GetBlob(blobID []byte) ([]byte, error) { } // UploadBlob uploads and stores a binary blob to the PocketIC server. -func (pic PocketIC) UploadBlob(bytes []byte) ([]byte, error) { +func (pic PocketIC) UploadBlob(bytes []byte, gzipCompression bool) ([]byte, error) { method := http.MethodPost url := fmt.Sprintf("%s/blobstore", pic.server.URL()) pic.logger.Printf("[POCKETIC] %s %s %+v", method, url, bytes) @@ -32,6 +32,9 @@ func (pic PocketIC) UploadBlob(bytes []byte) ([]byte, error) { return nil, err } req.Header.Set("content-type", "application/octet-stream") + if gzipCompression { + req.Header.Set("content-encoding", "gzip") + } resp, err := pic.client.Do(req) if err != nil { return nil, err diff --git a/pocketic/endpoints_test.go b/pocketic/endpoints_test.go index 4175361..779f45a 100644 --- a/pocketic/endpoints_test.go +++ b/pocketic/endpoints_test.go @@ -1,7 +1,9 @@ package pocketic_test import ( + "github.com/aviate-labs/agent-go/candid/idl" "github.com/aviate-labs/agent-go/pocketic" + "github.com/aviate-labs/agent-go/principal" "testing" ) @@ -21,7 +23,7 @@ func Endpoints(t *testing.T) *pocketic.PocketIC { } }) t.Run("blobstore", func(t *testing.T) { - id, err := pic.UploadBlob([]byte{0, 1, 2, 3}) + id, err := pic.UploadBlob([]byte{0, 1, 2, 3}, false) if err != nil { t.Fatal(err) } @@ -33,6 +35,157 @@ func Endpoints(t *testing.T) *pocketic.PocketIC { t.Fatalf("unexpected blob size: %d", len(bytes)) } }) + t.Run("instances", func(t *testing.T) { + var instances []string + t.Run("get", func(t *testing.T) { + instances, err = pic.GetInstances() + if err != nil { + t.Fatal(err) + } + if len(instances) == 0 { + t.Fatal("no instances found") + } + }) + var instanceConfig *pocketic.InstanceConfig + t.Run("post", func(t *testing.T) { + instanceConfig, err = pic.CreateInstance(pocketic.DefaultSubnetConfig) + if err != nil { + t.Fatal(err) + } + if instanceConfig == nil { + t.Fatal("instance config is nil") + } + newInstances, err := pic.GetInstances() + if err != nil { + t.Fatal(err) + } + if len(newInstances) != len(instances)+1 { + t.Fatalf("unexpected instances count: %d", len(newInstances)) + } + }) + t.Run("delete", func(t *testing.T) { + if err := pic.DeleteInstance(instanceConfig.InstanceID); err != nil { + t.Fatal(err) + } + newInstances, err := pic.GetInstances() + if err != nil { + t.Fatal(err) + } + if newInstances[len(newInstances)-1] != "Deleted" { + t.Fatal("instance was not deleted") + } + }) + + canisterID, err := pic.CreateCanister() + if err != nil { + t.Fatal(err) + } + wasmModule := compileMotoko(t, "testdata/main.mo", "testdata/main.wasm") + if err := pic.InstallCode(*canisterID, wasmModule, nil, nil); err != nil { + t.Fatal(err) + } + + t.Run("query", func(t *testing.T) { + if err := pic.QueryCall(*canisterID, principal.AnonymousID, "void", nil, nil); err == nil { + t.Fatal() + } + var resp string + if err := pic.QueryCall(*canisterID, principal.AnonymousID, "helloQuery", []any{"world"}, []any{&resp}); err != nil { + t.Fatal(err) + } + if resp != "Hello, world!" { + t.Fatalf("unexpected response: %s", resp) + } + }) + + t.Run("get_time", func(t *testing.T) { + dt, err := pic.GetTime() + if err != nil { + t.Fatal(err) + } + if dt == nil { + t.Fatal("time is nil") + } + }) + + t.Run("get_cycles", func(t *testing.T) { + cycles, err := pic.GetCycles(*canisterID) + if err != nil { + t.Fatal(err) + } + if cycles <= 0 { + t.Fatalf("unexpected cycles: %d", cycles) + } + }) + + t.Run("stable_memory", func(t *testing.T) { + if err := pic.SetStableMemory(*canisterID, []byte{0, 1, 2, 3}, false); err != nil { + t.Fatal(err) + } + if _, err := pic.GetStableMemory(*canisterID); err != nil { + t.Fatal(err) + } + }) + + t.Run("get_subnet", func(t *testing.T) { + subnetID, err := pic.GetSubnet(*canisterID) + if err != nil { + t.Fatal(err) + } + if subnetID == nil { + t.Fatal("subnet ID is nil") + } + }) + + t.Run("pub_key", func(t *testing.T) { + if _, err := pic.RootKey(); err != nil { + t.Fatal(err) + } + }) + + t.Run("ingress_message", func(t *testing.T) { + payload, err := idl.Marshal([]any{"world"}) + if err != nil { + t.Fatal(err) + } + { + msgID, err := pic.SubmitCall(*canisterID, principal.AnonymousID, "helloUpdate", payload) + if err != nil { + t.Fatal(err) + } + raw, err := pic.AwaitCall(*msgID) + if err != nil { + t.Fatal(err) + } + var resp string + if err := idl.Unmarshal(raw, []any{&resp}); err != nil { + t.Fatal(err) + } + if resp != "Hello, world!" { + t.Fatalf("unexpected response: %s", resp) + } + } + { + raw, err := pic.ExecuteCall(*canisterID, new(pocketic.RawEffectivePrincipalNone), principal.AnonymousID, "helloUpdate", payload) + if err != nil { + t.Fatal(err) + } + var resp string + if err := idl.Unmarshal(raw, []any{&resp}); err != nil { + t.Fatal(err) + } + if resp != "Hello, world!" { + t.Fatalf("unexpected response: %s", resp) + } + } + }) + + t.Run("tick", func(t *testing.T) { + if err := pic.Tick(); err != nil { + t.Fatal(err) + } + }) + }) return pic } diff --git a/pocketic/http.go b/pocketic/http.go index cfce3d5..6a187aa 100644 --- a/pocketic/http.go +++ b/pocketic/http.go @@ -133,7 +133,7 @@ func (pic PocketIC) SetTime(time time.Time) error { fmt.Sprintf("%s/update/set_time", pic.instanceURL()), http.StatusOK, RawTime{ - NanosSinceEpoch: int(time.UnixNano()), + NanosSinceEpoch: time.UnixNano(), }, nil, ) diff --git a/pocketic/instances.go b/pocketic/instances.go new file mode 100644 index 0000000..53121e0 --- /dev/null +++ b/pocketic/instances.go @@ -0,0 +1,171 @@ +package pocketic + +import ( + "fmt" + "github.com/aviate-labs/agent-go/principal" + "net/http" + "time" +) + +// CreateInstance creates a new PocketIC instance. +func (pic PocketIC) CreateInstance(config SubnetConfigSet) (*InstanceConfig, error) { + var a CreateResponse[InstanceConfig] + if err := pic.do( + http.MethodPost, + fmt.Sprintf("%s/instances", pic.server.URL()), + http.StatusCreated, + config, + &a, + ); err != nil { + return nil, err + } + if a.Error != nil { + return nil, a.Error + } + return a.Created, nil +} + +// DeleteInstance deletes a PocketIC instance. +func (pic PocketIC) DeleteInstance(instanceID int) error { + return pic.do( + http.MethodDelete, + fmt.Sprintf("%s/instances/%d", pic.server.URL(), instanceID), + http.StatusOK, + nil, + nil, + ) +} + +// GetCycles returns the cycles of a canister. +func (pic PocketIC) GetCycles(canisterID principal.Principal) (int, error) { + var cycles RawCycles + if err := pic.do( + http.MethodPost, + fmt.Sprintf("%s/read/get_cycles", pic.instanceURL()), + http.StatusOK, + &RawCanisterID{CanisterID: canisterID.Raw}, + &cycles, + ); err != nil { + return 0, err + } + return cycles.Cycles, nil +} + +// GetInstances lists all PocketIC instance availabilities. +func (pic PocketIC) GetInstances() ([]string, error) { + var instances []string + if err := pic.do( + http.MethodGet, + fmt.Sprintf("%s/instances", pic.server.URL()), + http.StatusOK, + nil, + &instances, + ); err != nil { + return nil, err + } + fmt.Println(instances) + return instances, nil +} + +// GetStableMemory returns the stable memory of a canister. +func (pic PocketIC) GetStableMemory(canisterID principal.Principal) ([]byte, error) { + var data RawStableMemory + if err := pic.do( + http.MethodPost, + fmt.Sprintf("%s/read/get_stable_memory", pic.instanceURL()), + http.StatusOK, + &RawCanisterID{CanisterID: canisterID.Raw}, + &data, + ); err != nil { + return nil, err + } + return data.Blob, nil +} + +// GetSubnet returns the subnet of a canister. +func (pic PocketIC) GetSubnet(canisterID principal.Principal) (*principal.Principal, error) { + var subnetID RawSubnetID + if err := pic.do( + http.MethodPost, + fmt.Sprintf("%s/read/get_subnet", pic.instanceURL()), + http.StatusOK, + &RawCanisterID{CanisterID: canisterID.Raw}, + &subnetID, + ); err != nil { + return nil, err + } + return &principal.Principal{Raw: subnetID.SubnetID}, nil +} + +// GetTime returns the current time of the PocketIC instance. +func (pic PocketIC) GetTime() (*time.Time, error) { + var t RawTime + if err := pic.do( + http.MethodGet, + fmt.Sprintf("%s/read/get_time", pic.instanceURL()), + http.StatusOK, + nil, + &t, + ); err != nil { + return nil, err + } + now := time.Unix(0, t.NanosSinceEpoch) + return &now, nil +} + +// RootKey returns the root key of the NNS subnet. +func (pic PocketIC) RootKey() ([]byte, error) { + var subnetID *principal.Principal + for k, v := range pic.topology { + if v.SubnetKind == NNSSubnet { + id, err := principal.Decode(k) + if err != nil { + return nil, err + } + subnetID = &id + break + } + } + if subnetID == nil { + return nil, fmt.Errorf("no NNS subnet found") + } + var key []byte + if err := pic.do( + http.MethodPost, + fmt.Sprintf("%s/read/pub_key", pic.instanceURL()), + http.StatusOK, + &RawSubnetID{SubnetID: subnetID.Raw}, + &key, + ); err != nil { + return nil, err + } + return key, nil +} + +// SetStableMemory sets the stable memory of a canister. +func (pic PocketIC) SetStableMemory(canisterID principal.Principal, data []byte, gzipCompression bool) error { + blobID, err := pic.UploadBlob(data, gzipCompression) + if err != nil { + return err + } + return pic.do( + http.MethodPost, + fmt.Sprintf("%s/update/set_stable_memory", pic.instanceURL()), + http.StatusOK, + RawSetStableMemory{ + CanisterID: canisterID.Raw, + BlobID: blobID, + }, + nil, + ) +} + +func (pic PocketIC) Tick() error { + return pic.do( + http.MethodPost, + fmt.Sprintf("%s/update/tick", pic.instanceURL()), + http.StatusOK, + nil, + nil, + ) +} diff --git a/pocketic/management.go b/pocketic/management.go index aca2be4..cd6ddd0 100644 --- a/pocketic/management.go +++ b/pocketic/management.go @@ -32,13 +32,58 @@ func (pic PocketIC) AddCycles(canisterID principal.Principal, amount int) (int, // CreateCanister creates a canister with default settings as the anonymous principal. func (pic PocketIC) CreateCanister() (*principal.Principal, error) { - payload, err := idl.Marshal([]any{ProvisionalCreateCanisterArgument{}}) + return pic.CreateCanisterWithArgs(ProvisionalCreateCanisterArgument{}) +} + +// CreateCanisterOnSubnet creates a canister on the specified subnet with the specified settings. +func (pic PocketIC) CreateCanisterOnSubnet(subnetID principal.Principal, args ProvisionalCreateCanisterArgument) (*principal.Principal, error) { + return pic.createCanister(&RawEffectivePrincipalSubnetID{SubnetID: subnetID.Raw}, args) +} + +// CreateCanisterWithArgs creates a canister with the specified settings and cycles. +func (pic PocketIC) CreateCanisterWithArgs(args ProvisionalCreateCanisterArgument) (*principal.Principal, error) { + return pic.createCanister(new(RawEffectivePrincipalNone), args) +} + +// CreateCanisterWithID creates a canister with the specified canister ID and settings. +func (pic PocketIC) CreateCanisterWithID(canisterID principal.Principal, args ProvisionalCreateCanisterArgument) (*principal.Principal, error) { + return pic.createCanister(&RawEffectivePrincipalCanisterID{CanisterID: canisterID.Raw}, args) +} + +func (pic PocketIC) InstallCode(canisterID principal.Principal, wasmModule []byte, arg []byte, optSender *principal.Principal) error { + sender := principal.AnonymousID + if optSender != nil { + sender = *optSender + } + payload, err := idl.Marshal([]any{ic0.InstallCodeArgs{ + Mode: ic0.CanisterInstallMode{ + Install: new(idl.Null), + }, + WasmModule: wasmModule, + CanisterId: canisterID, + Arg: arg, + }}) + if err != nil { + return err + } + _, err = pic.updateCallWithEP( + ic.MANAGEMENT_CANISTER_PRINCIPAL, + &RawEffectivePrincipalCanisterID{CanisterID: canisterID.Raw}, + sender, + "install_code", + payload, + ) + return err +} + +func (pic PocketIC) createCanister(effectivePrincipal RawEffectivePrincipal, args ProvisionalCreateCanisterArgument) (*principal.Principal, error) { + payload, err := idl.Marshal([]any{args}) if err != nil { return nil, err } raw, err := pic.updateCallWithEP( ic.MANAGEMENT_CANISTER_PRINCIPAL, - new(RawEffectivePrincipalNone), + effectivePrincipal, principal.AnonymousID, "provisional_create_canister_with_cycles", payload, diff --git a/pocketic/pocketic.go b/pocketic/pocketic.go index c87a3ae..0ef1ad6 100644 --- a/pocketic/pocketic.go +++ b/pocketic/pocketic.go @@ -215,13 +215,7 @@ func New(opts ...Option) (*PocketIC, error) { if err != nil { return nil, fmt.Errorf("failed to create instance: %v", err) } - var respBody struct { - Created *struct { - InstanceID int `json:"instance_id"` - Topology map[string]Topology `json:"topology"` - } `json:"Created,omitempty"` - Error *ErrorMessage `json:"Error,omitempty"` - } + var respBody CreateResponse[InstanceConfig] if respBody.Error != nil { return nil, respBody.Error } @@ -244,6 +238,7 @@ func (pic *PocketIC) Close() error { return pic.server.Close() } +// Status pings the PocketIC instance. func (pic PocketIC) Status() error { return pic.do( http.MethodGet, @@ -259,36 +254,21 @@ func (pic PocketIC) Topology() map[string]Topology { return pic.topology } +func (pic PocketIC) VerifySignature(sig RawVerifyCanisterSigArg) error { + return pic.do( + http.MethodPost, + fmt.Sprintf("%s/verify_signature", pic.server.URL()), + http.StatusOK, + sig, + nil, + ) +} + // instanceURL returns the URL of the PocketIC instance. func (pic PocketIC) instanceURL() string { return fmt.Sprintf("%s/instances/%d", pic.server.URL(), pic.InstanceID) } -type RawSubnetID struct { - SubnetID string `json:"subnet_id"` -} - -func (r RawSubnetID) MarshalJSON() ([]byte, error) { - return json.Marshal(map[string]interface{}{ - "subnet-id": base64.StdEncoding.EncodeToString([]byte(r.SubnetID)), - }) -} - -func (r *RawSubnetID) UnmarshalJSON(bytes []byte) error { - var rawSubnetID struct { - SubnetID string `json:"subnet_id-id"` - } - if err := json.Unmarshal(bytes, &rawSubnetID); err != nil { - return err - } - subnetID, err := base64.StdEncoding.DecodeString(rawSubnetID.SubnetID) - if err != nil { - return err - } - r.SubnetID = string(subnetID) - return nil -} - type SubnetConfigSet struct { Application []SubnetSpec `json:"application"` Bitcoin *SubnetSpec `json:"bitcoin,omitempty"` diff --git a/pocketic/pocketic_test.go b/pocketic/pocketic_test.go index 7888cb8..a92e4f5 100644 --- a/pocketic/pocketic_test.go +++ b/pocketic/pocketic_test.go @@ -79,11 +79,7 @@ func HttpGateway(t *testing.T) *pocketic.PocketIC { t.Fatal(err) } - compileMotoko(t, "testdata/main.mo", "testdata/main.wasm") - wasmModule, err := os.ReadFile("testdata/main.wasm") - if err != nil { - t.Fatal(err) - } + wasmModule := compileMotoko(t, "testdata/main.mo", "testdata/main.wasm") if err := mgmtAgent.InstallCode(ic0.InstallCodeArgs{ Mode: ic0.CanisterInstallMode{ Install: new(idl.Null), @@ -131,7 +127,7 @@ func TestPocketIC(t *testing.T) { } } -func compileMotoko(t *testing.T, in, out string) { +func compileMotoko(t *testing.T, in, out string) []byte { dfxPath, err := exec.LookPath("dfx") if err != nil { t.Skipf("dfx not found: %v", err) @@ -146,6 +142,11 @@ func compileMotoko(t *testing.T, in, out string) { if err := cmd.Run(); err != nil { t.Fatal(err) } + wasmModule, err := os.ReadFile(out) + if err != nil { + t.Fatal(err) + } + return wasmModule } type testLogger struct{} diff --git a/pocketic/raw.go b/pocketic/raw.go index c91bb4c..74d32a9 100644 --- a/pocketic/raw.go +++ b/pocketic/raw.go @@ -15,8 +15,14 @@ func (b Base64EncodedBlob) MarshalJSON() ([]byte, error) { return json.Marshal(encoded) } +// String returns the base64 encoded string of the blob. +// NOTE: it will truncate the string if it is too long. func (b Base64EncodedBlob) String() string { - return base64.StdEncoding.EncodeToString(b) + str := base64.StdEncoding.EncodeToString(b) + if len(str) > 20 { + return str[:10] + "..." + str[len(str)-10:] + } + return str } func (b *Base64EncodedBlob) UnmarshalJSON(bytes []byte) error { @@ -32,6 +38,16 @@ func (b *Base64EncodedBlob) UnmarshalJSON(bytes []byte) error { return nil } +type CreateResponse[T any] struct { + Created *T `json:"Created"` + Error *ErrorMessage `json:"Error"` +} + +type InstanceConfig struct { + InstanceID int `json:"instance_id"` + Topology map[string]Topology `json:"topology"` +} + type RawAddCycles struct { Amount int `json:"amount"` CanisterID Base64EncodedBlob `json:"canister_id"` @@ -74,6 +90,10 @@ type RawCanisterID struct { type RawCanisterResult Result[RawWasmResult] +type RawCycles struct { + Cycles int `json:"cycles"` +} + type RawEffectivePrincipal interface { rawEffectivePrincipal() } @@ -148,14 +168,18 @@ type RawSetStableMemory struct { CanisterID Base64EncodedBlob `json:"canister_id"` } +type RawStableMemory struct { + Blob Base64EncodedBlob `json:"blob"` +} + type RawSubmitIngressResult Result[RawMessageID] -type RawSubnetId struct { +type RawSubnetID struct { SubnetID Base64EncodedBlob `json:"subnet_id"` } type RawTime struct { - NanosSinceEpoch int `json:"nanos_since_epoch"` + NanosSinceEpoch int64 `json:"nanos_since_epoch"` } type RawVerifyCanisterSigArg struct { @@ -179,12 +203,12 @@ type Result[R any] struct { } type UserError struct { - Code int `json:"code"` + Code string `json:"code"` Description string `json:"description"` } func (e UserError) Error() string { - return fmt.Sprintf("(%d) %s", e.Code, e.Description) + return fmt.Sprintf("(%s) %s", e.Code, e.Description) } // WASMResult describes the different types that executing a WASM function in a canister can produce. diff --git a/pocketic/request.go b/pocketic/request.go index e14f101..608608a 100644 --- a/pocketic/request.go +++ b/pocketic/request.go @@ -4,14 +4,17 @@ import ( "bytes" "encoding/json" "fmt" + "github.com/aviate-labs/agent-go/candid/idl" "github.com/aviate-labs/agent-go/principal" "io" "net/http" ) -var headers = http.Header{ - "content-type": []string{"application/json"}, - "processing-timeout-ms": []string{"300000"}, +var headers = func() http.Header { + return http.Header{ + "content-type": []string{"application/json"}, + "processing-timeout-ms": []string{"300000"}, + } } func checkResponse(resp *http.Response, statusCode int, v any) error { @@ -38,7 +41,7 @@ func newRequest(method, url string, body any) (*http.Request, error) { if err != nil { return nil, err } - req.Header = headers + req.Header = headers() return req, nil } @@ -63,13 +66,53 @@ func (pic PocketIC) AwaitCall(messageID RawMessageID) ([]byte, error) { return *resp.Ok.Reply, nil } +// ExecuteCall executes an update call on a canister. +func (pic PocketIC) ExecuteCall( + canisterID principal.Principal, + effectivePrincipal RawEffectivePrincipal, + sender principal.Principal, + method string, + payload []byte, +) ([]byte, error) { + var resp RawCanisterResult + if err := pic.do( + http.MethodPost, + fmt.Sprintf("%s/update/execute_ingress_message", pic.instanceURL()), + http.StatusOK, + RawCanisterCall{ + CanisterID: canisterID.Raw, + EffectivePrincipal: effectivePrincipal, + Method: method, + Payload: payload, + Sender: sender.Raw, + }, + &resp, + ); err != nil { + return nil, err + } + if resp.Err != nil { + return nil, resp.Err + } + if resp.Ok.Reject != nil { + return nil, resp.Ok.Reject + } + return *resp.Ok.Reply, nil +} + // QueryCall executes a query call on a canister. -func (pic PocketIC) QueryCall(canisterID principal.Principal, sender principal.Principal, method string, payload []byte) ([]byte, error) { +func (pic PocketIC) QueryCall(canisterID principal.Principal, sender principal.Principal, method string, args []any, ret []any) error { + payload, err := idl.Marshal(args) + if err != nil { + return err + } raw, err := pic.canisterCall("read/query", canisterID, new(RawEffectivePrincipalNone), sender, method, payload) if err != nil { - return nil, err + return err + } + if err := idl.Unmarshal(*raw, ret); err != nil { + return err } - return *raw, nil + return nil } // SubmitCall submits an update call (without executing it immediately).