diff --git a/api/blockopts.go b/api/blockopts.go index 13a832d..d4965a9 100644 --- a/api/blockopts.go +++ b/api/blockopts.go @@ -14,11 +14,7 @@ package api import ( - "encoding/json" - "errors" - "fmt" - "strconv" - "strings" + "github.com/attestantio/go-starknet-client/types" ) // BlockOpts are the options for blocks. @@ -26,39 +22,5 @@ type BlockOpts struct { Common CommonOpts // Block is the ID of the block. - // It can be a block number, block hash, or one of the special values "latest" or "pending". - Block string -} - -// MarshalJSON marshals to a JSON representation. -func (o *BlockOpts) MarshalJSON() ([]byte, error) { - opts := map[string]any{} - switch { - case o.Block == "latest" || o.Block == "pending": - opts["block_id"] = o.Block - case strings.HasPrefix(o.Block, "0x"): - opts["block_id"] = map[string]any{ - "block_hash": o.Block, - } - default: - block, err := strconv.ParseUint(o.Block, 10, 64) - if err != nil { - return nil, errors.New("invalid from block") - } - opts["block_id"] = map[string]any{ - "block_number": block, - } - } - - return json.Marshal(opts) -} - -// String returns a string version of the structure. -func (o *BlockOpts) String() string { - data, err := o.MarshalJSON() - if err != nil { - return fmt.Sprintf("ERR: %v", err) - } - - return string(data) + Block types.BlockID } diff --git a/api/callopts.go b/api/callopts.go index 7f90f24..bf4634a 100644 --- a/api/callopts.go +++ b/api/callopts.go @@ -21,7 +21,7 @@ type CallOpts struct { // Block is the block for which the data is obtained. // It can be a block number, block hash, or one of the special values "latest" or "pending". - Block string + Block types.BlockID // Contact is the contract for which the data is obtained. Contract types.Address @@ -29,6 +29,6 @@ type CallOpts struct { // EntryPointSelector defines the entry point for the call. EntryPointSelector types.FieldElement - // CallData is the data passed to the call. - CallData types.FieldElement + // Calldata is the data passed to the call. + Calldata []types.FieldElement } diff --git a/api/eventsopts.go b/api/eventsopts.go index 4e469b8..0b5f417 100644 --- a/api/eventsopts.go +++ b/api/eventsopts.go @@ -15,10 +15,7 @@ package api import ( "encoding/json" - "errors" "fmt" - "strconv" - "strings" "github.com/attestantio/go-starknet-client/types" ) @@ -28,12 +25,11 @@ type EventsOpts struct { Common CommonOpts // FromBlock is the earliest block from which the events are obtained. - // It can be a block number, block hash, or one of the special values "latest" or "pending". - FromBlock string + FromBlock types.BlockID // ToBlock is the latest block from which the events are obtained. // It can be a block number, block hash, or one of the special values "latest" or "pending". - ToBlock string + ToBlock types.BlockID // Address is the contract address from which the events are obtained. // If empty then events there is no address filter on returned events. @@ -51,41 +47,10 @@ type EventsOpts struct { // MarshalJSON marshals to a JSON representation. func (o *EventsOpts) MarshalJSON() ([]byte, error) { filter := map[string]any{ - "to_block": o.FromBlock, + "from_block": o.FromBlock, + "to_block": o.ToBlock, "chunk_size": o.Limit, } - switch { - case o.FromBlock == "latest" || o.FromBlock == "pending": - filter["from_block"] = o.FromBlock - case strings.HasPrefix(o.FromBlock, "0x"): - filter["from_block"] = map[string]any{ - "block_hash": o.FromBlock, - } - default: - fromBlock, err := strconv.ParseUint(o.FromBlock, 10, 64) - if err != nil { - return nil, errors.New("invalid from block") - } - filter["from_block"] = map[string]any{ - "block_number": fromBlock, - } - } - switch { - case o.ToBlock == "latest" || o.ToBlock == "pending": - filter["to_block"] = o.ToBlock - case strings.HasPrefix(o.ToBlock, "0x"): - filter["to_block"] = map[string]any{ - "block_hash": o.ToBlock, - } - default: - toBlock, err := strconv.ParseUint(o.ToBlock, 10, 64) - if err != nil { - return nil, errors.New("invalid to block") - } - filter["to_block"] = map[string]any{ - "block_number": toBlock, - } - } if o.Address != nil { filter["address"] = o.Address.String() } diff --git a/api/nonceopts.go b/api/nonceopts.go index 748c712..aa8534e 100644 --- a/api/nonceopts.go +++ b/api/nonceopts.go @@ -20,8 +20,7 @@ type NonceOpts struct { Common CommonOpts // Block is the block for which the data is obtained. - // It can be a block number, block hash, or one of the special values "latest" or "pending". - Block string + Block types.BlockID // Contact is the contract for which the data is obtained. Contract types.Address diff --git a/go.mod b/go.mod index 0489dec..6d70103 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/attestantio/go-starknet-client -go 1.22 +go 1.23 require ( github.com/prometheus/client_golang v1.20.4 diff --git a/jsonrpc/block.go b/jsonrpc/block.go index 3601e96..6b50c51 100644 --- a/jsonrpc/block.go +++ b/jsonrpc/block.go @@ -40,8 +40,12 @@ func (s *Service) Block(ctx context.Context, return nil, errors.Join(errors.New("no block specified"), client.ErrInvalidOptions) } + blockOpts := map[string]any{ + "block_id": opts.Block, + } + var res spec.Block - err := s.client.CallFor(&res, "starknet_getBlockWithReceipts", opts) + err := s.client.CallFor(&res, "starknet_getBlockWithReceipts", blockOpts) if err != nil { return nil, errors.Join(errors.New("starknet_getBlockWithReceipts failed"), err) } diff --git a/jsonrpc/call.go b/jsonrpc/call.go index c37ac8b..c526042 100644 --- a/jsonrpc/call.go +++ b/jsonrpc/call.go @@ -16,7 +16,6 @@ package jsonrpc import ( "context" "errors" - "fmt" client "github.com/attestantio/go-starknet-client" "github.com/attestantio/go-starknet-client/api" @@ -42,12 +41,15 @@ func (s *Service) Call(ctx context.Context, } callOpts := make(map[string]any) - request := make(map[string]string) + request := make(map[string]any) request["contract_address"] = opts.Contract.String() request["entry_point_selector"] = opts.EntryPointSelector.String() - if len(opts.CallData) > 0 { - request["call_data"] = fmt.Sprintf("%#x", opts.CallData) + + calldata := make([]string, 0, len(opts.Calldata)) + for i := range opts.Calldata { + calldata = append(calldata, opts.Calldata[i].String()) } + request["calldata"] = calldata callOpts["request"] = request callOpts["block_id"] = opts.Block diff --git a/jsonrpc/call_test.go b/jsonrpc/call_test.go index 6800567..997b405 100644 --- a/jsonrpc/call_test.go +++ b/jsonrpc/call_test.go @@ -50,19 +50,43 @@ func TestCall(t *testing.T) { { name: "MultipleReturnElements", opts: &api.CallOpts{ - Block: "latest", + Block: "273028", Contract: strToAddress("0x0590e76a2e65435b7288bf3526cfa5c3ec7748d2f3433a934c931cce62460fc5"), EntryPointSelector: strToFieldElement("0x36735aa694184cd8116c479c296d9431cc04a470e0467c07067e4586f647ece"), }, expected: []types.FieldElement{ strToFieldElement("0xde0b6b3a7640000"), strToFieldElement("0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d"), - strToFieldElement("0x1f777aef22"), + strToFieldElement("0x2264e064ae"), strToFieldElement("0x3660fc6334c9485065394f6432933c2f04b4716c67511ba174384c65faebc19"), strToFieldElement("0x52bf6ec001452dbace0ca4c7db1b232b8031b5a0ccb117bb47a05569df435de"), strToFieldElement("0x12c"), }, }, + { + name: "CallData", + opts: &api.CallOpts{ + Block: "272995", + Contract: strToAddress("0x1d3ec552f7875a008783a1a2706c387ef96067469534458dbfb41003ebecb4f"), + EntryPointSelector: strToFieldElement("0x1af63b1cdc061704af767c81df4fd9eb5a3d989868ff970dd79d5431a89db44"), + Calldata: []types.FieldElement{ + strToFieldElement("0x714792a41f3651e171c46c93fc53adeb922a414a891cc36d73029d23e99a6ec"), + }, + }, + expected: []types.FieldElement{ + strToFieldElement("0x391d69afc1b49f01ad8d2e0e8a03756b694dd056fb6645781eb00f33dbd8caf"), + strToFieldElement("0x4a7876e03402cf253efdb3b17c760ee7349c7ec2876059b83ec2c92ca451e16"), + strToFieldElement("0x1"), + strToFieldElement("0xde0b6b3a7640000"), + strToFieldElement("0x2264e064ae"), + strToFieldElement("0x2f4d1ee7e060"), + strToFieldElement("0x0"), + strToFieldElement("0xca4ef197ec48699c2d4c9f9802770fd08743256759f6f0e500c92e2d397cd7"), + strToFieldElement("0x4563918244f40000"), + strToFieldElement("0x0"), + strToFieldElement("0x9c4"), + }, + }, } s, err := jsonrpc.New(ctx, @@ -86,7 +110,7 @@ func TestCall(t *testing.T) { default: require.NoError(t, err) require.NotNil(t, response) - require.NotNil(t, test.expected, response.Data) + require.Equal(t, test.expected, response.Data) } }) } diff --git a/jsonrpc/events_test.go b/jsonrpc/events_test.go index e6ce148..dc0099b 100644 --- a/jsonrpc/events_test.go +++ b/jsonrpc/events_test.go @@ -37,7 +37,7 @@ func TestEvents(t *testing.T) { response, err := s.Events(ctx, &api.EventsOpts{ FromBlock: "1", - ToBlock: "6", + ToBlock: "latest", Limit: 10, }) require.NoError(t, err) diff --git a/jsonrpc/nonce.go b/jsonrpc/nonce.go index 54ebcb7..c930b5b 100644 --- a/jsonrpc/nonce.go +++ b/jsonrpc/nonce.go @@ -40,8 +40,12 @@ func (s *Service) Nonce(ctx context.Context, return nil, errors.Join(errors.New("no block specified"), client.ErrInvalidOptions) } + nonceOpts := map[string]any{ + "block_id": opts.Block, + "contract_address": opts.Contract, + } var res types.Number - err := s.client.CallFor(&res, "starknet_getNonce", opts.Block, opts.Contract.String()) + err := s.client.CallFor(&res, "starknet_getNonce", nonceOpts) if err != nil { return nil, errors.Join(err, client.ErrRPCCallFailed) } diff --git a/types/blockid.go b/types/blockid.go new file mode 100644 index 0000000..3ca4369 --- /dev/null +++ b/types/blockid.go @@ -0,0 +1,65 @@ +// Copyright © 2024 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "errors" + "fmt" + "strconv" + "strings" +) + +// BlockID is a block identifier. +// It can be a block number, block hash, or one of the special values "latest" or "pending". +type BlockID string + +// String returns the string representation of the block ID. +func (b BlockID) String() string { + return string(b) +} + +// Format formats the address. +func (b BlockID) Format(state fmt.State, v rune) { + format := string(v) + switch v { + case 'q': + fmt.Fprint(state, `"`+string(b)+`"`) + case 's': + fmt.Fprint(state, string(b)) + case 'x', 'X': + if state.Flag('#') { + format = "#" + format + } + fmt.Fprintf(state, "%"+format, b[:]) + default: + fmt.Fprintf(state, "%"+format, b[:]) + } +} + +// MarshalJSON implements json.Marshaler. +func (b BlockID) MarshalJSON() ([]byte, error) { + switch { + case b == "latest" || b == "pending": + return []byte(fmt.Sprintf("%q", b)), nil + case strings.HasPrefix(string(b), "0x"): + return []byte(fmt.Sprintf(`{"block_hash":"%s"}`, b)), nil + default: + block, err := strconv.ParseUint(string(b), 10, 64) + if err != nil { + return nil, errors.New("invalid from block") + } + + return []byte(fmt.Sprintf(`{"block_number":%d}`, block)), nil + } +}