diff --git a/CHANGELOG.md b/CHANGELOG.md index 094bed7..bd98970 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) starting with the *v1.0.0-beta.1* release. +## [v2.0.0] - 2025-11-05 +[v2.0.0 release page](https://github.com/1inch/1inch-sdk-go/releases/tag/v2.0.0) + +### Changed +- Fusion Plus updated to use v1.1 API + +## [v2.0.0-preview.2] - 2025-10-30 +[v2.0.0-preview.2 release page](https://github.com/1inch/1inch-sdk-go/releases/tag/v2.0.0-preview.2) + +### Breaking Changes +- Limit Orders have been refactored. Order creation now uses a different flow. See the examples for more details. + +### Changed +- Limit Order SDK updated to support v4.1 API + ## [v2.0.0-preview] - 2025-1-22 [v2.0.0-preview release page](https://github.com/1inch/1inch-sdk-go/releases/tag/v2.0.0-preview) diff --git a/sdk-clients/fusionplus/api.go b/sdk-clients/fusionplus/api.go index 181754b..b8c36bb 100644 --- a/sdk-clients/fusionplus/api.go +++ b/sdk-clients/fusionplus/api.go @@ -3,14 +3,13 @@ package fusionplus import ( "context" "encoding/json" - "errors" "fmt" "github.com/1inch/1inch-sdk-go/common" ) func (api *api) GetOrderByOrderHash(ctx context.Context, params GetOrderByOrderHashParams) (*GetOrderFillsByHashOutputFixed, error) { - u := fmt.Sprintf("/fusion-plus/orders/v1.0/order/status/%s", params.Hash) + u := fmt.Sprintf("/fusion-plus/orders/v1.1/order/status/%s", params.Hash) payload := common.RequestPayload{ Method: "GET", @@ -28,8 +27,8 @@ func (api *api) GetOrderByOrderHash(ctx context.Context, params GetOrderByOrderH return &response, nil } -func (api *api) GetReadyToAcceptFills(ctx context.Context, params GetOrderByOrderHashParams) (*ReadyToAcceptSecretFills, error) { - u := fmt.Sprintf("/fusion-plus/orders/v1.0/order/ready-to-accept-secret-fills/%s", params.Hash) +func (api *api) GetReadyToAcceptFills(ctx context.Context, params GetReadyToAcceptFillsParams) (*ReadyToAcceptSecretFills, error) { + u := fmt.Sprintf("/fusion-plus/orders/v1.1/order/ready-to-accept-secret-fills/%s", params.Hash) payload := common.RequestPayload{ Method: "GET", @@ -48,7 +47,7 @@ func (api *api) GetReadyToAcceptFills(ctx context.Context, params GetOrderByOrde } func (api *api) SubmitSecret(ctx context.Context, params SecretInput) error { - u := "/fusion-plus/relayer/v1.0/submit/secret" + u := "/fusion-plus/relayer/v1.1/submit/secret" body, err := json.Marshal(params) if err != nil { @@ -70,57 +69,10 @@ func (api *api) SubmitSecret(ctx context.Context, params SecretInput) error { return nil } -func (api *api) GetActiveOrders(ctx context.Context, params OrderApiControllerGetActiveOrdersParams) (*GetActiveOrdersOutput, error) { - u := fmt.Sprintf("/fusion/orders/v2.0/%d/order/active", api.chainId) - - payload := common.RequestPayload{ - Method: "GET", - Params: params, - U: u, - Body: nil, - } - - var response GetActiveOrdersOutput - err := api.httpExecutor.ExecuteRequest(ctx, payload, &response) - if err != nil { - return nil, err - } - - return &response, nil -} - func (api *api) GetQuote(ctx context.Context, params QuoterControllerGetQuoteParamsFixed) (*GetQuoteOutputFixed, error) { - return nil, errors.New("fusion Plus API currently not supported") - - //u := "/fusion-plus/quoter/v1.0/quote/receive" - // - //err := params.Validate() - //if err != nil { - // return nil, err - //} - // - //payload := common.RequestPayload{ - // Method: "GET", - // Params: params, - // U: u, - // Body: nil, - //} - // - //var response GetQuoteOutputFixed - //err = api.httpExecutor.ExecuteRequest(ctx, payload, &response) - //if err != nil { - // return nil, err - //} - // - //// TODO must normalize response here - // - //return &response, nil -} - -func (api *api) GetQuoteWithCustomPreset(ctx context.Context, params QuoterControllerGetQuoteWithCustomPresetsParams, presetDetails QuoterControllerGetQuoteWithCustomPresetsJSONRequestBody) (*GetQuoteOutputFixed, error) { - u := fmt.Sprintf("/fusion/quoter/v2.0/%d/quote/receive", api.chainId) + u := "/fusion-plus/quoter/v1.1/quote/receive" - body, err := json.Marshal(presetDetails) + err := params.Validate() if err != nil { return nil, err } @@ -129,7 +81,7 @@ func (api *api) GetQuoteWithCustomPreset(ctx context.Context, params QuoterContr Method: "GET", Params: params, U: u, - Body: body, + Body: nil, } var response GetQuoteOutputFixed @@ -138,12 +90,14 @@ func (api *api) GetQuoteWithCustomPreset(ctx context.Context, params QuoterContr return nil, err } + // TODO must normalize response here + return &response, nil } // PlaceOrder accepts a quote and submits it as a fusion plus order func (api *api) PlaceOrder(ctx context.Context, quoteParams QuoterControllerGetQuoteParamsFixed, quote *GetQuoteOutputFixed, orderParams OrderParams, wallet common.Wallet) (string, error) { - u := "/fusion-plus/relayer/v1.0/submit" + u := "/fusion-plus/relayer/v1.1/submit" err := orderParams.Validate() if err != nil { diff --git a/sdk-clients/fusionplus/client.go b/sdk-clients/fusionplus/client.go index 343a7f3..2611125 100644 --- a/sdk-clients/fusionplus/client.go +++ b/sdk-clients/fusionplus/client.go @@ -10,7 +10,6 @@ type Client struct { } type api struct { - chainId uint64 httpExecutor common.HttpExecutor } diff --git a/sdk-clients/fusionplus/escrowextension.go b/sdk-clients/fusionplus/escrowextension.go index c619257..af61a1f 100644 --- a/sdk-clients/fusionplus/escrowextension.go +++ b/sdk-clients/fusionplus/escrowextension.go @@ -3,17 +3,20 @@ package fusionplus import ( "bytes" "encoding/binary" + "encoding/hex" "fmt" + "log" "math/big" + "strings" + "github.com/1inch/1inch-sdk-go/internal/bytesiterator" "github.com/1inch/1inch-sdk-go/internal/hexadecimal" - "github.com/1inch/1inch-sdk-go/sdk-clients/fusion" "github.com/1inch/1inch-sdk-go/sdk-clients/orderbook" "github.com/ethereum/go-ethereum/common" ) type EscrowExtension struct { - fusion.Extension + ExtensionFusion HashLock *HashLock DstChainId float32 DstToken common.Address @@ -24,13 +27,13 @@ type EscrowExtension struct { func NewEscrowExtension(escrowParams EscrowExtensionParams) (*EscrowExtension, error) { - extension, err := fusion.NewExtension(escrowParams.ExtensionParams) + extension, err := NewExtensionFusion(escrowParams.ExtensionParamsFusion) if err != nil { return nil, err } escrowExtension := &EscrowExtension{ - Extension: *extension, + ExtensionFusion: *extension, HashLock: escrowParams.HashLock, DstChainId: escrowParams.DstChainId, DstToken: escrowParams.DstToken, @@ -83,6 +86,132 @@ func (e *EscrowExtension) ConvertToOrderbookExtension() (*orderbook.Extension, e }, nil } +// DecodeEscrowExtension decodes the input byte slice into an Extension struct using reflection. +func DecodeEscrowExtension(data []byte) (*EscrowExtension, error) { + + const extraDataCharacterLength = 320 + + // Create one extension that will be used for the Escrow extension data + orderbookExtensionTruncated, err := orderbook.Decode(data) + if err != nil { + return nil, fmt.Errorf("error decoding extension: %v", err) + } + + // Remove the Fusion Plus Extension data before decoding + orderbookExtensionTruncated.PostInteraction = orderbookExtensionTruncated.PostInteraction[:len(orderbookExtensionTruncated.PostInteraction)-extraDataCharacterLength] + fusionExtension, err := FromLimitOrderExtension(orderbookExtensionTruncated) + if err != nil { + return &EscrowExtension{}, fmt.Errorf("error decoding escrow extension: %v", err) + } + + // Create a second extension that will be used as a Fusion extension + orderbookExtension, err := orderbook.Decode(data) + if err != nil { + return nil, fmt.Errorf("error decoding extension: %v", err) + } + extraDataRaw := orderbookExtension.PostInteraction[len(orderbookExtension.PostInteraction)-extraDataCharacterLength:] + extraDataBytes, err := hex.DecodeString(extraDataRaw) + if err != nil { + return nil, fmt.Errorf("error decoding escrow extension extra data: %v", err) + } + + // Send the final 160 bytes of the postInteraction to decodeExtraData + extraData, err := decodeExtraData(extraDataBytes) + if err != nil { + return nil, fmt.Errorf("error decoding escrow extension extra data: %v", err) + } + + return &EscrowExtension{ + ExtensionFusion: *fusionExtension, + HashLock: extraData.HashLock, + DstChainId: extraData.DstChainId, + DstToken: extraData.DstToken, + SrcSafetyDeposit: fmt.Sprintf("%x", extraData.SrcSafetyDeposit), + DstSafetyDeposit: fmt.Sprintf("%x", extraData.DstSafetyDeposit), + TimeLocks: *extraData.TimeLocks, + }, nil +} + +func decodeExtraData(data []byte) (*EscrowExtraData, error) { + iter := bytesiterator.New(data) + hashlockData, err := iter.NextUint256() + if err != nil { + log.Fatalf("Failed to read first uint256: %v", err) + } + + dstChainIdData, err := iter.NextUint256() + if err != nil { + log.Fatalf("Failed to read second uint256: %v", err) + } + + addressBig, err := iter.NextUint256() + if err != nil { + log.Fatalf("Failed to read address: %v", err) + } + + addressHex := strings.ToLower(common.BigToAddress(addressBig).Hex()) + + safetyDepositData, err := iter.NextUint256() + if err != nil { + log.Fatalf("Failed to read third uint256: %v", err) + } + + // Define a 128-bit mask (2^128 - 1) + mask := new(big.Int) + mask.Exp(big.NewInt(2), big.NewInt(128), nil).Sub(mask, big.NewInt(1)) + + srcSafetyDeposit := new(big.Int).And(safetyDepositData, mask) + dstSafetyDeposit := new(big.Int).Rsh(safetyDepositData, 128) + + timelocksData, err := iter.NextUint256() + if err != nil { + log.Fatalf("Failed to read fourth uint256: %v", err) + } + + timelocks, err := decodeTimeLocks(timelocksData) + if err != nil { + log.Fatalf("Failed to decode timelocks: %v", err) + } + + return &EscrowExtraData{ + HashLock: &HashLock{ + hashlockData.String(), + }, + DstChainId: float32(dstChainIdData.Uint64()), + DstToken: common.HexToAddress(addressHex), + SrcSafetyDeposit: srcSafetyDeposit, + DstSafetyDeposit: dstSafetyDeposit, + TimeLocks: timelocks, + }, nil +} + +// decodeTimeLocks takes a *big.Int containing the raw hex data and returns a TimeLocks struct. +func decodeTimeLocks(value *big.Int) (*TimeLocks, error) { + tl := &TimeLocks{} + + // Convert big.Int to byte slice + data := value.Bytes() + + if len(data) < 32 { + padded := make([]byte, 32) + copy(padded[32-len(data):], data) + data = padded + } + + //TODO big.Int cannot preserve leading zeroes, so decoding the deploy time is impossible atm + + // tl.DeployTime = float32(binary.BigEndian.Uint32((data[0:4]))) + tl.DstCancellation = float32(binary.BigEndian.Uint32((data[4:8]))) + tl.DstPublicWithdrawal = float32(binary.BigEndian.Uint32((data[8:12]))) + tl.DstWithdrawal = float32(binary.BigEndian.Uint32((data[12:16]))) + tl.SrcPublicCancellation = float32(binary.BigEndian.Uint32((data[16:20]))) + tl.SrcCancellation = float32(binary.BigEndian.Uint32((data[20:24]))) + tl.SrcPublicWithdrawal = float32(binary.BigEndian.Uint32((data[24:28]))) + tl.SrcWithdrawal = float32(binary.BigEndian.Uint32((data[28:32]))) + + return tl, nil +} + type EscrowExtraData struct { HashLock *HashLock DstChainId float32 diff --git a/sdk-clients/fusionplus/escrowextension_test.go b/sdk-clients/fusionplus/escrowextension_test.go index 13e7b43..de817e8 100644 --- a/sdk-clients/fusionplus/escrowextension_test.go +++ b/sdk-clients/fusionplus/escrowextension_test.go @@ -7,7 +7,6 @@ import ( "github.com/1inch/1inch-sdk-go/internal/bigint" random_number_generation "github.com/1inch/1inch-sdk-go/internal/random-number-generation" - "github.com/1inch/1inch-sdk-go/sdk-clients/fusion" "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -36,7 +35,7 @@ func TestGenerateSalt(t *testing.T) { { name: "Generate salt when extension is not empty", extension: &EscrowExtension{ - Extension: fusion.Extension{ + ExtensionFusion: ExtensionFusion{ MakerAssetSuffix: "suffix1", TakerAssetSuffix: "suffix2", MakingAmountData: "data1", @@ -48,7 +47,7 @@ func TestGenerateSalt(t *testing.T) { CustomData: "custom", }, }, - expected: "180431909497609865807168059378624320943465639784996571", + expected: "180431178743033967347942937469468920088249224033532329", expectErr: false, }, } @@ -77,10 +76,55 @@ func TestNewExtension(t *testing.T) { expectErr bool errMsg string }{ + { + name: "Valid parameters with Escrow", + params: EscrowExtensionParams{ + ExtensionParamsFusion: ExtensionParamsFusion{ + SettlementContract: "0x5678", + AuctionDetails: &AuctionDetails{ + StartTime: 0, + Duration: 0, + InitialRateBump: 0, + Points: nil, + GasCost: GasCostConfigClassFixed{}, + }, + PostInteractionData: &SettlementPostInteractionDataFusion{ + Whitelist: []WhitelistItem{}, + IntegratorFee: &IntegratorFeeFusion{ + Ratio: big.NewInt(0), + Receiver: common.Address{}, + }, + BankFee: big.NewInt(0), + ResolvingStartTime: big.NewInt(0), + CustomReceiver: common.Address{}, + }, + Asset: "0x1234", + Permit: "0x3456", + + MakerAssetSuffix: "0x1234", + TakerAssetSuffix: "0x1234", + Predicate: "0x1234", + PreInteraction: "pre", + }, + }, + expected: &EscrowExtension{ + ExtensionFusion: ExtensionFusion{ + MakerAssetSuffix: "0x1234", + TakerAssetSuffix: "0x1234", + MakingAmountData: "0x00000000000000000000000000000000000056780000000000000000000000000000000000", + TakingAmountData: "0x00000000000000000000000000000000000056780000000000000000000000000000000000", + Predicate: "0x1234", + MakerPermit: "0x00000000000000000000000000000000000012343456", + PreInteraction: "pre", + PostInteraction: "0x00000000000000000000000000000000000056780000000000", + }, + }, + expectErr: false, + }, { name: "Invalid MakerAssetSuffix", params: EscrowExtensionParams{ - ExtensionParams: fusion.ExtensionParams{ + ExtensionParamsFusion: ExtensionParamsFusion{ SettlementContract: "0x5678", MakerAssetSuffix: "invalid", TakerAssetSuffix: "0x1234", @@ -94,7 +138,7 @@ func TestNewExtension(t *testing.T) { { name: "Invalid TakerAssetSuffix", params: EscrowExtensionParams{ - ExtensionParams: fusion.ExtensionParams{ + ExtensionParamsFusion: ExtensionParamsFusion{ SettlementContract: "0x5678", MakerAssetSuffix: "0x1234", TakerAssetSuffix: "invalid", @@ -108,7 +152,7 @@ func TestNewExtension(t *testing.T) { { name: "Invalid Predicate", params: EscrowExtensionParams{ - ExtensionParams: fusion.ExtensionParams{ + ExtensionParamsFusion: ExtensionParamsFusion{ SettlementContract: "0x5678", MakerAssetSuffix: "0x1234", TakerAssetSuffix: "0x1234", @@ -122,7 +166,7 @@ func TestNewExtension(t *testing.T) { { name: "CustomData not supported", params: EscrowExtensionParams{ - ExtensionParams: fusion.ExtensionParams{ + ExtensionParamsFusion: ExtensionParamsFusion{ SettlementContract: "0x5678", MakerAssetSuffix: "0x1234", TakerAssetSuffix: "0x1234", @@ -150,6 +194,7 @@ func TestNewExtension(t *testing.T) { assert.Equal(t, tc.expected.Predicate, ext.Predicate) assert.Equal(t, tc.expected.PreInteraction, ext.PreInteraction) assert.Equal(t, tc.expected.PostInteraction, ext.PostInteraction) + assert.Equal(t, tc.expected.CustomData, ext.CustomData) } }) } diff --git a/sdk-clients/fusionplus/examples/get_order_by_hash/main.go b/sdk-clients/fusionplus/examples/get_order_by_hash/main.go index 979219c..3d5e2c9 100644 --- a/sdk-clients/fusionplus/examples/get_order_by_hash/main.go +++ b/sdk-clients/fusionplus/examples/get_order_by_hash/main.go @@ -30,7 +30,7 @@ func main() { } ctx := context.Background() - response, err := client.GetReadyToAcceptFills(ctx, fusionplus.GetOrderByOrderHashParams{ + response, err := client.GetOrderByOrderHash(ctx, fusionplus.GetOrderByOrderHashParams{ Hash: "0x97729858044d3838c82f2ea5ca4764bd20bfdf1f99d3af05786e4a358b16fa91", }) if err != nil { diff --git a/sdk-clients/fusionplus/examples/get_ready_to_accept_secret_fills/main.go b/sdk-clients/fusionplus/examples/get_ready_to_accept_secret_fills/main.go deleted file mode 100644 index 626d9f3..0000000 --- a/sdk-clients/fusionplus/examples/get_ready_to_accept_secret_fills/main.go +++ /dev/null @@ -1,53 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "fmt" - "log" - "os" - - "github.com/1inch/1inch-sdk-go/sdk-clients/fusionplus" -) - -var ( - devPortalToken = os.Getenv("DEV_PORTAL_TOKEN") - publicAddress = os.Getenv("WALLET_ADDRESS") - privateKey = os.Getenv("WALLET_KEY") -) - -const ( - usdc = "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359" - wmatic = "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270" - amount = "100000000" - chainId = 137 -) - -func main() { - config, err := fusionplus.NewConfiguration(fusionplus.ConfigurationParams{ - ApiUrl: "https://api.1inch.dev", - ApiKey: devPortalToken, - PrivateKey: privateKey, - }) - if err != nil { - log.Fatalf("failed to create configuration: %v", err) - } - client, err := fusionplus.NewClient(config) - if err != nil { - log.Fatalf("failed to create client: %v", err) - } - ctx := context.Background() - - response, err := client.GetOrderByOrderHash(ctx, fusionplus.GetOrderByOrderHashParams{ - Hash: "0x97729858044d3838c82f2ea5ca4764bd20bfdf1f99d3af05786e4a358b16fa91", - }) - if err != nil { - log.Fatalf("failed to request: %v", err) - } - - output, err := json.MarshalIndent(response, "", " ") - if err != nil { - log.Fatalf("Failed to marshal response: %v\n", err) - } - fmt.Printf("Response: %s\n", string(output)) -} diff --git a/sdk-clients/fusionplus/examples/place_order/main.go b/sdk-clients/fusionplus/examples/place_order/main.go index 0b6ccea..9f48e7d 100644 --- a/sdk-clients/fusionplus/examples/place_order/main.go +++ b/sdk-clients/fusionplus/examples/place_order/main.go @@ -38,7 +38,7 @@ func main() { srcToken := "0xaf88d065e77c8cC2239327C5EDb3A432268e5831" dstToken := "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" - invert := false + invert := true if invert { srcChain, dstChain = dstChain, srcChain srcToken, dstToken = dstToken, srcToken @@ -107,7 +107,6 @@ func main() { log.Fatalf("Failed to create order data: %v", err) } - // Get order by hash order, err := client.GetOrderByOrderHash(ctx, fusionplus.GetOrderByOrderHashParams{ Hash: orderHash, }) @@ -121,39 +120,31 @@ func main() { } fmt.Printf("Order: %s\n", string(orderQuickLookIndented)) - // Define loop parameters delay := 1 * time.Second // Delay between retries retryCount := 0 // Current retry count orderStatus := "" // Current order status - // Loop until order status is "executed" or max retries reached + fmt.Println("Waiting for fill requests from the Cross Chain Swap system...") + for { - // Get order by hash order, err = client.GetOrderByOrderHash(ctx, fusionplus.GetOrderByOrderHashParams{ Hash: orderHash, }) if err != nil { log.Printf("Failed to get order by hash: %v", err) } else { - // Assuming order.Status is a string. Adjust the field access as per actual response structure. orderStatus = string(order.Status) - fmt.Printf("Attempt %d: Order Status: %s\n", retryCount+1, orderStatus) - - // Check if status is "executed" if orderStatus == "executed" { - fmt.Println("Order has been executed.") + fmt.Println("Order completed successfully.") break } - - // Check if status is "executed" if orderStatus == "refunded" { - fmt.Println("Order has been refunded.") + fmt.Println("Order did not complete and has been refunded.") break } } - // TODO fix params on this - fills, err := client.GetReadyToAcceptFills(ctx, fusionplus.GetOrderByOrderHashParams{ + fills, err := client.GetReadyToAcceptFills(ctx, fusionplus.GetReadyToAcceptFillsParams{ Hash: orderHash, }) if err != nil { @@ -161,30 +152,17 @@ func main() { } if len(fills.Fills) > 0 { - // TODO the secret index needs to match the index of the fill object, but I can ignore it for single-secre orders + // TODO the secret index needs to match the index of the fill object, but this is not important for single-secre orders err = client.SubmitSecret(ctx, fusionplus.SecretInput{ OrderHash: orderHash, Secret: secrets[0], }) if err != nil { log.Fatalf("failed to submit secret: %v", err) - } else { - fmt.Println("Secret submitted!") } + fmt.Println("Fill request received and secret submitted! Checking for more fills...") } - - fmt.Printf("Fills: %v\n", fills) - - // Increment retry count retryCount++ - - // Wait before next retry time.Sleep(delay) } - - orderIndented, err := json.MarshalIndent(order, "", " ") - if err != nil { - log.Fatalf("Failed to marshal response: %v\n", err) - } - fmt.Printf("Order: %s\n", string(orderIndented)) } diff --git a/sdk-clients/fusionplus/extension_fusion.go b/sdk-clients/fusionplus/extension_fusion.go new file mode 100644 index 0000000..599aca9 --- /dev/null +++ b/sdk-clients/fusionplus/extension_fusion.go @@ -0,0 +1,201 @@ +package fusionplus + +import ( + "encoding/json" + "errors" + "fmt" + "math/big" + + "github.com/1inch/1inch-sdk-go/internal/hexadecimal" + geth_common "github.com/ethereum/go-ethereum/common" + "golang.org/x/crypto/sha3" + + random_number_generation "github.com/1inch/1inch-sdk-go/internal/random-number-generation" + "github.com/1inch/1inch-sdk-go/sdk-clients/orderbook" +) + +func NewExtensionFusion(params ExtensionParamsFusion) (*ExtensionFusion, error) { + if !hexadecimal.IsHexBytes(params.SettlementContract) { + return nil, errors.New("Settlement contract must be valid hex string") + } + if !hexadecimal.IsHexBytes(params.MakerAssetSuffix) { + return nil, errors.New("MakerAssetSuffix must be valid hex string") + } + if !hexadecimal.IsHexBytes(params.TakerAssetSuffix) { + return nil, errors.New("TakerAssetSuffix must be valid hex string") + } + if !hexadecimal.IsHexBytes(params.Predicate) { + return nil, errors.New("Predicate must be valid hex string") + } + if params.CustomData != "" { + return nil, errors.New("CustomData is not currently supported") + } + + settlementContractAddress := geth_common.HexToAddress(params.SettlementContract) + makingAndTakingAmountData := settlementContractAddress.String() + hexadecimal.Trim0x(params.AuctionDetails.Encode()) + + fusionExtension := &ExtensionFusion{ + SettlementContract: params.SettlementContract, + AuctionDetails: params.AuctionDetails, + PostInteractionData: params.PostInteractionData, + Asset: params.Asset, + Permit: params.Permit, + + MakerAssetSuffix: params.MakerAssetSuffix, + TakerAssetSuffix: params.TakerAssetSuffix, + MakingAmountData: makingAndTakingAmountData, + TakingAmountData: makingAndTakingAmountData, + Predicate: params.Predicate, + PreInteraction: params.PreInteraction, + CustomData: params.CustomData, + } + + postInteractoinDataEncoded, err := params.PostInteractionData.Encode() + if err != nil { + return nil, fmt.Errorf("failed to encode post interaction data: %v", err) + } + fusionExtension.PostInteraction = NewInteraction(settlementContractAddress, postInteractoinDataEncoded).Encode() + + if params.Permit != "" { + permitInteraction := &Interaction{ + Target: geth_common.HexToAddress(params.Asset), + Data: params.Permit, + } + fusionExtension.MakerPermit = permitInteraction.Target.String() + hexadecimal.Trim0x(permitInteraction.Data) + } + + return fusionExtension, nil +} + +// Keccak256 calculates the Keccak256 hash of the extension data +func (e *ExtensionFusion) Keccak256() *big.Int { + jsonData, err := json.Marshal(e) + if err != nil { + panic(err) + } + hash := sha3.New256() + hash.Write(jsonData) + return new(big.Int).SetBytes(hash.Sum(nil)) +} + +func (e *ExtensionFusion) ConvertToOrderbookExtension() *orderbook.Extension { + return &orderbook.Extension{ + MakerAssetSuffix: e.MakerAssetSuffix, + TakerAssetSuffix: e.TakerAssetSuffix, + MakingAmountData: e.MakingAmountData, + TakingAmountData: e.TakingAmountData, + Predicate: e.Predicate, + MakerPermit: e.MakerPermit, + PreInteraction: e.PreInteraction, + PostInteraction: e.PostInteraction, + //hexadecimal.Trim0x(e.CustomData), // TODO Blocking custom data for now because it is breaking the cumsum method. The extension constructor will return with an error if the user provides this field. + } +} + +func (e *ExtensionFusion) GenerateSalt() (*big.Int, error) { + + // Define the maximum value (2^96 - 1) + maxValue := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 96), big.NewInt(1)) + + // Generate a random big.Int within the range [0, 2^96 - 1] + baseSalt, err := random_number_generation.BigIntMaxFunc(maxValue) + if err != nil { + return nil, err + } + + if e.isEmpty() { + return baseSalt, nil + } + + uint160Max := new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 160), big.NewInt(1)) + + extensionHash := e.Keccak256() + salt := new(big.Int).Lsh(baseSalt, 160) + salt.Or(salt, new(big.Int).And(extensionHash, uint160Max)) + + return salt, nil +} + +// isEmpty checks if the extension data is empty +func (e *ExtensionFusion) isEmpty() bool { + return *e == (ExtensionFusion{}) +} + +func DecodeExtension(data []byte) (*ExtensionFusion, error) { + orderbookExtension, err := orderbook.Decode(data) + if err != nil { + return &ExtensionFusion{}, fmt.Errorf("error decoding extension: %v", err) + } + + fusionExtension, err := FromLimitOrderExtension(orderbookExtension) + if err != nil { + return nil, fmt.Errorf("failed to convert orderbook extension to fusion extension: %v", err) + } + + return &ExtensionFusion{ + SettlementContract: fusionExtension.SettlementContract, + AuctionDetails: fusionExtension.AuctionDetails, + PostInteractionData: fusionExtension.PostInteractionData, + Asset: fusionExtension.Asset, + Permit: fusionExtension.Permit, + + MakerAssetSuffix: orderbookExtension.MakerAssetSuffix, + TakerAssetSuffix: orderbookExtension.TakerAssetSuffix, + MakingAmountData: orderbookExtension.MakingAmountData, + TakingAmountData: orderbookExtension.TakingAmountData, + Predicate: orderbookExtension.Predicate, + MakerPermit: orderbookExtension.MakerPermit, + PreInteraction: orderbookExtension.PreInteraction, + PostInteraction: orderbookExtension.PostInteraction, + }, nil +} + +func FromLimitOrderExtension(extension *orderbook.Extension) (*ExtensionFusion, error) { + + settlementContractAddress := extension.MakingAmountData[:42] + + if settlementContractAddress != extension.TakingAmountData[:42] { + return nil, fmt.Errorf("malfomed extension: settlement contract address should be the same in making and taking amount data") + } + if settlementContractAddress != extension.PostInteraction[:42] { + return nil, fmt.Errorf("malfomed extension: settlement contract address should be the same in making and post interaction") + } + + auctionDetails, err := DecodeAuctionDetails(extension.MakingAmountData[42:]) + if err != nil { + return nil, fmt.Errorf("failed to decode auction details: %v", err) + } + + postInteractionData, err := DecodeFusion(extension.PostInteraction[42:]) + if err != nil { + return nil, fmt.Errorf("failed to decode post interaction data: %v", err) + } + + fusionExtension := &ExtensionFusion{ + SettlementContract: settlementContractAddress, + AuctionDetails: auctionDetails, + PostInteractionData: &postInteractionData, + + MakerAssetSuffix: extension.MakerAssetSuffix, + TakerAssetSuffix: extension.TakerAssetSuffix, + MakingAmountData: extension.MakingAmountData, + TakingAmountData: extension.TakingAmountData, + Predicate: extension.Predicate, + MakerPermit: extension.MakerPermit, + PreInteraction: extension.PreInteraction, + PostInteraction: extension.PostInteraction, + } + + var permitInteraction *Interaction + if extension.MakerPermit != "" && extension.MakerPermit != "0x" { + permitInteraction, err = DecodeInteraction(extension.MakerPermit) + if err != nil { + return nil, fmt.Errorf("failed to decode permit interaction: %v", err) + } + + fusionExtension.Asset = permitInteraction.Target.String() + fusionExtension.Permit = permitInteraction.Data + } + + return fusionExtension, nil +} diff --git a/sdk-clients/fusionplus/fusionplus_types_extended.go b/sdk-clients/fusionplus/fusionplus_types_extended.go index 84c45e2..d1a6325 100644 --- a/sdk-clients/fusionplus/fusionplus_types_extended.go +++ b/sdk-clients/fusionplus/fusionplus_types_extended.go @@ -11,6 +11,9 @@ import ( type GetOrderByOrderHashParams struct { Hash string `url:"hash" json:"hash"` } +type GetReadyToAcceptFillsParams struct { + Hash string `url:"hash" json:"hash"` +} // GetOrderFillsByHashOutputFixed replaces the DstTokenPriceUsd and SrcTokenPriceUsd fields with string and changes Points to be an array type GetOrderFillsByHashOutputFixed struct { @@ -136,6 +139,7 @@ type Order struct { type EscrowExtensionParams struct { fusion.ExtensionParams + ExtensionParamsFusion HashLock *HashLock DstChainId float32 DstToken common.Address @@ -223,10 +227,22 @@ type AdditionalParams struct { } type Details struct { - Auction *AuctionDetails `json:"auction"` - Whitelist []AuctionWhitelistItem `json:"whitelist"` - ResolvingStartTime *big.Int `json:"resolvingStartTime"` + Auction *AuctionDetails `json:"auction"` + Fees Fees `json:"fees"` + Whitelist []AuctionWhitelistItem + ResolvingStartTime *big.Int +} + +type Fees struct { + IntFee IntegratorFee + BankFee *big.Int } + +type IntegratorFee struct { + Ratio *big.Int + Receiver common.Address +} + type AuctionWhitelistItem struct { Address common.Address /** @@ -248,6 +264,8 @@ type ExtraParams struct { type SettlementSuffixData struct { Whitelist []AuctionWhitelistItem + IntegratorFee *IntegratorFee + BankFee *big.Int ResolvingStartTime *big.Int CustomReceiver common.Address } @@ -274,3 +292,120 @@ type ExtraData struct { EnablePermit2 bool Source string } + +// PresetClassFixedFusion defines model for PresetClass. +type PresetClassFixedFusion struct { + AllowMultipleFills bool `json:"allowMultipleFills"` + AllowPartialFills bool `json:"allowPartialFills"` + AuctionDuration float32 `json:"auctionDuration"` + AuctionEndAmount string `json:"auctionEndAmount"` + AuctionStartAmount string `json:"auctionStartAmount"` + BankFee string `json:"bankFee"` + EstP float32 `json:"estP"` + ExclusiveResolver string `json:"exclusiveResolver"` // This was changed to a string from a map[string]interface{} + GasCost GasCostConfigClassFusion `json:"gasCost"` + InitialRateBump float32 `json:"initialRateBump"` + Points []AuctionPointClassFusion `json:"points"` + StartAuctionIn float32 `json:"startAuctionIn"` + TokenFee string `json:"tokenFee"` +} + +// GasCostConfigClassFusion defines model for GasCostConfigClass. +type GasCostConfigClassFusion struct { + GasBumpEstimate float32 `json:"gasBumpEstimate"` + GasPriceEstimate string `json:"gasPriceEstimate"` +} + +// AuctionPointClassFusion defines model for AuctionPointClass. +type AuctionPointClassFusion struct { + Coefficient float32 `json:"coefficient"` + Delay float32 `json:"delay"` +} + +type FeesFusion struct { + IntFee IntegratorFeeFusion + BankFee *big.Int +} + +type IntegratorFeeFusion struct { + Ratio *big.Int + Receiver common.Address +} + +type DetailsFusion struct { + Auction *AuctionDetails `json:"auction"` + Fees FeesFusion `json:"fees"` + Whitelist []AuctionWhitelistItem + ResolvingStartTime *big.Int +} + +// FusionOrderV4 defines model for FusionOrderV4. +type FusionOrderV4 struct { + // Maker Address of the account creating the order (maker). + Maker string `json:"maker"` + + // MakerAsset Identifier of the asset being offered by the maker. + MakerAsset string `json:"makerAsset"` + + // MakerTraits Includes some flags like, allow multiple fills, is partial fill allowed or not, price improvement, nonce, deadline etc. + MakerTraits string `json:"makerTraits"` + + // MakingAmount Amount of the makerAsset being offered by the maker. + MakingAmount string `json:"makingAmount"` + + // Receiver Address of the account receiving the assets (receiver), if different from maker. + Receiver string `json:"receiver"` + + // Salt Some unique value. It is necessary to be able to create limit orders with the same parameters (so that they have a different hash), Lowest 160 bits of the order salt must be equal to the lowest 160 bits of the extension hash + Salt string `json:"salt"` + + // TakerAsset Identifier of the asset being requested by the maker in exchange. + TakerAsset string `json:"takerAsset"` + + // TakingAmount Amount of the takerAsset being requested by the maker. + TakingAmount string `json:"takingAmount"` +} + +type ExtensionParamsFusion struct { + SettlementContract string + AuctionDetails *AuctionDetails + PostInteractionData *SettlementPostInteractionDataFusion + Asset string + Permit string + + MakerAssetSuffix string + TakerAssetSuffix string + Predicate string + PreInteraction string + CustomData string +} + +type SettlementSuffixDataFusion struct { + Whitelist []AuctionWhitelistItem + IntegratorFee *IntegratorFeeFusion + BankFee *big.Int + ResolvingStartTime *big.Int + CustomReceiver common.Address +} + +// ExtensionFusion represents the extension data for the Fusion order +// and should be only created using the NewExtensionFusion function +type ExtensionFusion struct { + // Raw unencoded data + SettlementContract string + AuctionDetails *AuctionDetails + PostInteractionData *SettlementPostInteractionDataFusion + Asset string + Permit string + + // Data formatted for Limit Order Extension + MakerAssetSuffix string + TakerAssetSuffix string + MakingAmountData string + TakingAmountData string + Predicate string + MakerPermit string + PreInteraction string + PostInteraction string + CustomData string +} diff --git a/sdk-clients/fusionplus/order.go b/sdk-clients/fusionplus/order.go index d402dd2..8ebb646 100644 --- a/sdk-clients/fusionplus/order.go +++ b/sdk-clients/fusionplus/order.go @@ -5,11 +5,10 @@ import ( "fmt" "math/big" "strconv" + "time" "github.com/1inch/1inch-sdk-go/common" random_number_generation "github.com/1inch/1inch-sdk-go/internal/random-number-generation" - "github.com/1inch/1inch-sdk-go/internal/times" - "github.com/1inch/1inch-sdk-go/sdk-clients/fusion" "github.com/1inch/1inch-sdk-go/sdk-clients/orderbook" geth_common "github.com/ethereum/go-ethereum/common" ) @@ -24,19 +23,16 @@ func CreateFusionPlusOrderData(quoteParams QuoterControllerGetQuoteParamsFixed, return nil, fmt.Errorf("error getting preset: %v", err) } - auctionPointsFusion := make([]fusion.AuctionPointClass, 0) + auctionPointsFusion := make([]AuctionPointClassFusion, 0) for _, point := range preset.Points { - auctionPointsFusion = append(auctionPointsFusion, fusion.AuctionPointClass{ - Coefficient: point.Coefficient, - Delay: point.Delay, - }) + auctionPointsFusion = append(auctionPointsFusion, AuctionPointClassFusion(point)) } - gasCostsFusion := fusion.GasCostConfigClass{ + gasCostsFusion := GasCostConfigClassFusion{ GasBumpEstimate: preset.GasCost.GasBumpEstimate, GasPriceEstimate: preset.GasCost.GasPriceEstimate, } - presetFusion := &fusion.PresetClassFixed{ + presetFusion := &PresetClassFixedFusion{ AllowMultipleFills: preset.AllowMultipleFills, //ExclusiveResolver: preset.ExclusiveResolver, // TODO This is not working for fusion at the moment AllowPartialFills: preset.AllowPartialFills, @@ -54,7 +50,7 @@ func CreateFusionPlusOrderData(quoteParams QuoterControllerGetQuoteParamsFixed, return nil, fmt.Errorf("error creating auction details: %v", err) } - auctionDetailsFusion, err := fusion.CreateAuctionDetails(presetFusion, 0) + auctionDetailsFusion, err := CreateAuctionDetailsFusion(presetFusion, 0) if err != nil { return nil, fmt.Errorf("error creating auction details: %v", err) } @@ -68,6 +64,28 @@ func CreateFusionPlusOrderData(quoteParams QuoterControllerGetQuoteParamsFixed, takerAsset = takerAssetWrapped.Hex() } + var takingFreeReceiver geth_common.Address + if orderParams.TakingFeeReceiver == "" { + takingFreeReceiver = geth_common.HexToAddress("0x0000000000000000000000000000000000000000") + } else { + takingFreeReceiver = geth_common.HexToAddress(orderParams.TakingFeeReceiver) + } + + fees := Fees{ + IntFee: IntegratorFee{ + Ratio: bpsToRatioFormat(quoteParams.Fee), + Receiver: takingFreeReceiver, + }, + BankFee: big.NewInt(0), + } + feesFusion := FeesFusion{ + IntFee: IntegratorFeeFusion{ + Ratio: bpsToRatioFormat(quoteParams.Fee), + Receiver: takingFreeReceiver, + }, + BankFee: big.NewInt(0), + } + whitelistAddresses := make([]AuctionWhitelistItem, 0) for _, address := range quote.Whitelist { whitelistAddresses = append(whitelistAddresses, AuctionWhitelistItem{ @@ -75,9 +93,9 @@ func CreateFusionPlusOrderData(quoteParams QuoterControllerGetQuoteParamsFixed, AllowFrom: big.NewInt(0), // TODO generating the correct list here requires checking for an exclusive resolver. This needs to be checked for later. The generated object does not see exclusive resolver correctly }) } - whitelistAddressesFusion := make([]fusion.AuctionWhitelistItem, 0) + whitelistAddressesFusion := make([]AuctionWhitelistItem, 0) for _, address := range quote.Whitelist { - whitelistAddressesFusion = append(whitelistAddressesFusion, fusion.AuctionWhitelistItem{ + whitelistAddressesFusion = append(whitelistAddressesFusion, AuctionWhitelistItem{ Address: geth_common.HexToAddress(address), AllowFrom: big.NewInt(0), // TODO generating the correct list here requires checking for an exclusive resolver. This needs to be checked for later. The generated object does not see exclusive resolver correctly }) @@ -99,10 +117,12 @@ func CreateFusionPlusOrderData(quoteParams QuoterControllerGetQuoteParamsFixed, details := Details{ Auction: auctionDetails, + Fees: fees, Whitelist: whitelistAddresses, } - detailsFusion := fusion.Details{ + detailsFusion := DetailsFusion{ Auction: auctionDetailsFusion, + Fees: feesFusion, Whitelist: whitelistAddressesFusion, } @@ -114,7 +134,7 @@ func CreateFusionPlusOrderData(quoteParams QuoterControllerGetQuoteParamsFixed, OrderExpirationDelay: 0, Source: "", } - extraParamsFusion := fusion.ExtraParams{ + extraParamsFusion := ExtraParams{ Nonce: nonce, Permit: orderParams.Permit, AllowPartialFills: preset.AllowPartialFills, @@ -123,7 +143,7 @@ func CreateFusionPlusOrderData(quoteParams QuoterControllerGetQuoteParamsFixed, Source: "", } - makerTraitsFusion, err := fusion.CreateMakerTraits(detailsFusion, extraParamsFusion) + makerTraitsFusion, err := CreateMakerTraitsFusion(detailsFusion, extraParamsFusion) if err != nil { return nil, fmt.Errorf("error creating maker traits: %v", err) } @@ -136,7 +156,7 @@ func CreateFusionPlusOrderData(quoteParams QuoterControllerGetQuoteParamsFixed, TakerAsset: takerAsset, TakingAmount: preset.AuctionEndAmount, } - orderInfoFusion := fusion.FusionOrderV4{ + orderInfoFusion := FusionOrderV4{ Maker: quoteParams.WalletAddress, MakerAsset: quoteParams.SrcTokenAddress, MakingAmount: quoteParams.Amount, @@ -165,15 +185,13 @@ func CreateFusionPlusOrderData(quoteParams QuoterControllerGetQuoteParamsFixed, if err != nil { return nil, fmt.Errorf("error creating post interaction data: %v", err) } - - // TODO passing nil in for the whitelist until Fusion+ is updated - postInteractionDataFusion, err := fusion.CreateSettlementPostInteractionData(detailsFusion, nil, orderInfoFusion) + postInteractionDataFusion, err := CreateSettlementPostInteractionDataFusion(detailsFusion, orderInfoFusion) if err != nil { return nil, fmt.Errorf("error creating post interaction data: %v", err) } extension, err := NewEscrowExtension(EscrowExtensionParams{ - ExtensionParams: fusion.ExtensionParams{ + ExtensionParamsFusion: ExtensionParamsFusion{ SettlementContract: quote.SrcEscrowFactory, PostInteractionData: postInteractionDataFusion, AuctionDetails: auctionDetailsFusion, @@ -210,16 +228,28 @@ func CreateFusionPlusOrderData(quoteParams QuoterControllerGetQuoteParamsFixed, return nil, fmt.Errorf("error converting extension to orderbook extension: %v", err) } + extensionEncoded, err := extensionOrderbook.Encode() + if err != nil { + return nil, fmt.Errorf("error encoding extension: %v", err) + } + + salt, err := orderbook.GenerateSalt(extensionEncoded, nil) + if err != nil { + return nil, fmt.Errorf("error generating salt: %v", err) + } + limitOrder, err := orderbook.CreateLimitOrderMessage(orderbook.CreateOrderParams{ - Wallet: wallet, - MakerTraits: makerTraitsFusion, - Extension: *extensionOrderbook, - Maker: orderInfo.Maker, - MakerAsset: orderInfo.MakerAsset, - TakerAsset: orderInfo.TakerAsset, - TakingAmount: orderInfo.TakingAmount, - MakingAmount: orderInfo.MakingAmount, - Taker: orderInfo.Receiver, + Wallet: wallet, + MakerTraits: makerTraitsFusion, + Extension: *extensionOrderbook, + ExtensionEncoded: extensionEncoded, + Salt: salt, + Maker: orderInfo.Maker, + MakerAsset: orderInfo.MakerAsset, + TakerAsset: orderInfo.TakerAsset, + TakingAmount: orderInfo.TakingAmount, + MakingAmount: orderInfo.MakingAmount, + Taker: orderInfo.Receiver, }, chainId) if err != nil { return nil, fmt.Errorf("error creating limit order message: %v", err) @@ -250,6 +280,13 @@ func GetPreset(presets QuotePresets, presetType GetQuoteOutputRecommendedPreset) return nil, fmt.Errorf("unknown preset type: %v", presetType) } +var CalcAuctionStartTimeFunc func(uint32, uint32) uint32 = CalcAuctionStartTime + +func CalcAuctionStartTime(startAuctionIn uint32, additionalWaitPeriod uint32) uint32 { + currentTime := time.Now().Unix() + return uint32(currentTime) + additionalWaitPeriod + startAuctionIn +} + func CreateAuctionDetails(preset *Preset, additionalWaitPeriod float32) (*AuctionDetails, error) { pointsFixed := make([]AuctionPointClassFixed, 0) for _, point := range preset.Points { @@ -270,7 +307,7 @@ func CreateAuctionDetails(preset *Preset, additionalWaitPeriod float32) (*Auctio } return &AuctionDetails{ - StartTime: times.CalculateAuctionStartTime(uint32(preset.StartAuctionIn), uint32(additionalWaitPeriod)), + StartTime: CalcAuctionStartTimeFunc(uint32(preset.StartAuctionIn), uint32(additionalWaitPeriod)), Duration: uint32(preset.AuctionDuration), InitialRateBump: uint32(preset.InitialRateBump), Points: pointsFixed, @@ -278,13 +315,21 @@ func CreateAuctionDetails(preset *Preset, additionalWaitPeriod float32) (*Auctio }, nil } +var timeNow func() int64 = GetCurrentTime + +func GetCurrentTime() int64 { + return time.Now().Unix() +} + func CreateSettlementPostInteractionData(details Details, orderInfo CrossChainOrderDto) (*SettlementPostInteractionData, error) { resolverStartTime := details.ResolvingStartTime if details.ResolvingStartTime == nil || details.ResolvingStartTime.Cmp(big.NewInt(0)) == 0 { - resolverStartTime = big.NewInt(times.Now()) + resolverStartTime = big.NewInt(timeNow()) } return NewSettlementPostInteractionData(SettlementSuffixData{ Whitelist: details.Whitelist, + IntegratorFee: &details.Fees.IntFee, + BankFee: details.Fees.BankFee, ResolvingStartTime: resolverStartTime, CustomReceiver: geth_common.HexToAddress(orderInfo.Receiver), }) @@ -354,3 +399,69 @@ func bpsToRatioFormat(bps *big.Int) *big.Int { return bps.Mul(bps, bpsToRatioNumber) } + +func CreateAuctionDetailsFusion(preset *PresetClassFixedFusion, additionalWaitPeriod float32) (*AuctionDetails, error) { + pointsFixed := make([]AuctionPointClassFixed, 0) + for _, point := range preset.Points { + pointsFixed = append(pointsFixed, AuctionPointClassFixed{ + Coefficient: uint32(point.Coefficient), + Delay: uint16(point.Delay), + }) + } + + gasPriceEstimateFixed, err := strconv.ParseUint(preset.GasCost.GasPriceEstimate, 10, 32) + if err != nil { + return nil, fmt.Errorf("error parsing gas price estimate: %v", err) + } + + gasCostFixed := GasCostConfigClassFixed{ + GasBumpEstimate: uint32(preset.GasCost.GasBumpEstimate), + GasPriceEstimate: uint32(gasPriceEstimateFixed), + } + + return &AuctionDetails{ + StartTime: CalcAuctionStartTimeFunc(uint32(preset.StartAuctionIn), uint32(additionalWaitPeriod)), + Duration: uint32(preset.AuctionDuration), + InitialRateBump: uint32(preset.InitialRateBump), + Points: pointsFixed, + GasCost: gasCostFixed, + }, nil +} + +func CreateMakerTraitsFusion(details DetailsFusion, extraParams ExtraParams) (*orderbook.MakerTraits, error) { + deadline := details.Auction.StartTime + details.Auction.Duration + extraParams.OrderExpirationDelay + makerTraitParms := orderbook.MakerTraitsParams{ + Expiry: int64(deadline), + AllowPartialFills: extraParams.AllowPartialFills, + AllowMultipleFills: extraParams.AllowMultipleFills, + HasPostInteraction: true, + UnwrapWeth: extraParams.unwrapWeth, + UsePermit2: extraParams.EnablePermit2, + HasExtension: true, + Nonce: extraParams.Nonce.Int64(), + } + makerTraits, err := orderbook.NewMakerTraits(makerTraitParms) + if err != nil { + return nil, fmt.Errorf("error creating maker traits: %v", err) + } + if makerTraits.IsBitInvalidatorMode() { + if extraParams.Nonce == nil || extraParams.Nonce.Cmp(big.NewInt(0)) == 0 { + return nil, errors.New("nonce required when partial fill or multiple fill disallowed") + } + } + return makerTraits, nil +} + +func CreateSettlementPostInteractionDataFusion(details DetailsFusion, orderInfo FusionOrderV4) (*SettlementPostInteractionDataFusion, error) { + resolverStartTime := details.ResolvingStartTime + if details.ResolvingStartTime == nil || details.ResolvingStartTime.Cmp(big.NewInt(0)) == 0 { + resolverStartTime = big.NewInt(timeNow()) + } + return NewSettlementPostInteractionDataFusion(SettlementSuffixDataFusion{ + Whitelist: details.Whitelist, + IntegratorFee: &details.Fees.IntFee, + BankFee: details.Fees.BankFee, + ResolvingStartTime: resolverStartTime, + CustomReceiver: geth_common.HexToAddress(orderInfo.Receiver), + }) +} diff --git a/sdk-clients/fusionplus/settlementpostinteractiondatafusion.go b/sdk-clients/fusionplus/settlementpostinteractiondatafusion.go new file mode 100644 index 0000000..1fd48f3 --- /dev/null +++ b/sdk-clients/fusionplus/settlementpostinteractiondatafusion.go @@ -0,0 +1,221 @@ +package fusionplus + +import ( + "encoding/hex" + "errors" + "fmt" + "math/big" + "sort" + "strings" + + "github.com/1inch/1inch-sdk-go/internal/bytesbuilder" + "github.com/1inch/1inch-sdk-go/internal/bytesiterator" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +type SettlementPostInteractionDataFusion struct { + Whitelist []WhitelistItem + IntegratorFee *IntegratorFeeFusion + BankFee *big.Int + ResolvingStartTime *big.Int + CustomReceiver common.Address +} + +func NewSettlementPostInteractionDataFusion(data SettlementSuffixDataFusion) (*SettlementPostInteractionDataFusion, error) { + if len(data.Whitelist) == 0 { + return nil, errors.New("whitelist cannot be empty") + } + + sumDelay := big.NewInt(0) + whitelist := make([]WhitelistItem, len(data.Whitelist)) + + // Transform timestamps to cumulative delays + sort.Slice(data.Whitelist, func(i, j int) bool { + return data.Whitelist[i].AllowFrom.Cmp(data.Whitelist[j].AllowFrom) < 0 + }) + + for i, d := range data.Whitelist { + allowFrom := d.AllowFrom + if d.AllowFrom.Cmp(data.ResolvingStartTime) < 0 { + allowFrom = data.ResolvingStartTime + } + + zero := big.NewInt(0) + delay := new(big.Int).Sub(allowFrom, data.ResolvingStartTime) + delay.Sub(delay, sumDelay) + // If the resulting value of delay is zero, set it to a fresh big.Int of value zero (for comparisons in tests) + if delay.Cmp(zero) == 0 { + delay = zero + } + whitelist[i] = WhitelistItem{ + AddressHalf: strings.ToLower(d.Address.Hex())[len(d.Address.Hex())-20:], + Delay: delay, + } + + sumDelay.Add(sumDelay, whitelist[i].Delay) + + if whitelist[i].Delay.Cmp(uint16Max) >= 0 { + return nil, fmt.Errorf("delay too big - %d must be less than %d", whitelist[i].Delay, uint16Max) + } + } + + return &SettlementPostInteractionDataFusion{ + Whitelist: whitelist, + IntegratorFee: data.IntegratorFee, + BankFee: data.BankFee, + ResolvingStartTime: data.ResolvingStartTime, + CustomReceiver: data.CustomReceiver, + }, nil +} + +func DecodeFusion(data string) (SettlementPostInteractionDataFusion, error) { + bytes, err := hexutil.Decode(data) + if err != nil { + return SettlementPostInteractionDataFusion{}, errors.New("invalid hex string") + } + + flags := big.NewInt(int64(bytes[len(bytes)-1])) + bytesWithoutFlags := bytes[:len(bytes)-1] + + iter := bytesiterator.New(bytesWithoutFlags) + var bankFee *big.Int + var integratorFee *IntegratorFeeFusion + var customReceiver common.Address + + if flags.Bit(0) == 1 { + bankFee, err = iter.NextUint32() + if err != nil { + return SettlementPostInteractionDataFusion{}, err + } + } + + if flags.Bit(1) == 1 { + + ratio, err := iter.NextUint16() + if err != nil { + return SettlementPostInteractionDataFusion{}, err + } + + receiver, err := iter.NextUint160() + if err != nil { + return SettlementPostInteractionDataFusion{}, err + } + + integratorFee = &IntegratorFeeFusion{ + Ratio: ratio, + Receiver: common.HexToAddress(receiver.Text(16)), + } + + if flags.Bit(2) == 1 { + + customReceiverRaw, err := iter.NextUint160() + if err != nil { + return SettlementPostInteractionDataFusion{}, err + } + + customReceiver = common.HexToAddress(customReceiverRaw.Text(16)) + } + } + + resolvingStartTime, err := iter.NextUint32() + if err != nil { + return SettlementPostInteractionDataFusion{}, err + } + var whitelist []WhitelistItem + + for !iter.IsEmpty() { + addressHalfRaw, err := iter.NextBytes(10) + if err != nil { + return SettlementPostInteractionDataFusion{}, err + } + addressHalf := hex.EncodeToString(addressHalfRaw) + delay, err := iter.NextUint16() + if err != nil { + return SettlementPostInteractionDataFusion{}, err + } + whitelist = append(whitelist, WhitelistItem{ + AddressHalf: addressHalf, + Delay: delay, + }) + } + + return SettlementPostInteractionDataFusion{ + IntegratorFee: integratorFee, + BankFee: bankFee, + ResolvingStartTime: resolvingStartTime, + Whitelist: whitelist, + CustomReceiver: customReceiver, + }, nil +} + +func (spid SettlementPostInteractionDataFusion) Encode() (string, error) { + bitMask := big.NewInt(0) + bytes := bytesbuilder.New() + + if spid.BankFee != nil && spid.BankFee.Cmp(big.NewInt(0)) != 0 { + bitMask.SetBit(bitMask, 0, 1) + bytes.AddUint32(spid.BankFee) + } + + if spid.IntegratorFee != nil && spid.IntegratorFee.Ratio.Cmp(big.NewInt(0)) != 0 { + bitMask.SetBit(bitMask, 1, 1) + bytes.AddUint16(spid.IntegratorFee.Ratio) + bytes.AddAddress(spid.IntegratorFee.Receiver) + + // TODO this check is probably not good enough + if spid.CustomReceiver.Hex() != "0x0000000000000000000000000000000000000000" { + bitMask.SetBit(bitMask, 2, 1) + bytes.AddAddress(spid.CustomReceiver) + } + } + + bytes.AddUint32(spid.ResolvingStartTime) + + for _, wl := range spid.Whitelist { + err := bytes.AddBytes(wl.AddressHalf) + if err != nil { + return "", err + } + bytes.AddUint16(wl.Delay) + } + + bitMask.Or(bitMask, big.NewInt(int64(len(spid.Whitelist)<<3))) + bytes.AddUint8(uint8(bitMask.Int64())) + + output := fmt.Sprintf("0x%s", bytes.AsHex()) + + return output, nil +} + +func (spid SettlementPostInteractionDataFusion) CanExecuteAt(executor common.Address, executionTime *big.Int) bool { + addressHalf := executor.Hex()[len(executor.Hex())-20:] + + allowedFrom := spid.ResolvingStartTime + + for _, whitelist := range spid.Whitelist { + allowedFrom.Add(allowedFrom, whitelist.Delay) + + if addressHalf == whitelist.AddressHalf { + return executionTime.Cmp(allowedFrom) >= 0 + } else if executionTime.Cmp(allowedFrom) < 0 { + return false + } + } + + return false +} + +func (spid SettlementPostInteractionDataFusion) IsExclusiveResolver(wallet common.Address) bool { + addressHalf := wallet.Hex()[len(wallet.Hex())-20:] + + if len(spid.Whitelist) == 1 { + return addressHalf == spid.Whitelist[0].AddressHalf + } + + if spid.Whitelist[0].Delay.Cmp(spid.Whitelist[1].Delay) == 0 { + return false + } + + return addressHalf == spid.Whitelist[0].AddressHalf +}