Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions account/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func (account *Account) BuildAndSendInvokeTxn(
return response, err
}
txnFee := estimateFee[0]
broadcastInvokeTxnV3.ResourceBounds = utils.FeeEstToResBoundsMap(txnFee, opts.Multiplier)
broadcastInvokeTxnV3.ResourceBounds = utils.FeeEstToResBoundsMap(txnFee, opts.Multiplier, nil)

// assuring the signed txn version will be rpc.TransactionV3, since queryBit
// txn version is only used for estimation/simulation
Expand Down Expand Up @@ -161,7 +161,7 @@ func (account *Account) BuildAndSendDeclareTxn(
return response, err
}
txnFee := estimateFee[0]
broadcastDeclareTxnV3.ResourceBounds = utils.FeeEstToResBoundsMap(txnFee, opts.Multiplier)
broadcastDeclareTxnV3.ResourceBounds = utils.FeeEstToResBoundsMap(txnFee, opts.Multiplier, nil)

// assuring the signed txn version will be rpc.TransactionV3, since queryBit
// txn version is only used for estimation/simulation
Expand Down Expand Up @@ -248,7 +248,7 @@ func (account *Account) BuildAndEstimateDeployAccountTxn(
return nil, nil, err
}
txnFee := estimateFee[0]
broadcastDepAccTxnV3.ResourceBounds = utils.FeeEstToResBoundsMap(txnFee, opts.Multiplier)
broadcastDepAccTxnV3.ResourceBounds = utils.FeeEstToResBoundsMap(txnFee, opts.Multiplier, nil)

// assuring the signed txn version will be rpc.TransactionV3, since queryBit
// txn version is only used for estimation/simulation
Expand Down
2 changes: 1 addition & 1 deletion examples/invoke/verboseInvoke.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func verboseInvoke(
}

// assign the estimated fee to the transaction, multiplying the estimated fee by 1.5 for a better chance of success
InvokeTx.ResourceBounds = utils.FeeEstToResBoundsMap(feeRes[0], 1.5)
InvokeTx.ResourceBounds = utils.FeeEstToResBoundsMap(feeRes[0], 1.5, nil)

// As we changed the resource bounds, we need to sign the transaction again, since the resource bounds are part of the signature
err = accnt.SignInvokeTransaction(context.Background(), InvokeTx)
Expand Down
23 changes: 23 additions & 0 deletions rpc/types_contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"math/big"
"strconv"
"strings"

Expand All @@ -19,6 +20,7 @@ type NumAsHex string
type U64 string

// ToUint64 converts the U64 type to a uint64.
// If the value is greater than max uint64, returns an error.
func (u U64) ToUint64() (uint64, error) {
hexStr := strings.TrimPrefix(string(u), "0x")

Expand All @@ -33,6 +35,27 @@ func (u U64) ToUint64() (uint64, error) {
// 128 bit unsigned integers, represented by hex string of length at most 32
type U128 string

// ToBigInt converts the U128 type to a *big.Int.
// If the value is greater than max uint128, returns an error.
//
//nolint:mnd // 16 means hex base
func (u U128) ToBigInt() (*big.Int, error) {
hexStr := strings.TrimPrefix(string(u), "0x")

result, ok := new(big.Int).SetString(hexStr, 16)
if !ok {
return nil, fmt.Errorf("failed to parse hex string: %v", hexStr)
}

maxUint128, _ := new(big.Int).SetString("ffffffffffffffffffffffffffffffff", 16)

if result.Cmp(maxUint128) > 0 {
return nil, fmt.Errorf("value is greater than max uint128: %v", u)
}

return result, nil
}

type ClassOutput interface{}

var (
Expand Down
109 changes: 109 additions & 0 deletions rpc/types_contract_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package rpc_test

import (
"math"
"math/big"
"testing"

internalUtils "github.com/NethermindEth/starknet.go/internal/utils"
"github.com/NethermindEth/starknet.go/rpc"
"github.com/stretchr/testify/assert"
)

// TestU128_ToBigInt tests the ToBigInt method of the U128 type.
func TestU128_ToBigInt(t *testing.T) {
tests := []struct {
name string // description of this test case
u128 rpc.U128
want *big.Int
wantErr bool
}{
{
name: "within the range",
u128: "0xabcdef",
want: internalUtils.HexToBN("0xabcdef"),
},
{
name: "max uint128",
u128: "0xffffffffffffffffffffffffffffffff",
want: internalUtils.HexToBN("0xffffffffffffffffffffffffffffffff"),
},
{
name: "out of range",
u128: "0x100000000000000000000000000000000",
wantErr: true,
},
{
name: "invalid hex string",
u128: "56yrty45",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
u := tt.u128
got, gotErr := u.ToBigInt()
if gotErr != nil {
if !tt.wantErr {
t.Errorf("ToBigInt() failed: %v", gotErr)
}

return
}
if tt.wantErr {
t.Fatal("ToBigInt() succeeded unexpectedly")
}

assert.Equal(t, tt.want, got)
})
}
}

// TestU128_ToUint64 tests the ToUint64 method of the U128 type.
func TestU128_ToUint64(t *testing.T) {
tests := []struct {
name string // description of this test case
u64 rpc.U64
want uint64
wantErr bool
}{
{
name: "within the range",
u64: "0xabcdef",
want: 11259375,
},
{
name: "max uint64",
u64: "0xFFFFFFFFFFFFFFFF",
want: math.MaxUint64,
},
{
name: "out of range",
u64: "0x10000000000000000",
wantErr: true,
},
{
name: "invalid hex string",
u64: "56yrty45",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
u := tt.u64
got, gotErr := u.ToUint64()
if gotErr != nil {
if !tt.wantErr {
t.Errorf("ToUint64() failed: %v", gotErr)
}

return
}
if tt.wantErr {
t.Fatal("ToUint64() succeeded unexpectedly")
}

assert.Equal(t, tt.want, got)
})
}
}
2 changes: 1 addition & 1 deletion rpc/websocket_external_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func TestSubscribeTransactionStatus(t *testing.T) {
require.NoError(t, err, "Error estimating fee")

txnFee := estimateFee[0]
invokeTx.ResourceBounds = utils.FeeEstToResBoundsMap(txnFee, 1.5)
invokeTx.ResourceBounds = utils.FeeEstToResBoundsMap(txnFee, 1.5, nil)

// sign the txn
err = acc.SignInvokeTransaction(context.Background(), invokeTx)
Expand Down
107 changes: 81 additions & 26 deletions utils/transactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import (
)

const (
// Ref: https://docs.starknet.io/learn/cheatsheets/chain-info#current-limits
maxL2GasAmount uint64 = 1_000_000_000 // 10^9

maxUint64 uint64 = math.MaxUint64
maxUint128 = "0xffffffffffffffffffffffffffffffff"

Expand All @@ -24,7 +27,6 @@ const (
// Optional settings when building a transaction.
type TxnOptions struct {
// Tip amount in FRI for the transaction. Default: `"0x0"`.
// Note: only ready to be used after Starknet v0.14.0 upgrade.
Tip rpc.U64
// A boolean flag indicating whether the transaction version should have
// the query bit when estimating fees. If true, the transaction version
Expand Down Expand Up @@ -214,29 +216,77 @@ func InvokeFuncCallsToFunctionCalls(invokeFuncCalls []rpc.InvokeFunctionCall) []
return functionCalls
}

// FeeLimitOpts is a struct with custom limits for the fee values, used
// as a parameter for some utility functions.
// The corresponding resource bounds will be capped to the specified values
// to avoid overflows.
type FeeLimitOpts struct {
// Custom max value for L1 gas price
L1GasPriceLimit rpc.U128
// Custom max value for L1 gas amount
L1GasAmountLimit rpc.U64

// Custom max value for L2 gas price
L2GasPriceLimit rpc.U128
// Custom max value for L2 gas amount
L2GasAmountLimit rpc.U64

// Custom max value for L1 data gas price
L1DataGasPriceLimit rpc.U128
// Custom max value for L1 data gas amount
L1DataGasAmountLimit rpc.U64
}

// FeeEstToResBoundsMap converts a FeeEstimation to ResourceBoundsMapping with applied multipliers.
// Parameters:
// - feeEstimation: The fee estimation to convert
// - multiplier: Multiplier for max amount and max price per unit. Recommended to be 1.5,
// but at least greater than 0.
// If multiplier < 0, all resources bounds will be set to 0.
// If multiplier <= 0, all resources bounds will be set to 0.
// If resource bounds overflow, they will be set to the max allowed value (U64 or U128).
// - limitOpts: Optional custom limits for the resource bounds. If nil, default
// values will be used.
//
// Returns:
// - rpc.ResourceBoundsMapping: Resource bounds with applied multipliers
func FeeEstToResBoundsMap(
feeEstimation rpc.FeeEstimation,
multiplier float64,
limitOpts *FeeLimitOpts,
) *rpc.ResourceBoundsMapping {
if limitOpts == nil {
limitOpts = new(FeeLimitOpts)
}

// Create L1 resources bounds
l1Gas := toResourceBounds(feeEstimation.L1GasPrice, feeEstimation.L1GasConsumed, multiplier)
l1Gas := toResourceBounds(
feeEstimation.L1GasPrice,
limitOpts.L1GasPriceLimit,
feeEstimation.L1GasConsumed,
limitOpts.L1GasAmountLimit,
multiplier,
)
l1DataGas := toResourceBounds(
feeEstimation.L1DataGasPrice,
limitOpts.L1DataGasPriceLimit,
feeEstimation.L1DataGasConsumed,
limitOpts.L1DataGasAmountLimit,
multiplier,
)

// Create L2 resource bounds
l2Gas := toResourceBounds(feeEstimation.L2GasPrice, feeEstimation.L2GasConsumed, multiplier)
// If the L2 gas amount limit is not set, use the default limit
// defined by Starknet.
if limitOpts.L2GasAmountLimit == "" {
limitOpts.L2GasAmountLimit = rpc.U64(fmt.Sprintf("%#x", maxL2GasAmount))
}
l2Gas := toResourceBounds(
feeEstimation.L2GasPrice,
limitOpts.L2GasPriceLimit,
feeEstimation.L2GasConsumed,
limitOpts.L2GasAmountLimit,
multiplier,
)

return &rpc.ResourceBoundsMapping{
L1Gas: l1Gas,
Expand All @@ -250,18 +300,24 @@ func FeeEstToResBoundsMap(
//
// Parameters:
// - gasPrice: The gas price
// - gasPriceLimit: The limit for the gas price. If invalid, a default value
// will be used.
// - gasConsumed: The gas consumed
// - gasAmountLimit: The limit for the gas amount. If invalid, a default value
// will be used.
// - multiplier: Multiplier for max amount and max price per unit
//
// Returns:
// - rpc.ResourceBounds: Resource bounds with applied multiplier
func toResourceBounds(
gasPrice *felt.Felt,
gasPriceLimit rpc.U128,
gasConsumed *felt.Felt,
gasAmountLimit rpc.U64,
multiplier float64,
) rpc.ResourceBounds {
// negative multiplier is not allowed, default to 0
if multiplier < 0 {
// multiplier must be greater than 0. Default to 0 if not
if multiplier <= 0 {
return rpc.ResourceBounds{
MaxAmount: rpc.U64("0x0"),
MaxPricePerUnit: rpc.U128("0x0"),
Expand All @@ -272,34 +328,33 @@ func toResourceBounds(
gasPriceInt := gasPrice.BigInt(new(big.Int))
gasConsumedInt := gasConsumed.BigInt(new(big.Int))

// Check for overflow
maxUint64 := new(big.Int).SetUint64(maxUint64)
maxUint128, _ := new(big.Int).SetString(maxUint128, 0)
// max_price_per_unit is U128 by the spec
if gasPriceInt.Cmp(maxUint128) > 0 {
gasPriceInt = maxUint128
}
// max_amount is U64 by the spec
if gasConsumedInt.Cmp(maxUint64) > 0 {
gasConsumedInt = maxUint64
}

// multiply values by the multiplier
maxAmount := new(big.Float)
maxPricePerUnit := new(big.Float)

maxAmount.Mul(new(big.Float).SetInt(gasConsumedInt), big.NewFloat(multiplier))
maxPricePerUnit.Mul(new(big.Float).SetInt(gasPriceInt), big.NewFloat(multiplier))

// Convert big.Float to big.Int for proper hex formatting. The result is a truncated int
maxAmountInt, _ := maxAmount.Int(new(big.Int))
maxPricePerUnitInt, _ := maxPricePerUnit.Int(new(big.Int))

// Check for overflow after mul operation
if maxAmountInt.Cmp(maxUint64) > 0 {
maxAmountInt = maxUint64
// Get the limits OR set default values if invalid
gasPL, err := gasPriceLimit.ToBigInt()
if err != nil {
gasPL = internalUtils.HexToBN(maxUint128)
}
tempGasAL, err := gasAmountLimit.ToUint64()
if err != nil {
tempGasAL = maxUint64
}
gasAL := new(big.Int).SetUint64(tempGasAL)

// Check for overflow comparing with the limits
if maxAmountInt.Cmp(gasAL) > 0 {
maxAmountInt = gasAL
}
if maxPricePerUnitInt.Cmp(maxUint128) > 0 {
maxPricePerUnitInt = maxUint128
if maxPricePerUnitInt.Cmp(gasPL) > 0 {
maxPricePerUnitInt = gasPL
}

return rpc.ResourceBounds{
Expand Down Expand Up @@ -327,8 +382,8 @@ func ResBoundsMapToOverallFee(
}

// negative multiplier is not allowed
if multiplier < 0 {
return nil, errors.New("multiplier cannot be negative")
if multiplier <= 0 {
return nil, errors.New("multiplier must be greater than 0")
}

parseBound := func(value string) (*big.Int, error) {
Expand Down
Loading