From 01381a6ea6e4f6913c3044063a99d4bd4bb98b2f Mon Sep 17 00:00:00 2001 From: Quint Daenen Date: Fri, 3 May 2024 09:09:10 +0200 Subject: [PATCH] Add IR for calls/queries. --- Makefile | 1 + agent.go | 161 ++++++++++++------ candid/internal/candid/grammar.go | 4 +- candid/internal/candid/grammar.pegn | 2 +- didc | 0 gen/generator.go | 14 +- gen/templates/agent_indirect.gotmpl | 60 +++++++ ic/ic/agent.go | 242 ++++++++++++++++++++++++++++ ic/sns/ledger/agent.go | 67 +++++++- ic/sns/root/agent.go | 1 + ic/sns/testdata/did/ledger.did | 53 ++++-- ic/sns/testdata/did/root.did | 1 + ic/testdata/did/wallet.did | 13 ++ ic/testdata/gen.go | 3 + ic/types_agent_test.go | 4 +- ic/wallet/actor.mo | 3 + ic/wallet/agent.go | 26 +++ ic/wallet/agent_test.go | 42 +++++ ic/wallet/types.mo | 1 + pocketic/agent_test.go | 2 +- pocketic/gen.go | 3 + pocketic/http.go | 131 +++++++++++++++ pocketic/management.go | 1 + pocketic/pocketic.go | 34 ++-- pocketic/pocketic_test.go | 94 +++++++++++ pocketic/request.go | 11 +- pocketic/server.go | 26 +-- 27 files changed, 894 insertions(+), 106 deletions(-) delete mode 100644 didc create mode 100644 gen/templates/agent_indirect.gotmpl create mode 100644 pocketic/gen.go create mode 100644 pocketic/http.go diff --git a/Makefile b/Makefile index ca19f4d..c7aee9b 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,7 @@ test-cover: go tool cover -html=coverage.out gen: + cd pocketic && go generate cd candid && go generate gen-ic: diff --git a/agent.go b/agent.go index d083201..de92173 100644 --- a/agent.go +++ b/agent.go @@ -137,17 +137,26 @@ func New(cfg Config) (*Agent, error) { // Call calls a method on a canister and unmarshals the result into the given values. func (a Agent) Call(canisterID principal.Principal, methodName string, args []any, values []any) error { - rawArgs, err := idl.Marshal(args) + call, err := a.CreateCall(canisterID, methodName, args...) if err != nil { return err } + return call.CallAndWait(values...) +} + +// CreateCall creates a new call to the given canister and method. +func (a *Agent) CreateCall(canisterID principal.Principal, methodName string, args ...any) (*Call, error) { + rawArgs, err := idl.Marshal(args) + if err != nil { + return nil, err + } if len(args) == 0 { // Default to the empty Candid argument list. rawArgs = []byte{'D', 'I', 'D', 'L', 0, 0} } nonce, err := newNonce() if err != nil { - return err + return nil, err } requestID, data, err := a.sign(Request{ Type: RequestTypeCall, @@ -159,19 +168,49 @@ func (a Agent) Call(canisterID principal.Principal, methodName string, args []an Nonce: nonce, }) if err != nil { - return err - } - ecID := effectiveCanisterID(canisterID, args) - a.logger.Printf("[AGENT] CALL %s %s (%x)", ecID, methodName, *requestID) - if _, err := a.call(ecID, data); err != nil { - return err + return nil, err } + return &Call{ + a: a, + methodName: methodName, + effectiveCanisterID: effectiveCanisterID(canisterID, args), + requestID: *requestID, + data: data, + }, nil +} - raw, err := a.poll(ecID, *requestID) +// CreateQuery creates a new query to the given canister and method. +func (a *Agent) CreateQuery(canisterID principal.Principal, methodName string, args ...any) (*Query, error) { + rawArgs, err := idl.Marshal(args) if err != nil { - return err + return nil, err } - return idl.Unmarshal(raw, values) + if len(args) == 0 { + // Default to the empty Candid argument list. + rawArgs = []byte{'D', 'I', 'D', 'L', 0, 0} + } + nonce, err := newNonce() + if err != nil { + return nil, err + } + _, data, err := a.sign(Request{ + Type: RequestTypeQuery, + Sender: a.Sender(), + CanisterID: canisterID, + MethodName: methodName, + Arguments: rawArgs, + IngressExpiry: a.expiryDate(), + Nonce: nonce, + }) + if err != nil { + return nil, err + } + return &Query{ + a: a, + methodName: methodName, + effectiveCanisterID: effectiveCanisterID(canisterID, args), + data: data, + }, nil } // GetCanisterControllers returns the list of principals that can control the given canister. @@ -245,51 +284,18 @@ func (a Agent) GetCanisterModuleHash(canisterID principal.Principal) ([]byte, er return h, err } +// GetRootKey returns the root key of the host. func (a Agent) GetRootKey() []byte { return a.rootKey } +// Query calls a method on a canister and unmarshals the result into the given values. func (a Agent) Query(canisterID principal.Principal, methodName string, args []any, values []any) error { - rawArgs, err := idl.Marshal(args) + query, err := a.CreateQuery(canisterID, methodName, args...) if err != nil { return err } - if len(args) == 0 { - // Default to the empty Candid argument list. - rawArgs = []byte{'D', 'I', 'D', 'L', 0, 0} - } - nonce, err := newNonce() - if err != nil { - return err - } - _, data, err := a.sign(Request{ - Type: RequestTypeQuery, - Sender: a.Sender(), - CanisterID: canisterID, - MethodName: methodName, - Arguments: rawArgs, - IngressExpiry: a.expiryDate(), - Nonce: nonce, - }) - if err != nil { - return err - } - ecID := effectiveCanisterID(canisterID, args) - a.logger.Printf("[AGENT] QUERY %s %s", ecID, methodName) - resp, err := a.query(ecID, data) - if err != nil { - return err - } - var raw []byte - switch resp.Status { - case "replied": - raw = resp.Reply["arg"] - case "rejected": - return fmt.Errorf("(%d) %s", resp.RejectCode, resp.RejectMsg) - default: - panic("unreachable") - } - return idl.Unmarshal(raw, values) + return query.Query(values...) } // RequestStatus returns the status of the request with the given ID. @@ -332,8 +338,8 @@ func (a Agent) Sender() principal.Principal { return a.identity.Sender() } -func (a Agent) call(ecid principal.Principal, data []byte) ([]byte, error) { - return a.client.call(ecid, data) +func (a Agent) call(ecID principal.Principal, data []byte) ([]byte, error) { + return a.client.call(ecID, data) } func (a Agent) expiryDate() uint64 { @@ -428,6 +434,34 @@ func (a Agent) sign(request Request) (*RequestID, []byte, error) { return &requestID, data, nil } +// Call is an intermediate representation of a call to a canister. +type Call struct { + a *Agent + methodName string + effectiveCanisterID principal.Principal + requestID RequestID + data []byte +} + +// CallAndWait calls a method on a canister and waits for the result. +func (c Call) CallAndWait(values ...any) error { + c.a.logger.Printf("[AGENT] CALL %s %s (%x)", c.effectiveCanisterID, c.methodName, c.requestID) + if _, err := c.a.call(c.effectiveCanisterID, c.data); err != nil { + return err + } + raw, err := c.a.poll(c.effectiveCanisterID, c.requestID) + if err != nil { + return err + } + return idl.Unmarshal(raw, values) +} + +// WithEffectiveCanisterID sets the effective canister ID for the call. +func (c *Call) WithEffectiveCanisterID(canisterID principal.Principal) *Call { + c.effectiveCanisterID = canisterID + return c +} + // Config is the configuration for an Agent. type Config struct { // Identity is the identity used by the Agent. @@ -445,3 +479,30 @@ type Config struct { // PollTimeout is the timeout for polling for a response. PollTimeout time.Duration } + +// Query is an intermediate representation of a query to a canister. +type Query struct { + a *Agent + methodName string + effectiveCanisterID principal.Principal + data []byte +} + +// Query calls a method on a canister and unmarshals the result into the given values. +func (q Query) Query(values ...any) error { + q.a.logger.Printf("[AGENT] QUERY %s %s", q.effectiveCanisterID, q.methodName) + resp, err := q.a.query(q.effectiveCanisterID, q.data) + if err != nil { + return err + } + var raw []byte + switch resp.Status { + case "replied": + raw = resp.Reply["arg"] + case "rejected": + return fmt.Errorf("(%d) %s", resp.RejectCode, resp.RejectMsg) + default: + panic("unreachable") + } + return idl.Unmarshal(raw, values) +} diff --git a/candid/internal/candid/grammar.go b/candid/internal/candid/grammar.go index 7f397fe..48f3b28 100755 --- a/candid/internal/candid/grammar.go +++ b/candid/internal/candid/grammar.go @@ -643,7 +643,9 @@ func Record(p *ast.Parser) (*ast.Node, error) { TypeStrings: NodeTypes, Value: op.And{ "record", - Sp, + op.Optional( + Sp, + ), '{', Ws, op.Optional( diff --git a/candid/internal/candid/grammar.pegn b/candid/internal/candid/grammar.pegn index 0bf0868..3762624 100644 --- a/candid/internal/candid/grammar.pegn +++ b/candid/internal/candid/grammar.pegn @@ -31,7 +31,7 @@ ConsType <- Blob Blob <-- 'blob' Opt <-- 'opt' Sp DataType Vec <-- 'vec' Sp DataType -Record <-- 'record' Sp '{' Ws Fields? Ws '}' +Record <-- 'record' Sp? '{' Ws Fields? Ws '}' Variant <-- 'variant' Sp '{' Ws Fields? Ws '}' Fields <- FieldType (';' Ws FieldType)* ';'? diff --git a/didc b/didc deleted file mode 100644 index e69de29..0000000 diff --git a/gen/generator.go b/gen/generator.go index 8e014dc..1f41680 100644 --- a/gen/generator.go +++ b/gen/generator.go @@ -76,6 +76,8 @@ type Generator struct { PackageName string ServiceDescription did.Description usedIDL bool + + indirect bool } // NewGenerator creates a new generator for the given service description. @@ -145,7 +147,11 @@ func (g *Generator) Generate() ([]byte, error) { }) } } - t, ok := templates["agent"] + tmplName := "agent" + if g.indirect { + tmplName = "agent_indirect" + } + t, ok := templates[tmplName] if !ok { return nil, fmt.Errorf("template not found") } @@ -320,6 +326,12 @@ func (g *Generator) GenerateMock() ([]byte, error) { return io.ReadAll(&tmpl) } +// Indirect sets the generator to generate indirect calls. +func (g *Generator) Indirect() *Generator { + g.indirect = true + return g +} + func (g *Generator) dataToGoReturnValue(definitions map[string]did.Data, prefix string, data did.Data) string { switch t := data.(type) { case did.Primitive: diff --git a/gen/templates/agent_indirect.gotmpl b/gen/templates/agent_indirect.gotmpl new file mode 100644 index 0000000..099a51a --- /dev/null +++ b/gen/templates/agent_indirect.gotmpl @@ -0,0 +1,60 @@ +// Package {{ .PackageName }} provides a client for the "{{ .CanisterName }}" canister. +// Do NOT edit this file. It was automatically generated by https://github.com/aviate-labs/agent-go. +package {{ .PackageName }} + +import ( + "github.com/aviate-labs/agent-go" + {{ if .UsedIDL }}"github.com/aviate-labs/agent-go/candid/idl"{{ end }} + "github.com/aviate-labs/agent-go/principal" +) + +{{- range .Definitions }} + +type {{ .Name }} {{ if .Eq }}= {{end}}{{ .Type }} +{{- end }} + +// {{ .AgentName }}Agent is a client for the "{{ .CanisterName }}" canister. +type {{ .AgentName }}Agent struct { + a *agent.Agent + canisterId principal.Principal +} + +// New{{ .AgentName }}Agent creates a new agent for the "{{ .CanisterName }}" canister. +func New{{ .AgentName }}Agent(canisterId principal.Principal, config agent.Config) (*{{ .AgentName }}Agent, error) { + a, err := agent.New(config) + if err != nil { + return nil, err + } + return &{{ .AgentName }}Agent{ + a: a, + canisterId: canisterId, + }, nil +} +{{- range .Methods }} + +// {{ .Name }} calls the "{{ .RawName }}" method on the "{{ $.CanisterName }}" canister. +func (a {{ $.AgentName }}Agent) {{ .Name }}({{ range $i, $e := .ArgumentTypes }}{{ if $i }}, {{ end }}{{ $e.Name }} {{ $e.Type }}{{ end }}) {{ if .ReturnTypes }}({{ range .ReturnTypes }}*{{ . }}, {{ end }}error){{ else }}error{{ end }} { + {{ range $i, $e := .ReturnTypes -}} + var r{{ $i }} {{ $e }} + {{ end -}} + if err := a.a.{{ .Type }}( + a.canisterId, + "{{ .RawName }}", + []any{{ "{" }}{{ range $i, $e := .ArgumentTypes }}{{ if $i }}, {{ end }}{{ $e.Name }}{{ end }}{{ "}" }}, + []any{{ "{" }}{{ range $i, $e := .ReturnTypes }}{{ if $i }}, {{ end }}&r{{ $i }}{{ end }}{{ "}"}}, + ); err != nil { + return {{ range .ReturnTypes }}nil, {{ end }}err + } + return {{ range $i, $_ := .ReturnTypes }}&r{{ $i }}, {{ end }}nil +} + +// {{ .Name }}{{ .Type }} creates an indirect representation of the "{{ .RawName }}" method on the "{{ $.CanisterName }}" canister. +func (a {{ $.AgentName }}Agent) {{ .Name }}{{ .Type }}({{ range $i, $e := .ArgumentTypes }}{{ if $i }}, {{ end }}{{ $e.Name }} {{ $e.Type }}{{ end }}) (*agent.{{ .Type }},error) { + return a.a.Create{{ .Type }}( + a.canisterId, + "{{ .RawName }}",{{ range $i, $e := .ArgumentTypes }} + {{ $e.Name }},{{ end }} + ) +} + +{{- end }} \ No newline at end of file diff --git a/ic/ic/agent.go b/ic/ic/agent.go index 1882fa9..da8071e 100755 --- a/ic/ic/agent.go +++ b/ic/ic/agent.go @@ -40,6 +40,15 @@ func (a Agent) BitcoinGetBalance(arg0 BitcoinGetBalanceArgs) (*BitcoinGetBalance return &r0, nil } +// BitcoinGetBalanceCall creates an indirect representation of the "bitcoin_get_balance" method on the "ic" canister. +func (a Agent) BitcoinGetBalanceCall(arg0 BitcoinGetBalanceArgs) (*agent.Call, error) { + return a.a.CreateCall( + a.canisterId, + "bitcoin_get_balance", + arg0, + ) +} + // BitcoinGetBalanceQuery calls the "bitcoin_get_balance_query" method on the "ic" canister. func (a Agent) BitcoinGetBalanceQuery(arg0 BitcoinGetBalanceQueryArgs) (*BitcoinGetBalanceQueryResult, error) { var r0 BitcoinGetBalanceQueryResult @@ -54,6 +63,15 @@ func (a Agent) BitcoinGetBalanceQuery(arg0 BitcoinGetBalanceQueryArgs) (*Bitcoin return &r0, nil } +// BitcoinGetBalanceQueryQuery creates an indirect representation of the "bitcoin_get_balance_query" method on the "ic" canister. +func (a Agent) BitcoinGetBalanceQueryQuery(arg0 BitcoinGetBalanceQueryArgs) (*agent.Query, error) { + return a.a.CreateQuery( + a.canisterId, + "bitcoin_get_balance_query", + arg0, + ) +} + // BitcoinGetCurrentFeePercentiles calls the "bitcoin_get_current_fee_percentiles" method on the "ic" canister. func (a Agent) BitcoinGetCurrentFeePercentiles(arg0 BitcoinGetCurrentFeePercentilesArgs) (*BitcoinGetCurrentFeePercentilesResult, error) { var r0 BitcoinGetCurrentFeePercentilesResult @@ -68,6 +86,15 @@ func (a Agent) BitcoinGetCurrentFeePercentiles(arg0 BitcoinGetCurrentFeePercenti return &r0, nil } +// BitcoinGetCurrentFeePercentilesCall creates an indirect representation of the "bitcoin_get_current_fee_percentiles" method on the "ic" canister. +func (a Agent) BitcoinGetCurrentFeePercentilesCall(arg0 BitcoinGetCurrentFeePercentilesArgs) (*agent.Call, error) { + return a.a.CreateCall( + a.canisterId, + "bitcoin_get_current_fee_percentiles", + arg0, + ) +} + // BitcoinGetUtxos calls the "bitcoin_get_utxos" method on the "ic" canister. func (a Agent) BitcoinGetUtxos(arg0 BitcoinGetUtxosArgs) (*BitcoinGetUtxosResult, error) { var r0 BitcoinGetUtxosResult @@ -82,6 +109,15 @@ func (a Agent) BitcoinGetUtxos(arg0 BitcoinGetUtxosArgs) (*BitcoinGetUtxosResult return &r0, nil } +// BitcoinGetUtxosCall creates an indirect representation of the "bitcoin_get_utxos" method on the "ic" canister. +func (a Agent) BitcoinGetUtxosCall(arg0 BitcoinGetUtxosArgs) (*agent.Call, error) { + return a.a.CreateCall( + a.canisterId, + "bitcoin_get_utxos", + arg0, + ) +} + // BitcoinGetUtxosQuery calls the "bitcoin_get_utxos_query" method on the "ic" canister. func (a Agent) BitcoinGetUtxosQuery(arg0 BitcoinGetUtxosQueryArgs) (*BitcoinGetUtxosQueryResult, error) { var r0 BitcoinGetUtxosQueryResult @@ -96,6 +132,15 @@ func (a Agent) BitcoinGetUtxosQuery(arg0 BitcoinGetUtxosQueryArgs) (*BitcoinGetU return &r0, nil } +// BitcoinGetUtxosQueryQuery creates an indirect representation of the "bitcoin_get_utxos_query" method on the "ic" canister. +func (a Agent) BitcoinGetUtxosQueryQuery(arg0 BitcoinGetUtxosQueryArgs) (*agent.Query, error) { + return a.a.CreateQuery( + a.canisterId, + "bitcoin_get_utxos_query", + arg0, + ) +} + // BitcoinSendTransaction calls the "bitcoin_send_transaction" method on the "ic" canister. func (a Agent) BitcoinSendTransaction(arg0 BitcoinSendTransactionArgs) error { if err := a.a.Call( @@ -109,6 +154,15 @@ func (a Agent) BitcoinSendTransaction(arg0 BitcoinSendTransactionArgs) error { return nil } +// BitcoinSendTransactionCall creates an indirect representation of the "bitcoin_send_transaction" method on the "ic" canister. +func (a Agent) BitcoinSendTransactionCall(arg0 BitcoinSendTransactionArgs) (*agent.Call, error) { + return a.a.CreateCall( + a.canisterId, + "bitcoin_send_transaction", + arg0, + ) +} + // CanisterInfo calls the "canister_info" method on the "ic" canister. func (a Agent) CanisterInfo(arg0 CanisterInfoArgs) (*CanisterInfoResult, error) { var r0 CanisterInfoResult @@ -123,6 +177,15 @@ func (a Agent) CanisterInfo(arg0 CanisterInfoArgs) (*CanisterInfoResult, error) return &r0, nil } +// CanisterInfoCall creates an indirect representation of the "canister_info" method on the "ic" canister. +func (a Agent) CanisterInfoCall(arg0 CanisterInfoArgs) (*agent.Call, error) { + return a.a.CreateCall( + a.canisterId, + "canister_info", + arg0, + ) +} + // CanisterStatus calls the "canister_status" method on the "ic" canister. func (a Agent) CanisterStatus(arg0 CanisterStatusArgs) (*CanisterStatusResult, error) { var r0 CanisterStatusResult @@ -137,6 +200,15 @@ func (a Agent) CanisterStatus(arg0 CanisterStatusArgs) (*CanisterStatusResult, e return &r0, nil } +// CanisterStatusCall creates an indirect representation of the "canister_status" method on the "ic" canister. +func (a Agent) CanisterStatusCall(arg0 CanisterStatusArgs) (*agent.Call, error) { + return a.a.CreateCall( + a.canisterId, + "canister_status", + arg0, + ) +} + // ClearChunkStore calls the "clear_chunk_store" method on the "ic" canister. func (a Agent) ClearChunkStore(arg0 ClearChunkStoreArgs) error { if err := a.a.Call( @@ -150,6 +222,15 @@ func (a Agent) ClearChunkStore(arg0 ClearChunkStoreArgs) error { return nil } +// ClearChunkStoreCall creates an indirect representation of the "clear_chunk_store" method on the "ic" canister. +func (a Agent) ClearChunkStoreCall(arg0 ClearChunkStoreArgs) (*agent.Call, error) { + return a.a.CreateCall( + a.canisterId, + "clear_chunk_store", + arg0, + ) +} + // CreateCanister calls the "create_canister" method on the "ic" canister. func (a Agent) CreateCanister(arg0 CreateCanisterArgs) (*CreateCanisterResult, error) { var r0 CreateCanisterResult @@ -164,6 +245,15 @@ func (a Agent) CreateCanister(arg0 CreateCanisterArgs) (*CreateCanisterResult, e return &r0, nil } +// CreateCanisterCall creates an indirect representation of the "create_canister" method on the "ic" canister. +func (a Agent) CreateCanisterCall(arg0 CreateCanisterArgs) (*agent.Call, error) { + return a.a.CreateCall( + a.canisterId, + "create_canister", + arg0, + ) +} + // DeleteCanister calls the "delete_canister" method on the "ic" canister. func (a Agent) DeleteCanister(arg0 DeleteCanisterArgs) error { if err := a.a.Call( @@ -177,6 +267,15 @@ func (a Agent) DeleteCanister(arg0 DeleteCanisterArgs) error { return nil } +// DeleteCanisterCall creates an indirect representation of the "delete_canister" method on the "ic" canister. +func (a Agent) DeleteCanisterCall(arg0 DeleteCanisterArgs) (*agent.Call, error) { + return a.a.CreateCall( + a.canisterId, + "delete_canister", + arg0, + ) +} + // DepositCycles calls the "deposit_cycles" method on the "ic" canister. func (a Agent) DepositCycles(arg0 DepositCyclesArgs) error { if err := a.a.Call( @@ -190,6 +289,15 @@ func (a Agent) DepositCycles(arg0 DepositCyclesArgs) error { return nil } +// DepositCyclesCall creates an indirect representation of the "deposit_cycles" method on the "ic" canister. +func (a Agent) DepositCyclesCall(arg0 DepositCyclesArgs) (*agent.Call, error) { + return a.a.CreateCall( + a.canisterId, + "deposit_cycles", + arg0, + ) +} + // EcdsaPublicKey calls the "ecdsa_public_key" method on the "ic" canister. func (a Agent) EcdsaPublicKey(arg0 EcdsaPublicKeyArgs) (*EcdsaPublicKeyResult, error) { var r0 EcdsaPublicKeyResult @@ -204,6 +312,15 @@ func (a Agent) EcdsaPublicKey(arg0 EcdsaPublicKeyArgs) (*EcdsaPublicKeyResult, e return &r0, nil } +// EcdsaPublicKeyCall creates an indirect representation of the "ecdsa_public_key" method on the "ic" canister. +func (a Agent) EcdsaPublicKeyCall(arg0 EcdsaPublicKeyArgs) (*agent.Call, error) { + return a.a.CreateCall( + a.canisterId, + "ecdsa_public_key", + arg0, + ) +} + // HttpRequest calls the "http_request" method on the "ic" canister. func (a Agent) HttpRequest(arg0 HttpRequestArgs) (*HttpRequestResult, error) { var r0 HttpRequestResult @@ -218,6 +335,15 @@ func (a Agent) HttpRequest(arg0 HttpRequestArgs) (*HttpRequestResult, error) { return &r0, nil } +// HttpRequestCall creates an indirect representation of the "http_request" method on the "ic" canister. +func (a Agent) HttpRequestCall(arg0 HttpRequestArgs) (*agent.Call, error) { + return a.a.CreateCall( + a.canisterId, + "http_request", + arg0, + ) +} + // InstallChunkedCode calls the "install_chunked_code" method on the "ic" canister. func (a Agent) InstallChunkedCode(arg0 InstallChunkedCodeArgs) error { if err := a.a.Call( @@ -231,6 +357,15 @@ func (a Agent) InstallChunkedCode(arg0 InstallChunkedCodeArgs) error { return nil } +// InstallChunkedCodeCall creates an indirect representation of the "install_chunked_code" method on the "ic" canister. +func (a Agent) InstallChunkedCodeCall(arg0 InstallChunkedCodeArgs) (*agent.Call, error) { + return a.a.CreateCall( + a.canisterId, + "install_chunked_code", + arg0, + ) +} + // InstallCode calls the "install_code" method on the "ic" canister. func (a Agent) InstallCode(arg0 InstallCodeArgs) error { if err := a.a.Call( @@ -244,6 +379,15 @@ func (a Agent) InstallCode(arg0 InstallCodeArgs) error { return nil } +// InstallCodeCall creates an indirect representation of the "install_code" method on the "ic" canister. +func (a Agent) InstallCodeCall(arg0 InstallCodeArgs) (*agent.Call, error) { + return a.a.CreateCall( + a.canisterId, + "install_code", + arg0, + ) +} + // NodeMetricsHistory calls the "node_metrics_history" method on the "ic" canister. func (a Agent) NodeMetricsHistory(arg0 NodeMetricsHistoryArgs) (*NodeMetricsHistoryResult, error) { var r0 NodeMetricsHistoryResult @@ -258,6 +402,15 @@ func (a Agent) NodeMetricsHistory(arg0 NodeMetricsHistoryArgs) (*NodeMetricsHist return &r0, nil } +// NodeMetricsHistoryCall creates an indirect representation of the "node_metrics_history" method on the "ic" canister. +func (a Agent) NodeMetricsHistoryCall(arg0 NodeMetricsHistoryArgs) (*agent.Call, error) { + return a.a.CreateCall( + a.canisterId, + "node_metrics_history", + arg0, + ) +} + // ProvisionalCreateCanisterWithCycles calls the "provisional_create_canister_with_cycles" method on the "ic" canister. func (a Agent) ProvisionalCreateCanisterWithCycles(arg0 ProvisionalCreateCanisterWithCyclesArgs) (*ProvisionalCreateCanisterWithCyclesResult, error) { var r0 ProvisionalCreateCanisterWithCyclesResult @@ -272,6 +425,15 @@ func (a Agent) ProvisionalCreateCanisterWithCycles(arg0 ProvisionalCreateCaniste return &r0, nil } +// ProvisionalCreateCanisterWithCyclesCall creates an indirect representation of the "provisional_create_canister_with_cycles" method on the "ic" canister. +func (a Agent) ProvisionalCreateCanisterWithCyclesCall(arg0 ProvisionalCreateCanisterWithCyclesArgs) (*agent.Call, error) { + return a.a.CreateCall( + a.canisterId, + "provisional_create_canister_with_cycles", + arg0, + ) +} + // ProvisionalTopUpCanister calls the "provisional_top_up_canister" method on the "ic" canister. func (a Agent) ProvisionalTopUpCanister(arg0 ProvisionalTopUpCanisterArgs) error { if err := a.a.Call( @@ -285,6 +447,15 @@ func (a Agent) ProvisionalTopUpCanister(arg0 ProvisionalTopUpCanisterArgs) error return nil } +// ProvisionalTopUpCanisterCall creates an indirect representation of the "provisional_top_up_canister" method on the "ic" canister. +func (a Agent) ProvisionalTopUpCanisterCall(arg0 ProvisionalTopUpCanisterArgs) (*agent.Call, error) { + return a.a.CreateCall( + a.canisterId, + "provisional_top_up_canister", + arg0, + ) +} + // RawRand calls the "raw_rand" method on the "ic" canister. func (a Agent) RawRand() (*RawRandResult, error) { var r0 RawRandResult @@ -299,6 +470,14 @@ func (a Agent) RawRand() (*RawRandResult, error) { return &r0, nil } +// RawRandCall creates an indirect representation of the "raw_rand" method on the "ic" canister. +func (a Agent) RawRandCall() (*agent.Call, error) { + return a.a.CreateCall( + a.canisterId, + "raw_rand", + ) +} + // SignWithEcdsa calls the "sign_with_ecdsa" method on the "ic" canister. func (a Agent) SignWithEcdsa(arg0 SignWithEcdsaArgs) (*SignWithEcdsaResult, error) { var r0 SignWithEcdsaResult @@ -313,6 +492,15 @@ func (a Agent) SignWithEcdsa(arg0 SignWithEcdsaArgs) (*SignWithEcdsaResult, erro return &r0, nil } +// SignWithEcdsaCall creates an indirect representation of the "sign_with_ecdsa" method on the "ic" canister. +func (a Agent) SignWithEcdsaCall(arg0 SignWithEcdsaArgs) (*agent.Call, error) { + return a.a.CreateCall( + a.canisterId, + "sign_with_ecdsa", + arg0, + ) +} + // StartCanister calls the "start_canister" method on the "ic" canister. func (a Agent) StartCanister(arg0 StartCanisterArgs) error { if err := a.a.Call( @@ -326,6 +514,15 @@ func (a Agent) StartCanister(arg0 StartCanisterArgs) error { return nil } +// StartCanisterCall creates an indirect representation of the "start_canister" method on the "ic" canister. +func (a Agent) StartCanisterCall(arg0 StartCanisterArgs) (*agent.Call, error) { + return a.a.CreateCall( + a.canisterId, + "start_canister", + arg0, + ) +} + // StopCanister calls the "stop_canister" method on the "ic" canister. func (a Agent) StopCanister(arg0 StopCanisterArgs) error { if err := a.a.Call( @@ -339,6 +536,15 @@ func (a Agent) StopCanister(arg0 StopCanisterArgs) error { return nil } +// StopCanisterCall creates an indirect representation of the "stop_canister" method on the "ic" canister. +func (a Agent) StopCanisterCall(arg0 StopCanisterArgs) (*agent.Call, error) { + return a.a.CreateCall( + a.canisterId, + "stop_canister", + arg0, + ) +} + // StoredChunks calls the "stored_chunks" method on the "ic" canister. func (a Agent) StoredChunks(arg0 StoredChunksArgs) (*StoredChunksResult, error) { var r0 StoredChunksResult @@ -353,6 +559,15 @@ func (a Agent) StoredChunks(arg0 StoredChunksArgs) (*StoredChunksResult, error) return &r0, nil } +// StoredChunksCall creates an indirect representation of the "stored_chunks" method on the "ic" canister. +func (a Agent) StoredChunksCall(arg0 StoredChunksArgs) (*agent.Call, error) { + return a.a.CreateCall( + a.canisterId, + "stored_chunks", + arg0, + ) +} + // UninstallCode calls the "uninstall_code" method on the "ic" canister. func (a Agent) UninstallCode(arg0 UninstallCodeArgs) error { if err := a.a.Call( @@ -366,6 +581,15 @@ func (a Agent) UninstallCode(arg0 UninstallCodeArgs) error { return nil } +// UninstallCodeCall creates an indirect representation of the "uninstall_code" method on the "ic" canister. +func (a Agent) UninstallCodeCall(arg0 UninstallCodeArgs) (*agent.Call, error) { + return a.a.CreateCall( + a.canisterId, + "uninstall_code", + arg0, + ) +} + // UpdateSettings calls the "update_settings" method on the "ic" canister. func (a Agent) UpdateSettings(arg0 UpdateSettingsArgs) error { if err := a.a.Call( @@ -379,6 +603,15 @@ func (a Agent) UpdateSettings(arg0 UpdateSettingsArgs) error { return nil } +// UpdateSettingsCall creates an indirect representation of the "update_settings" method on the "ic" canister. +func (a Agent) UpdateSettingsCall(arg0 UpdateSettingsArgs) (*agent.Call, error) { + return a.a.CreateCall( + a.canisterId, + "update_settings", + arg0, + ) +} + // UploadChunk calls the "upload_chunk" method on the "ic" canister. func (a Agent) UploadChunk(arg0 UploadChunkArgs) (*UploadChunkResult, error) { var r0 UploadChunkResult @@ -393,6 +626,15 @@ func (a Agent) UploadChunk(arg0 UploadChunkArgs) (*UploadChunkResult, error) { return &r0, nil } +// UploadChunkCall creates an indirect representation of the "upload_chunk" method on the "ic" canister. +func (a Agent) UploadChunkCall(arg0 UploadChunkArgs) (*agent.Call, error) { + return a.a.CreateCall( + a.canisterId, + "upload_chunk", + arg0, + ) +} + type BitcoinAddress = string type BitcoinGetBalanceArgs struct { diff --git a/ic/sns/ledger/agent.go b/ic/sns/ledger/agent.go index 771d2d1..c8f29d1 100755 --- a/ic/sns/ledger/agent.go +++ b/ic/sns/ledger/agent.go @@ -289,6 +289,20 @@ func (a Agent) Icrc3GetArchives(arg0 GetArchivesArgs) (*GetArchivesResult, error return &r0, nil } +// Icrc3GetBlocks calls the "icrc3_get_blocks" method on the "ledger" canister. +func (a Agent) Icrc3GetBlocks(arg0 []GetBlocksArgs) (*GetBlocksResult, error) { + var r0 GetBlocksResult + if err := a.a.Query( + a.canisterId, + "icrc3_get_blocks", + []any{arg0}, + []any{&r0}, + ); err != nil { + return nil, err + } + return &r0, nil +} + // Icrc3GetTipCertificate calls the "icrc3_get_tip_certificate" method on the "ledger" canister. func (a Agent) Icrc3GetTipCertificate() (**ICRC3DataCertificate, error) { var r0 *ICRC3DataCertificate @@ -409,6 +423,17 @@ type Burn struct { Spender *Account `ic:"spender,omitempty" json:"spender,omitempty"` } +type ChangeArchiveOptions struct { + NumBlocksToArchive *uint64 `ic:"num_blocks_to_archive,omitempty" json:"num_blocks_to_archive,omitempty"` + MaxTransactionsPerResponse *uint64 `ic:"max_transactions_per_response,omitempty" json:"max_transactions_per_response,omitempty"` + TriggerThreshold *uint64 `ic:"trigger_threshold,omitempty" json:"trigger_threshold,omitempty"` + MaxMessageSizeBytes *uint64 `ic:"max_message_size_bytes,omitempty" json:"max_message_size_bytes,omitempty"` + CyclesForArchiveCreation *uint64 `ic:"cycles_for_archive_creation,omitempty" json:"cycles_for_archive_creation,omitempty"` + NodeMaxMemorySizeBytes *uint64 `ic:"node_max_memory_size_bytes,omitempty" json:"node_max_memory_size_bytes,omitempty"` + ControllerId *principal.Principal `ic:"controller_id,omitempty" json:"controller_id,omitempty"` + MoreControllerIds *[]principal.Principal `ic:"more_controller_ids,omitempty" json:"more_controller_ids,omitempty"` +} + type ChangeFeeCollector struct { Unset *idl.Null `ic:"Unset,variant"` SetTo *Account `ic:"SetTo,variant"` @@ -452,6 +477,19 @@ type GetBlocksResponse struct { } `ic:"archived_blocks" json:"archived_blocks"` } +type GetBlocksResult struct { + LogLength idl.Nat `ic:"log_length" json:"log_length"` + Blocks []struct { + Id idl.Nat `ic:"id" json:"id"` + Block ICRC3Value `ic:"block" json:"block"` + } `ic:"blocks" json:"blocks"` + ArchivedBlocks []struct { + Args []GetBlocksArgs `ic:"args" json:"args"` + Callback struct { /* NOT SUPPORTED */ + } `ic:"callback" json:"callback"` + } `ic:"archived_blocks" json:"archived_blocks"` +} + type GetTransactionsRequest struct { Start TxIndex `ic:"start" json:"start"` Length idl.Nat `ic:"length" json:"length"` @@ -492,6 +530,18 @@ type ICRC3DataCertificate struct { HashTree []byte `ic:"hash_tree" json:"hash_tree"` } +type ICRC3Value struct { + Blob *[]byte `ic:"Blob,variant"` + Text *string `ic:"Text,variant"` + Nat *idl.Nat `ic:"Nat,variant"` + Int *idl.Int `ic:"Int,variant"` + Array *[]ICRC3Value `ic:"Array,variant"` + Map *[]struct { + Field0 string `ic:"0" json:"0"` + Field1 ICRC3Value `ic:"1" json:"1"` + } `ic:"Map,variant"` +} + type InitArgs struct { MintingAccount Account `ic:"minting_account" json:"minting_account"` FeeCollectorAccount *Account `ic:"fee_collector_account,omitempty" json:"fee_collector_account,omitempty"` @@ -674,14 +724,15 @@ type UpgradeArgs struct { Field0 string `ic:"0" json:"0"` Field1 MetadataValue `ic:"1" json:"1"` } `ic:"metadata,omitempty" json:"metadata,omitempty"` - TokenSymbol *string `ic:"token_symbol,omitempty" json:"token_symbol,omitempty"` - TokenName *string `ic:"token_name,omitempty" json:"token_name,omitempty"` - TransferFee *idl.Nat `ic:"transfer_fee,omitempty" json:"transfer_fee,omitempty"` - ChangeFeeCollector *ChangeFeeCollector `ic:"change_fee_collector,omitempty" json:"change_fee_collector,omitempty"` - MaxMemoLength *uint16 `ic:"max_memo_length,omitempty" json:"max_memo_length,omitempty"` - FeatureFlags *FeatureFlags `ic:"feature_flags,omitempty" json:"feature_flags,omitempty"` - MaximumNumberOfAccounts *uint64 `ic:"maximum_number_of_accounts,omitempty" json:"maximum_number_of_accounts,omitempty"` - AccountsOverflowTrimQuantity *uint64 `ic:"accounts_overflow_trim_quantity,omitempty" json:"accounts_overflow_trim_quantity,omitempty"` + TokenSymbol *string `ic:"token_symbol,omitempty" json:"token_symbol,omitempty"` + TokenName *string `ic:"token_name,omitempty" json:"token_name,omitempty"` + TransferFee *idl.Nat `ic:"transfer_fee,omitempty" json:"transfer_fee,omitempty"` + ChangeFeeCollector *ChangeFeeCollector `ic:"change_fee_collector,omitempty" json:"change_fee_collector,omitempty"` + MaxMemoLength *uint16 `ic:"max_memo_length,omitempty" json:"max_memo_length,omitempty"` + FeatureFlags *FeatureFlags `ic:"feature_flags,omitempty" json:"feature_flags,omitempty"` + MaximumNumberOfAccounts *uint64 `ic:"maximum_number_of_accounts,omitempty" json:"maximum_number_of_accounts,omitempty"` + AccountsOverflowTrimQuantity *uint64 `ic:"accounts_overflow_trim_quantity,omitempty" json:"accounts_overflow_trim_quantity,omitempty"` + ChangeArchiveOptions *ChangeArchiveOptions `ic:"change_archive_options,omitempty" json:"change_archive_options,omitempty"` } type Value struct { diff --git a/ic/sns/root/agent.go b/ic/sns/root/agent.go index 2a936a0..a726040 100755 --- a/ic/sns/root/agent.go +++ b/ic/sns/root/agent.go @@ -260,6 +260,7 @@ type ManageDappCanisterSettingsRequest struct { CanisterIds []principal.Principal `ic:"canister_ids" json:"canister_ids"` ReservedCyclesLimit *uint64 `ic:"reserved_cycles_limit,omitempty" json:"reserved_cycles_limit,omitempty"` LogVisibility *int32 `ic:"log_visibility,omitempty" json:"log_visibility,omitempty"` + WasmMemoryLimit *uint64 `ic:"wasm_memory_limit,omitempty" json:"wasm_memory_limit,omitempty"` MemoryAllocation *uint64 `ic:"memory_allocation,omitempty" json:"memory_allocation,omitempty"` ComputeAllocation *uint64 `ic:"compute_allocation,omitempty" json:"compute_allocation,omitempty"` } diff --git a/ic/sns/testdata/did/ledger.did b/ic/sns/testdata/did/ledger.did index c8cc56b..e2799db 100644 --- a/ic/sns/testdata/did/ledger.did +++ b/ic/sns/testdata/did/ledger.did @@ -125,6 +125,17 @@ type ChangeFeeCollector = variant { Unset; SetTo: Account; }; +type ChangeArchiveOptions = record { + num_blocks_to_archive : opt nat64; + max_transactions_per_response : opt nat64; + trigger_threshold : opt nat64; + max_message_size_bytes : opt nat64; + cycles_for_archive_creation : opt nat64; + node_max_memory_size_bytes : opt nat64; + controller_id : opt principal; + more_controller_ids : opt vec principal; +}; + type UpgradeArgs = record { metadata : opt vec record { text; MetadataValue }; token_symbol : opt text; @@ -135,6 +146,7 @@ type UpgradeArgs = record { feature_flags : opt FeatureFlags; maximum_number_of_accounts: opt nat64; accounts_overflow_trim_quantity: opt nat64; + change_archive_options : opt ChangeArchiveOptions; }; type LedgerArg = variant { @@ -243,14 +255,14 @@ type Transfer = record { spender : opt Account; }; -type Value = variant { - Blob : blob; - Text : text; +type Value = variant { + Blob : blob; + Text : text; Nat : nat; - Nat64: nat64; + Nat64: nat64; Int : int; - Array : vec Value; - Map : Map; + Array : vec Value; + Map : Map; }; type Map = vec record { text; Value }; @@ -364,6 +376,15 @@ type ArchiveInfo = record { block_range_end: BlockIndex; }; +type ICRC3Value = variant { + Blob : blob; + Text : text; + Nat : nat; + Int : int; + Array : vec ICRC3Value; + Map : vec record { text; ICRC3Value }; +}; + type GetArchivesArgs = record { // The last archive seen by the client. // The Ledger will return archives coming @@ -383,6 +404,19 @@ type GetArchivesResult = vec record { end : nat; }; +type GetBlocksResult = record { + // Total number of blocks in the + // block log + log_length : nat; + + blocks : vec record { id : nat; block: ICRC3Value }; + + archived_blocks : vec record { + args : vec GetBlocksArgs; + callback : func (vec GetBlocksArgs) -> (GetBlocksResult) query; + }; +}; + type ICRC3DataCertificate = record { // See https://internetcomputer.org/docs/current/references/ic-interface-spec#certification certificate : blob; @@ -394,8 +428,8 @@ type ICRC3DataCertificate = record { service : (ledger_arg : LedgerArg) -> { archives : () -> (vec ArchiveInfo) query; get_transactions : (GetTransactionsRequest) -> (GetTransactionsResponse) query; - get_blocks : (GetBlocksArgs) -> (GetBlocksResponse) query; - get_data_certificate : () -> (DataCertificate) query; + get_blocks : (GetBlocksArgs) -> (GetBlocksResponse) query; + get_data_certificate : () -> (DataCertificate) query; icrc1_name : () -> (text) query; icrc1_symbol : () -> (text) query; @@ -407,12 +441,13 @@ service : (ledger_arg : LedgerArg) -> { icrc1_balance_of : (Account) -> (Tokens) query; icrc1_transfer : (TransferArg) -> (TransferResult); icrc1_supported_standards : () -> (vec StandardRecord) query; - + icrc2_approve : (ApproveArgs) -> (ApproveResult); icrc2_allowance : (AllowanceArgs) -> (Allowance) query; icrc2_transfer_from : (TransferFromArgs) -> (TransferFromResult); icrc3_get_archives : (GetArchivesArgs) -> (GetArchivesResult) query; icrc3_get_tip_certificate : () -> (opt ICRC3DataCertificate) query; + icrc3_get_blocks : (vec GetBlocksArgs) -> (GetBlocksResult) query; icrc3_supported_block_types : () -> (vec record { block_type : text; url : text }) query; } diff --git a/ic/sns/testdata/did/root.did b/ic/sns/testdata/did/root.did index da738d6..b8b96aa 100644 --- a/ic/sns/testdata/did/root.did +++ b/ic/sns/testdata/did/root.did @@ -73,6 +73,7 @@ type ManageDappCanisterSettingsRequest = record { canister_ids : vec principal; reserved_cycles_limit : opt nat64; log_visibility : opt int32; + wasm_memory_limit : opt nat64; memory_allocation : opt nat64; compute_allocation : opt nat64; }; diff --git a/ic/testdata/did/wallet.did b/ic/testdata/did/wallet.did index 7b0ec6b..729c10e 100644 --- a/ic/testdata/did/wallet.did +++ b/ic/testdata/did/wallet.did @@ -161,6 +161,14 @@ type WalletResultCall = variant { Err : text; }; +type WalletResultCallWithMaxCycles = variant { + Ok : record { + return: blob; + attached_cycles: nat; + }; + Err : text; +}; + type CanisterSettings = record { controller: opt principal; controllers: opt vec principal; @@ -258,6 +266,11 @@ service : { args: blob; cycles: nat; }) -> (WalletResultCall); + wallet_call_with_max_cycles: (record{ + canister: principal; + method_name: text; + args: blob; + }) -> (WalletResultCallWithMaxCycles); // Address book add_address: (address: AddressEntry) -> (); diff --git a/ic/testdata/gen.go b/ic/testdata/gen.go index bf86121..2def76f 100644 --- a/ic/testdata/gen.go +++ b/ic/testdata/gen.go @@ -106,6 +106,9 @@ func main() { if err != nil { log.Panic(err) } + if name == "ic" { + g = g.Indirect() + } raw, err := g.Generate() if err != nil { log.Panic(err) diff --git a/ic/types_agent_test.go b/ic/types_agent_test.go index c5b7be6..7f36a76 100755 --- a/ic/types_agent_test.go +++ b/ic/types_agent_test.go @@ -8,13 +8,13 @@ import ( "github.com/aviate-labs/agent-go/principal" ) -// Agent is a client for the "types" canister. +// TypesAgent is a client for the "types" canister. type TypesAgent struct { a *agent.Agent canisterId principal.Principal } -// NewAgent creates a new agent for the "types" canister. +// NewTypesAgent creates a new agent for the "types" canister. func NewTypesAgent(canisterId principal.Principal, config agent.Config) (*TypesAgent, error) { a, err := agent.New(config) if err != nil { diff --git a/ic/wallet/actor.mo b/ic/wallet/actor.mo index d676607..071882b 100755 --- a/ic/wallet/actor.mo +++ b/ic/wallet/actor.mo @@ -67,6 +67,9 @@ actor class _wallet() : async actor {} { public shared func wallet_call128(_arg0 : { canister : Principal; method_name : Text; args : Blob; cycles : Nat }) : async (T.WalletResultCall) { (#Ok({ return = "xAABD0E49110AC27F321B5538A4BD8E1D253F8AFFD1A9C81C5B6EBD2C7EE44EF3" })) }; + public shared func wallet_call_with_max_cycles(_arg0 : { canister : Principal; method_name : Text; args : Blob }) : async (T.WalletResultCallWithMaxCycles) { + (#Ok({ return = "xAABD0E49110AC27F321B5538A4BD8E1D253F8AFFD1A9C81C5B6EBD2C7EE44EF3"; attached_cycles = 9207851698408531338 })) + }; public shared func add_address(_arg0 : T.AddressEntry) : async () { () }; diff --git a/ic/wallet/agent.go b/ic/wallet/agent.go index 18cecb8..aa8d696 100755 --- a/ic/wallet/agent.go +++ b/ic/wallet/agent.go @@ -410,6 +410,24 @@ func (a Agent) WalletCall128(arg0 struct { return &r0, nil } +// WalletCallWithMaxCycles calls the "wallet_call_with_max_cycles" method on the "wallet" canister. +func (a Agent) WalletCallWithMaxCycles(arg0 struct { + Canister principal.Principal `ic:"canister" json:"canister"` + MethodName string `ic:"method_name" json:"method_name"` + Args []byte `ic:"args" json:"args"` +}) (*WalletResultCallWithMaxCycles, error) { + var r0 WalletResultCallWithMaxCycles + if err := a.a.Call( + a.canisterId, + "wallet_call_with_max_cycles", + []any{arg0}, + []any{&r0}, + ); err != nil { + return nil, err + } + return &r0, nil +} + // WalletCreateCanister calls the "wallet_create_canister" method on the "wallet" canister. func (a Agent) WalletCreateCanister(arg0 CreateCanisterArgs) (*WalletResultCreate, error) { var r0 WalletResultCreate @@ -733,6 +751,14 @@ type WalletResultCall struct { Err *string `ic:"Err,variant"` } +type WalletResultCallWithMaxCycles struct { + Ok *struct { + Return []byte `ic:"return" json:"return"` + AttachedCycles idl.Nat `ic:"attached_cycles" json:"attached_cycles"` + } `ic:"Ok,variant"` + Err *string `ic:"Err,variant"` +} + type WalletResultCreate struct { Ok *struct { CanisterId principal.Principal `ic:"canister_id" json:"canister_id"` diff --git a/ic/wallet/agent_test.go b/ic/wallet/agent_test.go index 017ce0c..01bc5d5 100755 --- a/ic/wallet/agent_test.go +++ b/ic/wallet/agent_test.go @@ -716,6 +716,48 @@ func Test_WalletCall128(t *testing.T) { } +// Test_WalletCallWithMaxCycles tests the "wallet_call_with_max_cycles" method on the "wallet" canister. +func Test_WalletCallWithMaxCycles(t *testing.T) { + a, err := newAgent([]mock.Method{ + { + Name: "wallet_call_with_max_cycles", + Arguments: []any{new(struct { + Canister principal.Principal `ic:"canister" json:"canister"` + MethodName string `ic:"method_name" json:"method_name"` + Args []byte `ic:"args" json:"args"` + })}, + Handler: func(request mock.Request) ([]any, error) { + return []any{wallet.WalletResultCallWithMaxCycles{ + Ok: idl.Ptr(struct { + Return []byte `ic:"return" json:"return"` + AttachedCycles idl.Nat `ic:"attached_cycles" json:"attached_cycles"` + }{ + *new([]byte), + idl.NewNat(uint(0)), + }), + }}, nil + }, + }, + }) + if err != nil { + t.Fatal(err) + } + + var a0 = struct { + Canister principal.Principal `ic:"canister" json:"canister"` + MethodName string `ic:"method_name" json:"method_name"` + Args []byte `ic:"args" json:"args"` + }{ + *new(principal.Principal), + *new(string), + *new([]byte), + } + if _, err := a.WalletCallWithMaxCycles(a0); err != nil { + t.Fatal(err) + } + +} + // Test_WalletCreateCanister tests the "wallet_create_canister" method on the "wallet" canister. func Test_WalletCreateCanister(t *testing.T) { a, err := newAgent([]mock.Method{ diff --git a/ic/wallet/types.mo b/ic/wallet/types.mo index 3a6be74..5dc71d9 100755 --- a/ic/wallet/types.mo +++ b/ic/wallet/types.mo @@ -16,6 +16,7 @@ module T { public type WalletResultCreate = { #Ok : { canister_id : Principal }; #Err : Text }; public type WalletResult = { #Ok : (); #Err : Text }; public type WalletResultCall = { #Ok : { return : Blob }; #Err : Text }; + public type WalletResultCallWithMaxCycles = { #Ok : { return : Blob; attached_cycles : Nat }; #Err : Text }; public type CanisterSettings = { controller : ?Principal; controllers : ?[Principal]; compute_allocation : ?Nat; memory_allocation : ?Nat; freezing_threshold : ?Nat }; public type CreateCanisterArgs = { cycles : Nat64; settings : T.CanisterSettings }; public type CreateCanisterArgs128 = { cycles : Nat; settings : T.CanisterSettings }; diff --git a/pocketic/agent_test.go b/pocketic/agent_test.go index 3a6a207..3e9d0f3 100755 --- a/pocketic/agent_test.go +++ b/pocketic/agent_test.go @@ -1,4 +1,4 @@ -// Package pocketic provides a client for the "hello" canister. +// Package pocketic_test provides a client for the "hello" canister. // Do NOT edit this file. It was automatically generated by https://github.com/aviate-labs/agent-go. package pocketic_test diff --git a/pocketic/gen.go b/pocketic/gen.go new file mode 100644 index 0000000..0030c0e --- /dev/null +++ b/pocketic/gen.go @@ -0,0 +1,3 @@ +package pocketic + +//go:generate go run ../cmd/goic/main.go generate did testdata/main.did hello --output=agent_test.go --packageName=pocketic_test diff --git a/pocketic/http.go b/pocketic/http.go new file mode 100644 index 0000000..5d62c47 --- /dev/null +++ b/pocketic/http.go @@ -0,0 +1,131 @@ +package pocketic + +import ( + "encoding/json" + "fmt" + "net/http" + "time" +) + +type CreateHttpGatewayResponse struct { + Created *HttpGatewayInfo `json:"Created,omitempty"` + Error *ErrorMessage `json:"Error,omitempty"` +} + +type ErrorMessage struct { + Message string `json:"message"` +} + +func (e ErrorMessage) Error() string { + return e.Message +} + +type HttpGatewayBackend interface { + httpGatewayBackend() +} + +type HttpGatewayBackendPocketICInstance struct { + PocketIcInstance int `json:"PocketIcInstance"` +} + +func (HttpGatewayBackendPocketICInstance) httpGatewayBackend() {} + +type HttpGatewayBackendReplica struct { + Replica string `json:"Replica"` +} + +func (HttpGatewayBackendReplica) httpGatewayBackend() {} + +type HttpGatewayConfig struct { + ListenAt *int `json:"listen_at,omitempty"` + ForwardTo HttpGatewayBackend `json:"forward_to"` +} + +func (h *HttpGatewayConfig) UnmarshalJSON(bytes []byte) error { + var raw struct { + ListenAt *int `json:"listen_at,omitempty"` + ForwardTo json.RawMessage `json:"forward_to"` + } + if err := json.Unmarshal(bytes, &raw); err != nil { + return err + } + h.ListenAt = raw.ListenAt + + var pocketIC HttpGatewayBackendPocketICInstance + if err := json.Unmarshal(bytes, &pocketIC); err == nil { + h.ForwardTo = pocketIC + return nil + } + var replica HttpGatewayBackendReplica + if err := json.Unmarshal(bytes, &replica); err == nil { + h.ForwardTo = replica + return nil + } + return fmt.Errorf("unknown HttpGatewayBackend type") +} + +type HttpGatewayInfo struct { + InstanceID int `json:"instance_id"` + Port int `json:"port"` +} + +// AutoProgress configures the IC to make progress automatically, i.e., periodically update the time of the IC to the +// real time and execute rounds on the subnets. Returns the URL at which `/api/v2` requests for this instance can be made. +func (pic PocketIC) AutoProgress() error { + now := time.Now() + if err := pic.SetTime(now); err != nil { + return err + } + return pic.do( + http.MethodPost, + fmt.Sprintf("%s/auto_progress", pic.instanceURL()), + http.StatusOK, + nil, + nil, + ) +} + +// MakeLive creates an HTTP gateway for this IC instance listening on an optionally specified port and configures the IC +// instance to make progress automatically, i.e., periodically update the time of the IC to the real time and execute +// rounds on the subnets. Returns the URL at which `/api/v2` requests for this instance can be made. +func (pic PocketIC) MakeLive(port *int) (string, error) { + if err := pic.AutoProgress(); err != nil { + return "", err + } + // Gateway already running. + if pic.httpGateway != nil { + return fmt.Sprintf("http://127.0.0.1:%d", pic.httpGateway.Port), nil + } + var resp CreateHttpGatewayResponse + if err := pic.do( + http.MethodPost, + fmt.Sprintf("http://127.0.0.1:%d/http_gateway", pic.server.port), + http.StatusCreated, + HttpGatewayConfig{ + ListenAt: port, + ForwardTo: HttpGatewayBackendPocketICInstance{ + PocketIcInstance: pic.InstanceID, + }, + }, + &resp, + ); err != nil { + return "", err + } + if resp.Error != nil { + return "", resp.Error + } + return fmt.Sprintf("http://127.0.0.1:%d", resp.Created.Port), nil +} + +// SetTime sets the current time of the IC, on all subnets. +func (pic PocketIC) SetTime(time time.Time) error { + return pic.do( + http.MethodPost, + fmt.Sprintf("%s/update/set_time", pic.instanceURL()), + http.StatusOK, + RawTime{ + NanosSinceEpoch: int(time.UnixNano()), + }, + nil, + ) +} diff --git a/pocketic/management.go b/pocketic/management.go index 10e18df..3bf5f6f 100644 --- a/pocketic/management.go +++ b/pocketic/management.go @@ -17,6 +17,7 @@ func (pic PocketIC) AddCycles(canisterID principal.Principal, amount int) (int, if err := pic.do( http.MethodPost, fmt.Sprintf("%s/update/add_cycles", pic.instanceURL()), + http.StatusOK, RawAddCycles{ Amount: amount, CanisterID: canisterID.Raw, diff --git a/pocketic/pocketic.go b/pocketic/pocketic.go index e76c015..0764a46 100644 --- a/pocketic/pocketic.go +++ b/pocketic/pocketic.go @@ -61,13 +61,6 @@ type Config struct { logger agent.Logger } -// WithTTL sets the time-to-live for the PocketIC server, in seconds. -func WithTTL(ttl int) Option { - return func(p *Config) { - p.serverConfig = append(p.serverConfig, withTTL(ttl)) - } -} - type DTSFlag bool func (f DTSFlag) MarshalJSON() ([]byte, error) { @@ -178,10 +171,17 @@ func WithSystemSubnet() Option { } } +// WithTTL sets the time-to-live for the PocketIC server, in seconds. +func WithTTL(ttl int) Option { + return func(p *Config) { + p.serverConfig = append(p.serverConfig, withTTL(ttl)) + } +} + // PocketIC is a client for the local PocketIC server. type PocketIC struct { InstanceID int - httpGateway *httpGatewayInfo + httpGateway *HttpGatewayInfo topology map[string]Topology logger agent.Logger @@ -220,12 +220,10 @@ func New(opts ...Option) (*PocketIC, error) { InstanceID int `json:"instance_id"` Topology map[string]Topology `json:"topology"` } `json:"Created,omitempty"` - Error struct { - Message string `json:"message"` - } `json:"Error,omitempty"` + Error *ErrorMessage `json:"Error,omitempty"` } - if respBody.Error.Message != "" { - return nil, fmt.Errorf("failed to create instance: %s", respBody.Error.Message) + if respBody.Error != nil { + return nil, respBody.Error } if err := checkResponse(resp, http.StatusCreated, &respBody); err != nil { return nil, fmt.Errorf("failed to create instance: %v", err) @@ -246,6 +244,11 @@ func (pic *PocketIC) Close() error { return pic.server.Close() } +// Topology returns the topology of the PocketIC instance. +func (pic PocketIC) Topology() map[string]Topology { + return pic.topology +} + // instanceURL returns the URL of the PocketIC instance. func (pic PocketIC) instanceURL() string { return fmt.Sprintf("%s/instances/%d", pic.server.URL(), pic.InstanceID) @@ -423,8 +426,3 @@ type canisterIDRange struct { Start canisterID `json:"start"` End canisterID `json:"end"` } - -type httpGatewayInfo struct { - InstanceID int - Port int -} diff --git a/pocketic/pocketic_test.go b/pocketic/pocketic_test.go index 3e15cf8..98b2848 100644 --- a/pocketic/pocketic_test.go +++ b/pocketic/pocketic_test.go @@ -2,7 +2,17 @@ package pocketic_test import ( "fmt" + "github.com/aviate-labs/agent-go" + "github.com/aviate-labs/agent-go/candid/idl" + "github.com/aviate-labs/agent-go/ic" + ic0 "github.com/aviate-labs/agent-go/ic/ic" "github.com/aviate-labs/agent-go/pocketic" + "github.com/aviate-labs/agent-go/principal" + "net/url" + "os" + "os/exec" + "path" + "strings" "testing" ) @@ -27,6 +37,90 @@ func TestPocketIC_CreateCanister(t *testing.T) { } } +func TestPocketIC_MakeLive(t *testing.T) { + pic, err := pocketic.New( + pocketic.WithLogger(new(testLogger)), + pocketic.WithNNSSubnet(), + pocketic.WithApplicationSubnet(), + ) + if err != nil { + t.Fatal(err) + } + defer func() { + err := pic.Close() + if err != nil { + t.Fatal(err) + } + }() + + endpoint, err := pic.MakeLive(nil) + if err != nil { + t.Fatal(err) + } + host, err := url.Parse(endpoint) + if err != nil { + t.Fatal(err) + } + + mgmtAgent, err := ic0.NewAgent(ic.MANAGEMENT_CANISTER_PRINCIPAL, agent.Config{ + ClientConfig: &agent.ClientConfig{Host: host}, + FetchRootKey: true, + Logger: new(testLogger), + }) + if err != nil { + t.Fatal(err) + } + + var ecID principal.Principal + for _, t := range pic.Topology() { + if t.SubnetKind == pocketic.ApplicationSubnet { + ecID = t.CanisterRanges[0].Start + break + } + } + + var result ic0.ProvisionalCreateCanisterWithCyclesResult + createCall, err := mgmtAgent.ProvisionalCreateCanisterWithCyclesCall(ic0.ProvisionalCreateCanisterWithCyclesArgs{}) + if err != nil { + t.Fatal(err) + } + if err := createCall.WithEffectiveCanisterID(ecID).CallAndWait(&result); err != nil { + t.Fatal(err) + } + + compileMotoko(t, "testdata/main.mo", "testdata/main.wasm") + wasmModule, err := os.ReadFile("testdata/main.wasm") + if err != nil { + t.Fatal(err) + } + if err := mgmtAgent.InstallCode(ic0.InstallCodeArgs{ + Mode: ic0.CanisterInstallMode{ + Install: new(idl.Null), + }, + CanisterId: result.CanisterId, + WasmModule: wasmModule, + }); err != nil { + t.Fatal(err) + } +} + +func compileMotoko(t *testing.T, in, out string) { + dfxPath, err := exec.LookPath("dfx") + if err != nil { + t.Skipf("dfx not found: %v", err) + } + cmd := exec.Command(dfxPath, "cache", "show") + raw, err := cmd.CombinedOutput() + if err != nil { + t.Fatal(err) + } + mocPath := path.Join(strings.TrimSpace(string(raw)), "moc") + cmd = exec.Command(mocPath, in, "-o", out) + if err := cmd.Run(); err != nil { + t.Fatal(err) + } +} + type testLogger struct{} func (t testLogger) Printf(format string, v ...any) { diff --git a/pocketic/request.go b/pocketic/request.go index 38d657c..e14f101 100644 --- a/pocketic/request.go +++ b/pocketic/request.go @@ -18,6 +18,10 @@ func checkResponse(resp *http.Response, statusCode int, v any) error { if resp.StatusCode != statusCode { return fmt.Errorf("unexpected status code: %d", resp.StatusCode) } + if v == nil { + // No need to decode the response body. + return nil + } return json.NewDecoder(resp.Body).Decode(v) } @@ -44,6 +48,7 @@ func (pic PocketIC) AwaitCall(messageID RawMessageID) ([]byte, error) { if err := pic.do( http.MethodPost, fmt.Sprintf("%s/update/await_ingress_message", pic.instanceURL()), + http.StatusOK, messageID, &resp, ); err != nil { @@ -95,6 +100,7 @@ func (pic PocketIC) SubmitCallWithEP( if err := pic.do( http.MethodPost, fmt.Sprintf("%s/update/submit_ingress_message", pic.instanceURL()), + http.StatusOK, RawCanisterCall{ CanisterID: canisterID.Raw, EffectivePrincipal: effectivePrincipal, @@ -123,6 +129,7 @@ func (pic PocketIC) canisterCall(endpoint string, canisterID principal.Principal if err := pic.do( http.MethodPost, fmt.Sprintf("%s/%s", pic.instanceURL(), endpoint), + http.StatusOK, RawCanisterCall{ CanisterID: canisterID.Raw, EffectivePrincipal: effectivePrincipal, @@ -143,7 +150,7 @@ func (pic PocketIC) canisterCall(endpoint string, canisterID principal.Principal return resp.Ok.Reply, nil } -func (pic PocketIC) do(method, url string, input, output any) error { +func (pic PocketIC) do(method, url string, statusCode int, input, output any) error { pic.logger.Printf("[POCKETIC] %s %s %+v", method, url, input) req, err := newRequest(method, url, input) if err != nil { @@ -153,7 +160,7 @@ func (pic PocketIC) do(method, url string, input, output any) error { if err != nil { return err } - return checkResponse(resp, http.StatusOK, output) + return checkResponse(resp, statusCode, output) } // updateCallWithEP calls SubmitCallWithEP and AwaitCall in sequence. diff --git a/pocketic/server.go b/pocketic/server.go index c544c0b..9493e52 100644 --- a/pocketic/server.go +++ b/pocketic/server.go @@ -16,19 +16,6 @@ type server struct { cmd *exec.Cmd } -type serverConfig struct { - ttl *int -} - -type serverOption func(*serverConfig) - -// withTTL sets the time-to-live for the pocket-ic server, in seconds. -func withTTL(ttl int) serverOption { - return func(c *serverConfig) { - c.ttl = &ttl - } -} - func newServer(opts ...serverOption) (*server, error) { config := serverConfig{} for _, fn := range opts { @@ -111,3 +98,16 @@ func (s server) Close() error { func (s server) URL() string { return fmt.Sprintf("http://127.0.0.1:%d", s.port) } + +type serverConfig struct { + ttl *int +} + +type serverOption func(*serverConfig) + +// withTTL sets the time-to-live for the pocket-ic server, in seconds. +func withTTL(ttl int) serverOption { + return func(c *serverConfig) { + c.ttl = &ttl + } +}