diff --git a/pkg/oneinch/auction/calculator.go b/pkg/oneinch/auction/calculator.go index cb0240f..0d612cb 100644 --- a/pkg/oneinch/auction/calculator.go +++ b/pkg/oneinch/auction/calculator.go @@ -4,7 +4,6 @@ import ( "math/big" "github.com/KyberNetwork/tradinglib/pkg/oneinch/fusionorder" - "github.com/KyberNetwork/tradinglib/pkg/oneinch/fusionutils" ) const ( @@ -25,36 +24,27 @@ type Calculator struct { } func NewCalculator( - startTime *big.Int, - duration *big.Int, - initialRateBump *big.Int, + startTime int64, + duration int64, + initialRateBump int64, points []fusionorder.AuctionPoint, - takerFeeRatio *big.Int, + takerFeeRatio int64, gasCost fusionorder.AuctionGasCostInfo, ) Calculator { - if gasCost.GasBumpEstimate == nil { - gasCost.GasBumpEstimate = big.NewInt(0) - } - if gasCost.GasPriceEstimate == nil { - gasCost.GasPriceEstimate = big.NewInt(0) - } return Calculator{ - startTime: startTime, - duration: duration, - initialRateBump: initialRateBump, + startTime: big.NewInt(startTime), + duration: big.NewInt(duration), + initialRateBump: big.NewInt(initialRateBump), points: points, - takerFeeRatio: takerFeeRatio, + takerFeeRatio: big.NewInt(takerFeeRatio), gasCost: gasCost, } } func NewCalculatorFromAuctionData( - takerFeeRatio *big.Int, + takerFeeRatio int64, auctionDetails fusionorder.AuctionDetails, ) Calculator { - if takerFeeRatio == nil { - takerFeeRatio = big.NewInt(0) - } return NewCalculator( auctionDetails.StartTime, auctionDetails.Duration, @@ -82,23 +72,24 @@ func (c Calculator) CalcRateBump(time, blockBaseFee *big.Int) int64 { } func (c Calculator) getGasPriceBump(blockBaseFee *big.Int) *big.Int { - zeroBigInt := new(big.Int) + zeroBigInt := big.NewInt(0) if zeroBigInt.Cmp(blockBaseFee) == 0 { return zeroBigInt } - if zeroBigInt.Cmp(c.gasCost.GasPriceEstimate) == 0 { + if c.gasCost.GasPriceEstimate == 0 { return zeroBigInt } - if zeroBigInt.Cmp(c.gasCost.GasBumpEstimate) == 0 { + if c.gasCost.GasBumpEstimate == 0 { return zeroBigInt } return new(big.Int).Div( new(big.Int).Div( new(big.Int).Mul( - c.gasCost.GasBumpEstimate, blockBaseFee, + big.NewInt(c.gasCost.GasBumpEstimate), blockBaseFee, ), - c.gasCost.GasPriceEstimate), + big.NewInt(c.gasCost.GasPriceEstimate), + ), big.NewInt(GasPriceBase), ) } @@ -171,7 +162,7 @@ func calcAuctionTakingAmount(takingAmount *big.Int, rate int64, takerFeeRatio *b return auctionTakingAmount } - return fusionutils.AddRatioToAmount(auctionTakingAmount, takerFeeRatio) + return fusionorder.AddRatioToAmount(auctionTakingAmount, takerFeeRatio) } func CalcInitialRateBump(startAmount *big.Int, endAmount *big.Int) int64 { diff --git a/pkg/oneinch/auction/calculator_test.go b/pkg/oneinch/auction/calculator_test.go index 284b0a6..c4e0f36 100644 --- a/pkg/oneinch/auction/calculator_test.go +++ b/pkg/oneinch/auction/calculator_test.go @@ -8,7 +8,6 @@ import ( "github.com/KyberNetwork/tradinglib/pkg/convert" "github.com/KyberNetwork/tradinglib/pkg/oneinch/auction" "github.com/KyberNetwork/tradinglib/pkg/oneinch/fusionorder" - "github.com/KyberNetwork/tradinglib/pkg/oneinch/fusionutils" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -16,14 +15,14 @@ import ( func TestAuctionCalculator(t *testing.T) { t.Run("should be created successfully from suffix and salt", func(t *testing.T) { - auctionStartTime := big.NewInt(1708448252) + auctionStartTime := int64(1708448252) postInteraction, err := fusionorder.NewSettlementPostInteractionDataFromSettlementSuffixData( fusionorder.SettlementSuffixData{ IntegratorFee: fusionorder.IntegratorFee{ - Ratio: fusionutils.BpsToRatioFormat(1), + Ratio: fusionorder.BpsToRatioFormat(1).Int64(), Receiver: common.BigToAddress(big.NewInt(1)), }, - BankFee: big.NewInt(0), + BankFee: 0, ResolvingStartTime: auctionStartTime, Whitelist: []fusionorder.AuctionWhitelistItem{ { @@ -36,8 +35,8 @@ func TestAuctionCalculator(t *testing.T) { actionDetails, err := fusionorder.NewAuctionDetails( auctionStartTime, - big.NewInt(50_000), - big.NewInt(120), + 50_000, + 120, nil, fusionorder.AuctionGasCostInfo{}, ) @@ -48,7 +47,7 @@ func TestAuctionCalculator(t *testing.T) { takingAmount, ok := new(big.Int).SetString("1420000000", 10) require.True(t, ok) - rate := calculator.CalcRateBump(new(big.Int).Add(auctionStartTime, big.NewInt(60)), big.NewInt(0)) + rate := calculator.CalcRateBump(big.NewInt(auctionStartTime+60), big.NewInt(0)) auctionTakingAmount := calculator.CalcAuctionTakingAmount(takingAmount, rate) assert.Equal(t, int64(25000), rate) @@ -57,46 +56,46 @@ func TestAuctionCalculator(t *testing.T) { } func TestCalculator_GasBump(t *testing.T) { - now := big.NewInt(time.Now().Unix()) - duration := big.NewInt(1800) // 30 minutes + now := time.Now().Unix() + duration := int64(1800) // 30 minutes takingAmount := parseEther(t, 1) calculator := auction.NewCalculator( - new(big.Int).Sub(now, big.NewInt(60)), + now-60, duration, - big.NewInt(1000000), + 1000000, []fusionorder.AuctionPoint{ { Delay: 60, Coefficient: 500000, }, }, - big.NewInt(0), + 0, fusionorder.AuctionGasCostInfo{ - GasBumpEstimate: big.NewInt(10000), - GasPriceEstimate: big.NewInt(1000), + GasBumpEstimate: 10_000, + GasPriceEstimate: 1000, }, ) t.Run("0 gwei = no gas fee", func(t *testing.T) { - bump := calculator.CalcRateBump(now, big.NewInt(0)) + bump := calculator.CalcRateBump(big.NewInt(now), big.NewInt(0)) auctionTakingAmount := calculator.CalcAuctionTakingAmount(takingAmount, bump) assert.Zero(t, auctionTakingAmount.Cmp(parseEther(t, 1.05))) }) t.Run("0.1 gwei == 0.01% gas fee", func(t *testing.T) { - bump := calculator.CalcRateBump(now, parseUnits(t, 1, 8)) + bump := calculator.CalcRateBump(big.NewInt(now), parseUnits(t, 1, 8)) auctionTakingAmount := calculator.CalcAuctionTakingAmount(takingAmount, bump) assert.Zero(t, auctionTakingAmount.Cmp(parseEther(t, 1.0499))) }) t.Run("15 gwei == 1.5% gas fee", func(t *testing.T) { - bump := calculator.CalcRateBump(now, parseUnits(t, 15, 9)) + bump := calculator.CalcRateBump(big.NewInt(now), parseUnits(t, 15, 9)) auctionTakingAmount := calculator.CalcAuctionTakingAmount(takingAmount, bump) assert.Zero(t, auctionTakingAmount.Cmp(parseEther(t, 1.035))) }) t.Run("100 gwei == 10% gas fee", func(t *testing.T) { - bump := calculator.CalcRateBump(now, parseUnits(t, 100, 9)) + bump := calculator.CalcRateBump(big.NewInt(now), parseUnits(t, 100, 9)) auctionTakingAmount := calculator.CalcAuctionTakingAmount(takingAmount, bump) assert.Zero(t, auctionTakingAmount.Cmp(parseEther(t, 1))) }) diff --git a/pkg/oneinch/decode/bytes_iterator.go b/pkg/oneinch/decode/bytes_iterator.go new file mode 100644 index 0000000..3330bf6 --- /dev/null +++ b/pkg/oneinch/decode/bytes_iterator.go @@ -0,0 +1,80 @@ +package decode + +import ( + "encoding/binary" + "errors" +) + +var ErrOutOfData = errors.New("out of data") + +type BytesIterator struct { + data []byte +} + +func NewBytesIterator(data []byte) *BytesIterator { + return &BytesIterator{data: data} +} + +func (bi *BytesIterator) RemainingData() []byte { + return bi.data +} + +func (bi *BytesIterator) HasMore() bool { + return len(bi.data) > 0 +} + +func (bi *BytesIterator) NextBytes(length int) ([]byte, error) { + if len(bi.data) < length { + return nil, ErrOutOfData + } + + result := bi.data[:length] + bi.data = bi.data[length:] + + return result, nil +} + +func (bi *BytesIterator) NextUint8() (uint8, error) { + result, err := bi.NextBytes(1) // nolint: gomnd + if err != nil { + return 0, err + } + + return result[0], nil +} + +func (bi *BytesIterator) NextUint16() (uint16, error) { + result, err := bi.NextBytes(2) // nolint: gomnd + if err != nil { + return 0, err + } + + return binary.BigEndian.Uint16(result), nil +} + +func (bi *BytesIterator) NextUint24() (uint32, error) { + result, err := bi.NextBytes(3) // nolint: gomnd + if err != nil { + return 0, err + } + + return binary.BigEndian.Uint32(append([]byte{0}, result...)), nil +} + +func (bi *BytesIterator) NextUint32() (uint32, error) { + result, err := bi.NextBytes(4) // nolint: gomnd + if err != nil { + return 0, err + } + + return binary.BigEndian.Uint32(result), nil +} + +func (bi *BytesIterator) NextUint64() (uint64, error) { + result, err := bi.NextBytes(8) // nolint: gomnd + if err != nil { + return 0, err + } + + return binary.BigEndian.Uint64(result), nil +} diff --git a/pkg/oneinch/fusionorder/address.go b/pkg/oneinch/fusionorder/address.go new file mode 100644 index 0000000..c3b85f7 --- /dev/null +++ b/pkg/oneinch/fusionorder/address.go @@ -0,0 +1,19 @@ +package fusionorder + +import "github.com/ethereum/go-ethereum/common" + +const ( + addressHalfLength = common.AddressLength / 2 +) + +type AddressHalf [addressHalfLength]byte + +func HalfAddressFromAddress(a common.Address) AddressHalf { + var addressHalf AddressHalf + copy(addressHalf[:], a.Bytes()[common.AddressLength-addressHalfLength:]) // take the last 10 bytes + return addressHalf +} + +func AddressFromFirstBytes(s []byte) common.Address { + return common.BytesToAddress(s[:common.AddressLength]) +} diff --git a/pkg/oneinch/fusionorder/auction_details.go b/pkg/oneinch/fusionorder/auction_details.go index da6f2bb..53bd378 100644 --- a/pkg/oneinch/fusionorder/auction_details.go +++ b/pkg/oneinch/fusionorder/auction_details.go @@ -1,9 +1,11 @@ package fusionorder import ( + "bytes" "errors" - "math/big" + "fmt" + "github.com/KyberNetwork/tradinglib/pkg/oneinch/decode" "github.com/ethereum/go-ethereum/common/math" ) @@ -12,49 +14,42 @@ const ( ) var ( - ErrGasBumpEstimateTooLarge = errors.New("gas bump estimate is too large") - ErrGasPriceEstimateTooLarge = errors.New("gas price estimate is too large") - ErrStartTimeTooLarge = errors.New("start time is too large") - ErrDurationTooLarge = errors.New("duration is too large") - ErrInitialRateBumpTooLarge = errors.New("initial rate bump is too large") + ErrGasBumpEstimateInvalid = errors.New("gas bump estimate is invalid") + ErrGasPriceEstimateInvalid = errors.New("gas price estimate is invalid") + ErrStartTimeInvalid = errors.New("start time is invalid") + ErrDurationInvalid = errors.New("duration is invalid") + ErrInitialRateBumpInvalid = errors.New("initial rate bump is invalid") ) type AuctionDetails struct { - StartTime *big.Int - Duration *big.Int - InitialRateBump *big.Int + StartTime int64 + Duration int64 + InitialRateBump int64 Points []AuctionPoint GasCost AuctionGasCostInfo } func NewAuctionDetails( - startTime *big.Int, - initialRateBump *big.Int, - duration *big.Int, + startTime int64, + initialRateBump int64, + duration int64, points []AuctionPoint, gasCost AuctionGasCostInfo, ) (AuctionDetails, error) { - if gasCost.GasPriceEstimate == nil { - gasCost.GasPriceEstimate = big.NewInt(0) + if gasCost.GasBumpEstimate > MaxUint24 || gasCost.GasBumpEstimate < 0 { + return AuctionDetails{}, ErrGasBumpEstimateInvalid } - if gasCost.GasBumpEstimate == nil { - gasCost.GasBumpEstimate = big.NewInt(0) + if gasCost.GasPriceEstimate > math.MaxUint32 || gasCost.GasPriceEstimate < 0 { + return AuctionDetails{}, ErrGasPriceEstimateInvalid } - - if gasCost.GasBumpEstimate.Cmp(big.NewInt(MaxUint24)) > 0 { // gasCost.GasBumpEstimate > MaxUint24 - return AuctionDetails{}, ErrGasBumpEstimateTooLarge - } - if gasCost.GasPriceEstimate.Cmp(big.NewInt(math.MaxUint32)) > 0 { // gasCost.GasPriceEstimate > MaxUint32 - return AuctionDetails{}, ErrGasPriceEstimateTooLarge - } - if startTime.Cmp(big.NewInt(math.MaxUint32)) > 0 { // startTime > MaxUint32 - return AuctionDetails{}, ErrStartTimeTooLarge + if startTime > math.MaxUint32 || startTime <= 0 { + return AuctionDetails{}, ErrStartTimeInvalid } - if duration.Cmp(big.NewInt(MaxUint24)) > 0 { // duration > MaxUint24 - return AuctionDetails{}, ErrDurationTooLarge + if duration > MaxUint24 || duration <= 0 { + return AuctionDetails{}, ErrDurationInvalid } - if initialRateBump.Cmp(big.NewInt(MaxUint24)) > 0 { // initialRateBump > MaxUint24 - return AuctionDetails{}, ErrInitialRateBumpTooLarge + if initialRateBump > MaxUint24 || initialRateBump < 0 { + return AuctionDetails{}, ErrInitialRateBumpInvalid } return AuctionDetails{ @@ -72,6 +67,100 @@ type AuctionPoint struct { } type AuctionGasCostInfo struct { - GasBumpEstimate *big.Int - GasPriceEstimate *big.Int + GasBumpEstimate int64 + GasPriceEstimate int64 +} + +// DecodeAuctionDetails decodes auction details from hex string +// ``` +// +// struct AuctionDetails { +// bytes3 gasBumpEstimate; +// bytes4 gasPriceEstimate; +// bytes4 auctionStartTime; +// bytes3 auctionDuration; +// bytes3 initialRateBump; +// (bytes3,bytes2)[N] pointsAndTimeDeltas; +// } +// +// ``` +// Logic is copied from +// https://etherscan.io/address/0xfb2809a5314473e1165f6b58018e20ed8f07b840#code#F23#L140 +// nolint: gomnd +func DecodeAuctionDetails(data []byte) (AuctionDetails, error) { + bi := decode.NewBytesIterator(data) + + gasBumpEstimate, err := bi.NextUint24() + if err != nil { + return AuctionDetails{}, fmt.Errorf("next gas bump estimate: %w", err) + } + gasPriceEstimate, err := bi.NextUint32() + if err != nil { + return AuctionDetails{}, fmt.Errorf("next gas price estimate: %w", err) + } + startTime, err := bi.NextUint32() + if err != nil { + return AuctionDetails{}, fmt.Errorf("next start time: %w", err) + } + duration, err := bi.NextUint24() + if err != nil { + return AuctionDetails{}, fmt.Errorf("next duration: %w", err) + } + initialRateBump, err := bi.NextUint24() + if err != nil { + return AuctionDetails{}, fmt.Errorf("next initial rate bump: %w", err) + } + + points, err := decodeAuctionPoints(bi.RemainingData()) + if err != nil { + return AuctionDetails{}, fmt.Errorf("decode auction points: %w", err) + } + + return NewAuctionDetails( + int64(startTime), + int64(initialRateBump), + int64(duration), + points, + AuctionGasCostInfo{ + GasBumpEstimate: int64(gasBumpEstimate), + GasPriceEstimate: int64(gasPriceEstimate), + }, + ) +} + +func decodeAuctionPoints(data []byte) ([]AuctionPoint, error) { + bi := decode.NewBytesIterator(data) + points := make([]AuctionPoint, 0) + for bi.HasMore() { + coefficient, err := bi.NextUint24() + if err != nil { + return nil, fmt.Errorf("next coefficient: %w", err) + } + delay, err := bi.NextUint16() + if err != nil { + return nil, fmt.Errorf("next delay: %w", err) + } + points = append(points, AuctionPoint{ + Coefficient: int64(coefficient), + Delay: int64(delay), + }) + } + return points, nil +} + +// Encode encodes AuctionDetails to bytes +// nolint: gomnd +func (a AuctionDetails) Encode() []byte { + buf := new(bytes.Buffer) + buf.Write(encodeInt64ToBytes(a.GasCost.GasBumpEstimate, 3)) + buf.Write(encodeInt64ToBytes(a.GasCost.GasPriceEstimate, 4)) + buf.Write(encodeInt64ToBytes(a.StartTime, 4)) + buf.Write(encodeInt64ToBytes(a.Duration, 3)) + buf.Write(encodeInt64ToBytes(a.InitialRateBump, 3)) + for _, point := range a.Points { + buf.Write(encodeInt64ToBytes(point.Coefficient, 3)) + buf.Write(encodeInt64ToBytes(point.Delay, 2)) + } + + return buf.Bytes() } diff --git a/pkg/oneinch/fusionorder/auction_details_test.go b/pkg/oneinch/fusionorder/auction_details_test.go new file mode 100644 index 0000000..3e9811e --- /dev/null +++ b/pkg/oneinch/fusionorder/auction_details_test.go @@ -0,0 +1,119 @@ +package fusionorder_test + +import ( + "testing" + + "github.com/KyberNetwork/tradinglib/pkg/oneinch/decode" + "github.com/KyberNetwork/tradinglib/pkg/oneinch/fusionorder" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// nolint: lll,funlen +func TestAuctionDetail(t *testing.T) { + t.Run("should encode/decode", func(t *testing.T) { + auctionDetail, err := fusionorder.NewAuctionDetails( + 1_673_548_149, + 50_000, + 180, + []fusionorder.AuctionPoint{ + { + Delay: 10, + Coefficient: 10_000, + }, + { + Delay: 20, + Coefficient: 5_000, + }, + }, + fusionorder.AuctionGasCostInfo{}, + ) + require.NoError(t, err) + + encodedAuctionDetail := auctionDetail.Encode() + + decodedAuctionDetail, err := fusionorder.DecodeAuctionDetails(encodedAuctionDetail) + require.NoError(t, err) + + assert.Equal(t, auctionDetail, decodedAuctionDetail) + }) + + t.Run("decode", func(t *testing.T) { + makingAmountData, err := hexutil.Decode( + "0xfb2809a5314473e1165f6b58018e20ed8f07b84000f1b8000005e566bb30120000b401def800f1b800b4", + ) + require.NoError(t, err) + decodeAuctionDetails, err := fusionorder.DecodeAuctionDetails( + makingAmountData[common.AddressLength:], + ) + require.NoError(t, err) + + t.Logf("AuctionDetails: %+v", decodeAuctionDetails) + + expected := fusionorder.AuctionDetails{ + StartTime: 1723543570, + Duration: 180, + InitialRateBump: 122616, + Points: []fusionorder.AuctionPoint{ + { + Delay: 180, + Coefficient: 61880, + }, + }, + GasCost: fusionorder.AuctionGasCostInfo{ + GasBumpEstimate: 61880, + GasPriceEstimate: 1509, + }, + } + + assert.Equal(t, expected, decodeAuctionDetails) + }) + + // nolint: lll + t.Run("decode real data", func(t *testing.T) { + // This data is get from + // https://app.blocksec.com/explorer/tx/eth/0x73e317981af9c352f26bac125b1a6d3e1d31076b87c679a4f771b4a5c5a7f76f?line=4&debugLine=4 + extraData, err := hexutil.Decode("0x01e9f1000005d866bda8b40000b404fa0103d477003c01e9f10078") + require.NoError(t, err) + decodeAuctionDetails, err := fusionorder.DecodeAuctionDetails(extraData) + require.NoError(t, err) + + // those value is collected from + // https://app.blocksec.com/explorer/tx/eth/0x73e317981af9c352f26bac125b1a6d3e1d31076b87c679a4f771b4a5c5a7f76f?line=79&debugLine=79 + expectedStartTime := uint64(1_723_705_524) + expectedDuration := 1_723_705_704 - expectedStartTime + initialRateBump := 326_145 + // those value is collected from running decode function in fusion-sdk with this extraData + // https://github.com/1inch/fusion-sdk/blob/8721c62612b08cc7c0e01423a1bdd62594e7b8d0/src/fusion-order/auction-details/auction-details.ts#L76 + points := []fusionorder.AuctionPoint{ + { + Delay: 60, + Coefficient: 250_999, + }, + { + Delay: 120, + Coefficient: 125_425, + }, + } + gasBumpEstimate := 125_425 + gasPriceEstimate := 1496 + + assert.EqualValues(t, expectedStartTime, decodeAuctionDetails.StartTime) + assert.EqualValues(t, expectedDuration, decodeAuctionDetails.Duration) + assert.EqualValues(t, initialRateBump, decodeAuctionDetails.InitialRateBump) + assert.ElementsMatch(t, points, decodeAuctionDetails.Points) + assert.EqualValues(t, gasBumpEstimate, decodeAuctionDetails.GasCost.GasBumpEstimate) + assert.EqualValues(t, gasPriceEstimate, decodeAuctionDetails.GasCost.GasPriceEstimate) + }) + + t.Run("should return error when data invalid", func(t *testing.T) { + extraData, err := hexutil.Decode("0x01e9f1000005d866bda8b40000b404fa0103d4") + require.NoError(t, err) + + _, err = fusionorder.DecodeAuctionDetails(extraData) + + require.ErrorIs(t, err, decode.ErrOutOfData) + }) +} diff --git a/pkg/oneinch/fusionorder/encode.go b/pkg/oneinch/fusionorder/encode.go new file mode 100644 index 0000000..f2f3f2b --- /dev/null +++ b/pkg/oneinch/fusionorder/encode.go @@ -0,0 +1,11 @@ +package fusionorder + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common/math" +) + +func encodeInt64ToBytes(n int64, size int) []byte { + return math.PaddedBigBytes(big.NewInt(n), size) +} diff --git a/pkg/oneinch/fusionorder/fusion_extension.go b/pkg/oneinch/fusionorder/fusion_extension.go new file mode 100644 index 0000000..67bd193 --- /dev/null +++ b/pkg/oneinch/fusionorder/fusion_extension.go @@ -0,0 +1,58 @@ +package fusionorder + +import ( + "errors" + "fmt" + + "github.com/KyberNetwork/tradinglib/pkg/oneinch/limitorder" + "github.com/ethereum/go-ethereum/common" +) + +type FusionExtension struct { + Address common.Address + AuctionDetails AuctionDetails + PostInteractionData SettlementPostInteractionData + MakerPermit limitorder.Interaction +} + +var ErrSettlementContractMismatch = errors.New("settlement contract mismatch") + +func NewFusionExtensionFromExtension(extension limitorder.Extension) (FusionExtension, error) { + settlementContract := AddressFromFirstBytes(extension.MakingAmountData) + + if AddressFromFirstBytes(extension.TakingAmountData) != settlementContract { + return FusionExtension{}, + fmt.Errorf("taking amount data settlement contract mismatch: %w", ErrSettlementContractMismatch) + } + if AddressFromFirstBytes(extension.PostInteraction) != settlementContract { + return FusionExtension{}, + fmt.Errorf("post interaction settlement contract mismatch: %w", ErrSettlementContractMismatch) + } + + auctionDetails, err := DecodeAuctionDetails(extension.MakingAmountData[common.AddressLength:]) + if err != nil { + return FusionExtension{}, fmt.Errorf("decode auction details: %w", err) + } + + postInteractionData, err := DecodeSettlementPostInteractionData( + extension.PostInteraction[common.AddressLength:], + ) + if err != nil { + return FusionExtension{}, fmt.Errorf("decode post interaction data: %w", err) + } + + var makerPermit limitorder.Interaction + if extension.HasMakerPermit() { + makerPermit, err = limitorder.DecodeInteraction(extension.MakerPermit) + if err != nil { + return FusionExtension{}, fmt.Errorf("decode maker permit: %w", err) + } + } + + return FusionExtension{ + Address: settlementContract, + AuctionDetails: auctionDetails, + PostInteractionData: postInteractionData, + MakerPermit: makerPermit, + }, nil +} diff --git a/pkg/oneinch/fusionorder/settlement_post_interaction_data.go b/pkg/oneinch/fusionorder/settlement_post_interaction_data.go index 5a8ed07..d1180fc 100644 --- a/pkg/oneinch/fusionorder/settlement_post_interaction_data.go +++ b/pkg/oneinch/fusionorder/settlement_post_interaction_data.go @@ -3,21 +3,14 @@ package fusionorder import ( "errors" "math" - "math/big" "github.com/ethereum/go-ethereum/common" "golang.org/x/exp/slices" ) -const ( - addressHalfLength = common.AddressLength / 2 -) - -type AddressHalf [addressHalfLength]byte - var ( ErrEmptyWhitelist = errors.New("white list cannot be empty") - ErrResolvingStartTimeNil = errors.New("resolving start time can not be nil") + ErrResolvingStartTimeZero = errors.New("resolving start time can not be 0") ErrFeeReceiverZero = errors.New("fee receiver can not be zero when fee set") ErrTooBigDiffBetweenTimestamps = errors.New("too big diff between timestamps") ) @@ -25,31 +18,26 @@ var ( type SettlementPostInteractionData struct { Whitelist []WhitelistItem IntegratorFee IntegratorFee - BankFee *big.Int - ResolvingStartTime *big.Int + BankFee int64 + ResolvingStartTime int64 CustomReceiver common.Address } func NewSettlementPostInteractionData( whitelist []WhitelistItem, integratorFee IntegratorFee, - bankFee *big.Int, - resolvingStartTime *big.Int, + bankFee int64, + resolvingStartTime int64, customReceiver common.Address, ) (SettlementPostInteractionData, error) { - // set default - if bankFee == nil { - bankFee = big.NewInt(0) - } - // assert - if !integratorFee.IsZero() && integratorFee.Ratio.Cmp(big.NewInt(0)) != 0 { + if !integratorFee.IsZero() && integratorFee.Ratio > 0 { if integratorFee.Receiver.Cmp(common.Address{}) == 0 { // integrator fee receiver is empty return SettlementPostInteractionData{}, ErrFeeReceiverZero } } - if resolvingStartTime == nil { - return SettlementPostInteractionData{}, ErrResolvingStartTimeNil + if resolvingStartTime == 0 { + return SettlementPostInteractionData{}, ErrResolvingStartTimeZero } return SettlementPostInteractionData{ Whitelist: whitelist, @@ -70,7 +58,7 @@ func NewSettlementPostInteractionDataFromSettlementSuffixData( auctionWhitelist := make([]AuctionWhitelistItem, 0, len(data.Whitelist)) for _, item := range data.Whitelist { allowFrom := item.AllowFrom - if allowFrom.Cmp(data.ResolvingStartTime) == -1 { // allowFrom < resolvingStartTime + if allowFrom < data.ResolvingStartTime { allowFrom = data.ResolvingStartTime } auctionWhitelist = append(auctionWhitelist, AuctionWhitelistItem{ @@ -80,23 +68,22 @@ func NewSettlementPostInteractionDataFromSettlementSuffixData( } slices.SortFunc(auctionWhitelist, func(a, b AuctionWhitelistItem) int { - return a.AllowFrom.Cmp(b.AllowFrom) // sort by AllowFrom in ascending order + // sort by AllowFrom in ascending order + return int(a.AllowFrom - b.AllowFrom) }) whitelist := make([]WhitelistItem, 0, len(data.Whitelist)) - sumDelay := big.NewInt(0) + sumDelay := int64(0) for _, item := range auctionWhitelist { - delay := new(big.Int).Sub(new(big.Int).Sub(item.AllowFrom, data.ResolvingStartTime), sumDelay) - sumDelay = new(big.Int).Add(sumDelay, delay) + delay := item.AllowFrom - data.ResolvingStartTime - sumDelay + sumDelay += delay - if delay.Cmp(new(big.Int).SetUint64(math.MaxUint16)) >= 0 { // delay >= math.MaxUint16 + if delay > math.MaxUint16 { return SettlementPostInteractionData{}, ErrTooBigDiffBetweenTimestamps } - var addressHalf AddressHalf - copy(addressHalf[:], item.Address.Bytes()[common.AddressLength-addressHalfLength:]) // take the last 10 bytes whitelist = append(whitelist, WhitelistItem{ - AddressHalf: addressHalf, + AddressHalf: HalfAddressFromAddress(item.Address), Delay: delay, }) } @@ -112,11 +99,11 @@ func NewSettlementPostInteractionDataFromSettlementSuffixData( type WhitelistItem struct { AddressHalf AddressHalf - Delay *big.Int + Delay int64 } type IntegratorFee struct { - Ratio *big.Int + Ratio int64 Receiver common.Address } @@ -126,13 +113,31 @@ func (f IntegratorFee) IsZero() bool { type AuctionWhitelistItem struct { Address common.Address - AllowFrom *big.Int + AllowFrom int64 } type SettlementSuffixData struct { Whitelist []AuctionWhitelistItem IntegratorFee IntegratorFee - BankFee *big.Int - ResolvingStartTime *big.Int + BankFee int64 + ResolvingStartTime int64 CustomReceiver common.Address } + +func (s SettlementPostInteractionData) CanExecuteAt(executor common.Address, executionTime int64) bool { + addressHalf := HalfAddressFromAddress(executor) + + allowedFrom := s.ResolvingStartTime + + for _, item := range s.Whitelist { + allowedFrom += item.Delay + + if addressHalf == item.AddressHalf { + return executionTime >= allowedFrom + } else if executionTime < allowedFrom { + return false + } + } + + return false +} diff --git a/pkg/oneinch/fusionorder/settlement_post_interaction_data_encode_decode.go b/pkg/oneinch/fusionorder/settlement_post_interaction_data_encode_decode.go new file mode 100644 index 0000000..6dec5a8 --- /dev/null +++ b/pkg/oneinch/fusionorder/settlement_post_interaction_data_encode_decode.go @@ -0,0 +1,165 @@ +package fusionorder + +import ( + "bytes" + "errors" + "fmt" + + "github.com/KyberNetwork/tradinglib/pkg/oneinch/decode" + "github.com/ethereum/go-ethereum/common" +) + +const ( + resolverFeeFlag = 0x01 + integratorFeeFlag = 0x02 + customReceiverFlag = 0x04 + whitelistShift = 3 +) + +func resolverFeeEnabled(flags byte) bool { + return flags&resolverFeeFlag == resolverFeeFlag +} + +func integratorFeeEnabled(flags byte) bool { + return flags&integratorFeeFlag == integratorFeeFlag +} + +func hasCustomReceiver(flags byte) bool { + return flags&customReceiverFlag == customReceiverFlag +} + +func resolversCount(flags byte) byte { + return flags >> whitelistShift +} + +var ErrDataTooShort = errors.New("data is too short") + +// DecodeSettlementPostInteractionData decodes SettlementPostInteractionData from bytes +// nolint: gomnd +func DecodeSettlementPostInteractionData(data []byte) (SettlementPostInteractionData, error) { + // must have at least 1 byte for flags + if len(data) < 1 { + return SettlementPostInteractionData{}, ErrDataTooShort + } + + flags := data[len(data)-1] + bi := decode.NewBytesIterator(data[:len(data)-1]) + + var bankFee uint32 + var integratorFee IntegratorFee + var customReceiver common.Address + var err error + + if resolverFeeEnabled(flags) { + bankFee, err = bi.NextUint32() + if err != nil { + return SettlementPostInteractionData{}, fmt.Errorf("get bank fee: %w", err) + } + } + + integratorFee, customReceiver, err = decodeIntegratorFee(flags, bi) + if err != nil { + return SettlementPostInteractionData{}, fmt.Errorf("get integrator fee: %w", err) + } + + resolvingStartTime, err := bi.NextUint32() + if err != nil { + return SettlementPostInteractionData{}, fmt.Errorf("get resolving start time: %w", err) + } + + whitelistCount := resolversCount(flags) + whitelist := make([]WhitelistItem, 0, whitelistCount) + + for i := byte(0); i < whitelistCount; i++ { + addressHalfBytes, err := bi.NextBytes(addressHalfLength) + if err != nil { + return SettlementPostInteractionData{}, fmt.Errorf("get whitelist item address half: %w", err) + } + + var address AddressHalf + copy(address[:], addressHalfBytes) + + delay, err := bi.NextUint16() + if err != nil { + return SettlementPostInteractionData{}, fmt.Errorf("get whitelist item delay: %w", err) + } + + whitelist = append(whitelist, WhitelistItem{ + AddressHalf: address, + Delay: int64(delay), + }) + } + + return SettlementPostInteractionData{ + Whitelist: whitelist, + IntegratorFee: integratorFee, + BankFee: int64(bankFee), + ResolvingStartTime: int64(resolvingStartTime), + CustomReceiver: customReceiver, + }, nil +} + +func decodeIntegratorFee( + flags byte, bi *decode.BytesIterator, +) (integratorFee IntegratorFee, customReceiver common.Address, err error) { + if !integratorFeeEnabled(flags) { + return integratorFee, customReceiver, nil + } + + integratorFeeRatio, err := bi.NextUint16() + if err != nil { + return integratorFee, customReceiver, fmt.Errorf("get integrator fee ratio: %w", err) + } + + integratorAddress, err := bi.NextBytes(common.AddressLength) + if err != nil { + return integratorFee, customReceiver, fmt.Errorf("get integrator fee address: %w", err) + } + + integratorFee = IntegratorFee{ + Ratio: int64(integratorFeeRatio), + Receiver: common.BytesToAddress(integratorAddress), + } + + if hasCustomReceiver(flags) { + customReceiverBytes, err := bi.NextBytes(common.AddressLength) + if err != nil { + return integratorFee, customReceiver, fmt.Errorf("get custom receiver: %w", err) + } + customReceiver = common.BytesToAddress(customReceiverBytes) + } + + return integratorFee, customReceiver, nil +} + +// Encode encodes SettlementPostInteractionData to bytes +// nolint: gomnd +func (s SettlementPostInteractionData) Encode() []byte { + buf := new(bytes.Buffer) + var flags byte + if s.BankFee != 0 { + flags |= resolverFeeFlag + buf.Write(encodeInt64ToBytes(s.BankFee, 4)) + } + if s.IntegratorFee.Ratio != 0 { + flags |= integratorFeeFlag + buf.Write(encodeInt64ToBytes(s.IntegratorFee.Ratio, 2)) + buf.Write(s.IntegratorFee.Receiver.Bytes()) + if s.CustomReceiver != (common.Address{}) { + flags |= customReceiverFlag + buf.Write(s.CustomReceiver.Bytes()) + } + } + buf.Write(encodeInt64ToBytes(s.ResolvingStartTime, 4)) + + for _, wl := range s.Whitelist { + buf.Write(wl.AddressHalf[:]) + buf.Write(encodeInt64ToBytes(wl.Delay, 2)) + } + + flags |= byte(len(s.Whitelist)) << whitelistShift + + buf.WriteByte(flags) + + return buf.Bytes() +} diff --git a/pkg/oneinch/fusionorder/settlement_post_interaction_data_encode_decode_test.go b/pkg/oneinch/fusionorder/settlement_post_interaction_data_encode_decode_test.go new file mode 100644 index 0000000..044d2eb --- /dev/null +++ b/pkg/oneinch/fusionorder/settlement_post_interaction_data_encode_decode_test.go @@ -0,0 +1,199 @@ +package fusionorder_test + +import ( + "math/big" + "testing" + + "github.com/KyberNetwork/tradinglib/pkg/oneinch/decode" + "github.com/KyberNetwork/tradinglib/pkg/oneinch/fusionorder" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Those tests are copied from +// nolint: lll,funlen +// https://github.com/1inch/fusion-sdk/blob/8721c62612b08cc7c0e01423a1bdd62594e7b8d0/src/fusion-order/settlement-post-interaction-data/settlement-post-interaction-data.spec.ts#L6 +func TestSettlementPostInteractionData(t *testing.T) { + t.Run("Should encode/decode with bank fee and whitelist", func(t *testing.T) { + data, err := fusionorder.NewSettlementPostInteractionDataFromSettlementSuffixData( + fusionorder.SettlementSuffixData{ + BankFee: 1, + ResolvingStartTime: 1708117482, + Whitelist: []fusionorder.AuctionWhitelistItem{ + { + Address: common.BigToAddress(big.NewInt(0)), + AllowFrom: 0, + }, + }, + }, + ) + require.NoError(t, err) + + encoded := data.Encode() + + assert.Len(t, encoded, 21) + + decoded, err := fusionorder.DecodeSettlementPostInteractionData(encoded) + require.NoError(t, err) + + assert.Equal(t, data, decoded) + }) + + t.Run("Should encode/decode with no fees and whitelist", func(t *testing.T) { + data, err := fusionorder.NewSettlementPostInteractionDataFromSettlementSuffixData( + fusionorder.SettlementSuffixData{ + ResolvingStartTime: 1708117482, + Whitelist: []fusionorder.AuctionWhitelistItem{ + { + Address: common.BigToAddress(big.NewInt(0)), + AllowFrom: 0, + }, + }, + }, + ) + require.NoError(t, err) + + encoded := data.Encode() + + assert.Len(t, encoded, 17) + + decoded, err := fusionorder.DecodeSettlementPostInteractionData(encoded) + require.NoError(t, err) + + assert.Equal(t, data, decoded) + }) + + t.Run("Should encode/decode with fees and whitelist", func(t *testing.T) { + data, err := fusionorder.NewSettlementPostInteractionDataFromSettlementSuffixData( + fusionorder.SettlementSuffixData{ + BankFee: 0, + ResolvingStartTime: 1708117482, + Whitelist: []fusionorder.AuctionWhitelistItem{ + { + Address: common.BigToAddress(big.NewInt(0)), + AllowFrom: 0, + }, + }, + IntegratorFee: fusionorder.IntegratorFee{ + Ratio: fusionorder.BpsToRatioFormat(10).Int64(), + Receiver: common.BigToAddress(big.NewInt(1)), + }, + }, + ) + require.NoError(t, err) + + decoded, err := fusionorder.DecodeSettlementPostInteractionData(data.Encode()) + require.NoError(t, err) + + assert.Equal(t, data, decoded) + }) + t.Run("Should encode/decode with fees, custom receiver and whitelist", func(t *testing.T) { + data, err := fusionorder.NewSettlementPostInteractionDataFromSettlementSuffixData( + fusionorder.SettlementSuffixData{ + BankFee: 0, + ResolvingStartTime: 1708117482, + Whitelist: []fusionorder.AuctionWhitelistItem{ + { + Address: common.BigToAddress(big.NewInt(0)), + AllowFrom: 0, + }, + }, + IntegratorFee: fusionorder.IntegratorFee{ + Ratio: fusionorder.BpsToRatioFormat(10).Int64(), + Receiver: common.BigToAddress(big.NewInt(1)), + }, + CustomReceiver: common.BigToAddress(big.NewInt(1337)), + }, + ) + require.NoError(t, err) + + decoded, err := fusionorder.DecodeSettlementPostInteractionData(data.Encode()) + require.NoError(t, err) + + assert.Equal(t, data, decoded) + }) + + t.Run("Should generate correct whitelist", func(t *testing.T) { + start := int64(1708117482) + + data, err := fusionorder.NewSettlementPostInteractionDataFromSettlementSuffixData( + fusionorder.SettlementSuffixData{ + ResolvingStartTime: start, + Whitelist: []fusionorder.AuctionWhitelistItem{ + { + Address: common.BigToAddress(big.NewInt(2)), + AllowFrom: start + 1_000, + }, + { + Address: common.BigToAddress(big.NewInt(0)), + AllowFrom: start - 10, // should be set to start + }, + { + Address: common.BigToAddress(big.NewInt(1)), + AllowFrom: start + 10, + }, + { + Address: common.BigToAddress(big.NewInt(3)), + AllowFrom: start + 10, + }, + }, + }, + ) + require.NoError(t, err) + + expectedWhitelist := []fusionorder.WhitelistItem{ + { + AddressHalf: fusionorder.AddressHalf{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, + Delay: 0, + }, + { + AddressHalf: fusionorder.AddressHalf{0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, + Delay: 10, + }, + { + AddressHalf: fusionorder.AddressHalf{0, 0, 0, 0, 0, 0, 0, 0, 0, 3}, + Delay: 0, + }, + { + AddressHalf: fusionorder.AddressHalf{0, 0, 0, 0, 0, 0, 0, 0, 0, 2}, + Delay: 990, + }, + } + + assert.ElementsMatch(t, expectedWhitelist, data.Whitelist) + + assert.True(t, + data.CanExecuteAt(common.BigToAddress(big.NewInt(1)), start+10), + ) + assert.False(t, + data.CanExecuteAt(common.BigToAddress(big.NewInt(1)), start+9), + ) + assert.True(t, + data.CanExecuteAt(common.BigToAddress(big.NewInt(3)), start+10), + ) + assert.False(t, + data.CanExecuteAt(common.BigToAddress(big.NewInt(3)), start+9), + ) + assert.False(t, + data.CanExecuteAt(common.BigToAddress(big.NewInt(2)), start+50), + ) + }) +} + +func TestSettlementPostInteractionData_invalid_data_length(t *testing.T) { + t.Run("empty data", func(t *testing.T) { + _, err := fusionorder.DecodeSettlementPostInteractionData([]byte{}) + require.ErrorIs(t, err, fusionorder.ErrDataTooShort) + }) + + t.Run("invalid data", func(t *testing.T) { + data, err := hexutil.Decode("0x010203") + require.NoError(t, err) + + _, err = fusionorder.DecodeSettlementPostInteractionData(data) + + require.ErrorIs(t, err, decode.ErrOutOfData) + }) +} diff --git a/pkg/oneinch/fusionutils/fusion.go b/pkg/oneinch/fusionorder/utils.go similarity index 95% rename from pkg/oneinch/fusionutils/fusion.go rename to pkg/oneinch/fusionorder/utils.go index eaa9f82..c1cb7c4 100644 --- a/pkg/oneinch/fusionutils/fusion.go +++ b/pkg/oneinch/fusionorder/utils.go @@ -1,4 +1,4 @@ -package fusionutils +package fusionorder import "math/big" diff --git a/pkg/oneinch/limitorder/extension.go b/pkg/oneinch/limitorder/extension.go new file mode 100644 index 0000000..3da7c06 --- /dev/null +++ b/pkg/oneinch/limitorder/extension.go @@ -0,0 +1,167 @@ +package limitorder + +import ( + "bytes" + "fmt" + "math" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + eMath "github.com/ethereum/go-ethereum/common/math" +) + +const ( + totalOffsetSlots = 8 + offsetSlotSizeInBits = 32 + offsetLength = common.HashLength +) + +// Extension represents the extension data of a 1inch order. +// This is copied from +// nolint: lll +// https://github.com/1inch/limit-order-sdk/blob/999852bc3eb92fb75332b7e3e0300e74a51943c1/src/limit-order/extension.ts#L6 +type Extension struct { + MakerAssetSuffix []byte + TakerAssetSuffix []byte + MakingAmountData []byte + TakingAmountData []byte + Predicate []byte + MakerPermit []byte + PreInteraction []byte + PostInteraction []byte + CustomData []byte +} + +func (e Extension) HasMakerPermit() bool { + return len(e.MakerPermit) > 0 +} + +func (e Extension) IsEmpty() bool { + return len(e.getConcatenatedInteractions()) == 0 +} + +func (e Extension) Encode() string { + interactionsConcatenated := e.getConcatenatedInteractions() + if len(interactionsConcatenated) == 0 { + return hexutil.Encode(interactionsConcatenated) + } + + offset := e.getOffsets() + + b := new(bytes.Buffer) + b.Write(offset[:]) + b.Write(interactionsConcatenated) + b.Write(e.CustomData) + + return hexutil.Encode(b.Bytes()) +} + +func (e Extension) interactionsArray() [totalOffsetSlots][]byte { + return [totalOffsetSlots][]byte{ + e.MakerAssetSuffix, + e.TakerAssetSuffix, + e.MakingAmountData, + e.TakingAmountData, + e.Predicate, + e.MakerPermit, + e.PreInteraction, + e.PostInteraction, + } +} + +func (e Extension) getConcatenatedInteractions() []byte { + builder := new(bytes.Buffer) + for _, interaction := range e.interactionsArray() { + builder.Write(interaction) + } + return builder.Bytes() +} + +func (e Extension) getOffsets() common.Hash { + var lengthMap [totalOffsetSlots]int + for i, interaction := range e.interactionsArray() { + lengthMap[i] = len(interaction) + } + + cumulativeSum := 0 + bytesAccumulator := big.NewInt(0) + + for i, length := range lengthMap { + cumulativeSum += length + shiftVal := big.NewInt(int64(cumulativeSum)) + shiftVal.Lsh(shiftVal, uint(offsetSlotSizeInBits*i)) // Shift left + bytesAccumulator.Add(bytesAccumulator, shiftVal) // Add to accumulator + } + + return common.Hash(eMath.PaddedBigBytes(bytesAccumulator, offsetLength)) +} + +// DecodeExtension decodes the encoded extension string into an Extension struct. +// The encoded extension string is expected to be in the format of "0x" followed by the hex-encoded extension data. +// The hex-encoded extension data is expected to be in +// the format of 32 bytes of offset data followed by the extension data. +func DecodeExtension(encodedExtension string) (Extension, error) { + extensionDataBytes, err := hexutil.Decode(encodedExtension) + if err != nil { + return Extension{}, fmt.Errorf("decode extension data: %w", err) + } + + if len(extensionDataBytes) == 0 { + return defaultExtension(), nil + } + + if len(extensionDataBytes) < offsetLength { + return Extension{}, + fmt.Errorf("extension data length (%d) is less than offset length (%d)", + len(extensionDataBytes), offsetLength) + } + + offset := new(big.Int).SetBytes(extensionDataBytes[:offsetLength]) + + maxInt32 := big.NewInt(math.MaxInt32) + + extensionData := extensionDataBytes[offsetLength:] + + data := [totalOffsetSlots][]byte{} + prevLength := 0 + for i := 0; i < totalOffsetSlots; i++ { + length := int(new(big.Int).And( + new(big.Int).Rsh( + offset, uint(i*offsetSlotSizeInBits), + ), + maxInt32, + ).Int64()) + + start := prevLength + end := length + + if len(extensionData) < end { + return Extension{}, + fmt.Errorf("extension data length (%d) is less than expected (%d)", len(extensionData), end) + } + + data[i] = extensionData[start:end] + + prevLength = length + } + customData := extensionData[prevLength:] + + e := Extension{ + MakerAssetSuffix: data[0], + TakerAssetSuffix: data[1], + MakingAmountData: data[2], + TakingAmountData: data[3], + Predicate: data[4], + MakerPermit: data[5], + PreInteraction: data[6], + PostInteraction: data[7], + CustomData: customData, + } + + return e, nil +} + +func defaultExtension() Extension { + return Extension{} +} diff --git a/pkg/oneinch/limitorder/extension_test.go b/pkg/oneinch/limitorder/extension_test.go new file mode 100644 index 0000000..950e591 --- /dev/null +++ b/pkg/oneinch/limitorder/extension_test.go @@ -0,0 +1,54 @@ +package limitorder_test + +import ( + "testing" + + "github.com/KyberNetwork/tradinglib/pkg/oneinch/limitorder" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestExtension(t *testing.T) { + t.Run("should encode/decode", func(t *testing.T) { + extension := limitorder.Extension{ + MakerAssetSuffix: []byte{0x01}, + TakerAssetSuffix: []byte{0x02}, + MakingAmountData: []byte{0x03}, + TakingAmountData: []byte{0x04}, + Predicate: []byte{0x05}, + MakerPermit: []byte{0x06}, + PreInteraction: []byte{0x07}, + PostInteraction: []byte{0x08}, + CustomData: []byte{0xff}, + } + + encodedExtension := extension.Encode() + + decodedExtension, err := limitorder.DecodeExtension(encodedExtension) + require.NoError(t, err) + + require.Equal(t, extension, decodedExtension) + }) + + t.Run("decode empty", func(t *testing.T) { + encodedExtension := "0x" + e, err := limitorder.DecodeExtension(encodedExtension) + require.NoError(t, err) + + assert.True(t, e.IsEmpty()) + }) + + t.Run("decode", func(t *testing.T) { + // nolint: lll + encodedExtension := "0x000000e5000000540000005400000054000000540000002a0000000000000000fb2809a5314473e1165f6b58018e20ed8f07b84000f1b8000005e566bb30120000b401def800f1b800b4fb2809a5314473e1165f6b58018e20ed8f07b84000f1b8000005e566bb30120000b401def800f1b800b4fb2809a5314473e1165f6b58018e20ed8f07b84066bb2ffab09498030ae3416b66dc0000db05a6a504f04d92e79d0000339fb574bdc56763f9950000d18bd45f0b94f54a968f0000d61b892b2ad6249011850000f7f4f96b98e102b56b0400000000000000000000000000006de5e0e428ac771d77b5000006455390207c1d485be90000ade19567bb538035ed36000050" + e, err := limitorder.DecodeExtension(encodedExtension) + require.NoError(t, err) + + t.Logf("Extension: %+v", e) + + assert.False(t, e.IsEmpty()) + assert.NotEmpty(t, e.MakingAmountData) + assert.NotEmpty(t, e.TakingAmountData) + assert.NotEmpty(t, e.PostInteraction) + }) +} diff --git a/pkg/oneinch/limitorder/interaction.go b/pkg/oneinch/limitorder/interaction.go new file mode 100644 index 0000000..008f61f --- /dev/null +++ b/pkg/oneinch/limitorder/interaction.go @@ -0,0 +1,34 @@ +package limitorder + +import ( + "encoding/hex" + "fmt" + + "github.com/KyberNetwork/tradinglib/pkg/oneinch/decode" + "github.com/ethereum/go-ethereum/common" +) + +type Interaction struct { + Target common.Address + Data []byte +} + +func (i Interaction) IsZero() bool { + return i.Target.String() == common.Address{}.String() && len(i.Data) == 0 +} + +func (i Interaction) Encode() string { + return i.Target.String() + hex.EncodeToString(i.Data) +} + +func DecodeInteraction(data []byte) (Interaction, error) { + bi := decode.NewBytesIterator(data) + target, err := bi.NextBytes(common.AddressLength) + if err != nil { + return Interaction{}, fmt.Errorf("get target: %w", err) + } + return Interaction{ + Target: common.BytesToAddress(target), + Data: bi.RemainingData(), + }, nil +} diff --git a/pkg/oneinch/limitorder/interaction_test.go b/pkg/oneinch/limitorder/interaction_test.go new file mode 100644 index 0000000..fbe9c29 --- /dev/null +++ b/pkg/oneinch/limitorder/interaction_test.go @@ -0,0 +1,31 @@ +package limitorder_test + +import ( + "math/big" + "testing" + + "github.com/KyberNetwork/tradinglib/pkg/oneinch/limitorder" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestInteraction(t *testing.T) { + t.Run("should encode/decode", func(t *testing.T) { + data, err := hexutil.Decode("0xdeadbeef") + require.NoError(t, err) + interaction := limitorder.Interaction{ + Target: common.BigToAddress(big.NewInt(1337)), + Data: data, + } + + encodedInteraction, err := hexutil.Decode(interaction.Encode()) + require.NoError(t, err) + + decodedInteraction, err := limitorder.DecodeInteraction(encodedInteraction) + require.NoError(t, err) + + assert.Equal(t, interaction, decodedInteraction) + }) +}