From 7e67fd7c75a8ff176e4e3b547b205f2e8c4617ad Mon Sep 17 00:00:00 2001 From: Marcin Gorzynski Date: Fri, 9 Feb 2024 15:33:29 +0100 Subject: [PATCH] new intent struct --- intentv1/intent.gen.go | 226 +++++++++++++++++++++++++++++ intentv1/intent.go | 4 + intentv1/intent.ridl | 103 +++++++++++++ intentv1/intent_ext.go | 192 ++++++++++++++++++++++++ intentv1/intent_transaction_ext.go | 1 + intentv1/intent_typed.go | 92 ++++++++++++ intentv1/intent_typed_test.go | 165 +++++++++++++++++++++ intentv1/sign.go | 77 ++++++++++ 8 files changed, 860 insertions(+) create mode 100644 intentv1/intent.gen.go create mode 100644 intentv1/intent.go create mode 100644 intentv1/intent.ridl create mode 100644 intentv1/intent_ext.go create mode 100644 intentv1/intent_transaction_ext.go create mode 100644 intentv1/intent_typed.go create mode 100644 intentv1/intent_typed_test.go create mode 100644 intentv1/sign.go diff --git a/intentv1/intent.gen.go b/intentv1/intent.gen.go new file mode 100644 index 0000000..b686844 --- /dev/null +++ b/intentv1/intent.gen.go @@ -0,0 +1,226 @@ +// sequence-waas-intents v0.1.0 b0dc5c3bceb25d04f130e1807829bd8110a0a3e4 +// -- +// Code generated by webrpc-gen@v0.14.0-dev with golang generator. DO NOT EDIT. +// +// webrpc-gen -schema=intent.ridl -target=golang -pkg=intents -client -out=./intent.gen.go +package intents + +import ( + "context" + "errors" + "fmt" + "net/http" +) + +// WebRPC description and code-gen version +func WebRPCVersion() string { + return "v1" +} + +// Schema version of your RIDL schema +func WebRPCSchemaVersion() string { + return "v0.1.0" +} + +// Schema hash generated from your RIDL schema +func WebRPCSchemaHash() string { + return "b0dc5c3bceb25d04f130e1807829bd8110a0a3e4" +} + +// +// Types +// + +type Signature struct { + SessionId string `json:"sessionId"` + Signature string `json:"signature"` +} + +type Intent struct { + Version string `json:"version"` + Name string `json:"name"` + Expires uint64 `json:"expires"` + Issued uint64 `json:"issued"` + Data interface{} `json:"data"` + Signatures []*Signature `json:"signatures"` +} + +type SessionPacketProof struct { + Email *string `json:"email"` + IdToken *string `json:"idToken"` +} + +type IntentDataOpenSession struct { + SessionId string `json:"sessionId"` + Proof *SessionPacketProof `json:"proof"` +} + +type IntentDataCloseSession struct { + SessionId string `json:"sessionId"` +} + +type IntentDataValidateSession struct { + SessionId string `json:"sessionId"` + DeviceMetadata string `json:"deviceMetadata"` +} + +type IntentDataFinishValidateSession struct { + SessionId string `json:"sessionId"` + Salt string `json:"salt"` + Challenge string `json:"challenge"` +} + +type IntentDataListSessions struct { +} + +type IntentDataGetSession struct { + SessionId string `json:"sessionId"` +} + +type IntentDataSign struct { + Network string `json:"network"` + Message string `json:"message"` +} + +type IntentDataTransaction struct { + Network string `json:"network"` + Identifier string `json:"identifier"` + Transactions []interface{} `json:"transactions"` +} + +type TransactionRaw struct { + Type string `json:"type"` + To string `json:"to"` + Value string `json:"value"` + Data string `json:"data"` +} + +type TransactionERC20 struct { + Type string `json:"type"` + Token string `json:"token"` + To string `json:"to"` + Value string `json:"value"` +} + +type TransactionERC721 struct { + Type string `json:"type"` + Token string `json:"token"` + To string `json:"to"` + Id string `json:"id"` + Safe *bool `json:"safe"` + Data *string `json:"data"` +} + +type TransactionERC1155Value struct { + Id string `json:"id"` + Amount string `json:"amount"` +} + +type TransactionERC1155 struct { + Type string `json:"type"` + Token string `json:"token"` + To string `json:"to"` + Vals []*TransactionERC1155Value `json:"vals"` + Data *string `json:"data"` +} + +type IntentResponse struct { + Code string `json:"code"` + Data interface{} `json:"data"` +} + +// +// Helpers +// + +type contextKey struct { + name string +} + +func (k *contextKey) String() string { + return "webrpc context value " + k.name +} + +var ( + HTTPClientRequestHeadersCtxKey = &contextKey{"HTTPClientRequestHeaders"} + HTTPRequestCtxKey = &contextKey{"HTTPRequest"} + + ServiceNameCtxKey = &contextKey{"ServiceName"} + + MethodNameCtxKey = &contextKey{"MethodName"} +) + +func ServiceNameFromContext(ctx context.Context) string { + service, _ := ctx.Value(ServiceNameCtxKey).(string) + return service +} + +func MethodNameFromContext(ctx context.Context) string { + method, _ := ctx.Value(MethodNameCtxKey).(string) + return method +} + +func RequestFromContext(ctx context.Context) *http.Request { + r, _ := ctx.Value(HTTPRequestCtxKey).(*http.Request) + return r +} + +// +// Errors +// + +type WebRPCError struct { + Name string `json:"error"` + Code int `json:"code"` + Message string `json:"msg"` + Cause string `json:"cause,omitempty"` + HTTPStatus int `json:"status"` + cause error +} + +var _ error = WebRPCError{} + +func (e WebRPCError) Error() string { + if e.cause != nil { + return fmt.Sprintf("%s %d: %s: %v", e.Name, e.Code, e.Message, e.cause) + } + return fmt.Sprintf("%s %d: %s", e.Name, e.Code, e.Message) +} + +func (e WebRPCError) Is(target error) bool { + if rpcErr, ok := target.(WebRPCError); ok { + return rpcErr.Code == e.Code + } + return errors.Is(e.cause, target) +} + +func (e WebRPCError) Unwrap() error { + return e.cause +} + +func (e WebRPCError) WithCause(cause error) WebRPCError { + err := e + err.cause = cause + err.Cause = cause.Error() + return err +} + +// Deprecated: Use .WithCause() method on WebRPCError. +func ErrorWithCause(rpcErr WebRPCError, cause error) WebRPCError { + return rpcErr.WithCause(cause) +} + +// Webrpc errors +var ( + ErrWebrpcEndpoint = WebRPCError{Code: 0, Name: "WebrpcEndpoint", Message: "endpoint error", HTTPStatus: 400} + ErrWebrpcRequestFailed = WebRPCError{Code: -1, Name: "WebrpcRequestFailed", Message: "request failed", HTTPStatus: 400} + ErrWebrpcBadRoute = WebRPCError{Code: -2, Name: "WebrpcBadRoute", Message: "bad route", HTTPStatus: 404} + ErrWebrpcBadMethod = WebRPCError{Code: -3, Name: "WebrpcBadMethod", Message: "bad method", HTTPStatus: 405} + ErrWebrpcBadRequest = WebRPCError{Code: -4, Name: "WebrpcBadRequest", Message: "bad request", HTTPStatus: 400} + ErrWebrpcBadResponse = WebRPCError{Code: -5, Name: "WebrpcBadResponse", Message: "bad response", HTTPStatus: 500} + ErrWebrpcServerPanic = WebRPCError{Code: -6, Name: "WebrpcServerPanic", Message: "server panic", HTTPStatus: 500} + ErrWebrpcInternalError = WebRPCError{Code: -7, Name: "WebrpcInternalError", Message: "internal error", HTTPStatus: 500} + ErrWebrpcClientDisconnected = WebRPCError{Code: -8, Name: "WebrpcClientDisconnected", Message: "client disconnected", HTTPStatus: 400} + ErrWebrpcStreamLost = WebRPCError{Code: -9, Name: "WebrpcStreamLost", Message: "stream lost", HTTPStatus: 400} + ErrWebrpcStreamFinished = WebRPCError{Code: -10, Name: "WebrpcStreamFinished", Message: "stream finished", HTTPStatus: 200} +) diff --git a/intentv1/intent.go b/intentv1/intent.go new file mode 100644 index 0000000..0b78252 --- /dev/null +++ b/intentv1/intent.go @@ -0,0 +1,4 @@ +// Server +//go:generate go run github.com/webrpc/webrpc/cmd/webrpc-gen -schema=intent.ridl -target=golang -pkg=intents -client -out=./intent.gen.go + +package intents diff --git a/intentv1/intent.ridl b/intentv1/intent.ridl new file mode 100644 index 0000000..208f83d --- /dev/null +++ b/intentv1/intent.ridl @@ -0,0 +1,103 @@ +webrpc = v1 + +name = sequence-waas-intents +version = v0.1.0 + +struct Signature + - sessionId: string + - signature: string + +# no way to generate string enums +# enum IntentName: uint8 +# - openSession +# - closeSession +# - validateSession +# - finishValidateSession +# - listSessions +# - getSession +# - sign +# - transaction + +struct Intent + - version: string + - name: string + - expires: uint64 + - issued: uint64 + - data: any + - signatures: []Signature + +struct SessionPacketProof + - email?: string + - idToken?: string + +struct IntentDataOpenSession + - sessionId: string + - proof: SessionPacketProof + +struct IntentDataCloseSession + - sessionId: string + +struct IntentDataValidateSession + - sessionId: string + - deviceMetadata: string + +struct IntentDataFinishValidateSession + - sessionId: string + - salt: string + - challenge: string + +struct IntentDataListSessions + +struct IntentDataGetSession + - sessionId: string + +struct IntentDataSign + - network: string + - message: string + +struct IntentDataTransaction + - network: string + - identifier: string + - transactions: []any + +# no way to generate string enums +#enum TransactionType: uint8 +# - transaction +# - erc20send +# - erc721send +# - erc1155send + +struct TransactionRaw + - type: string + - to: string + - value: string + - data: string + +struct TransactionERC20 + - type: string + - token: string + - to: string + - value: string + +struct TransactionERC721 + - type: string + - token: string + - to: string + - id: string + - safe?: bool + - data?: string + +struct TransactionERC1155Value + - id: string + - amount: string + +struct TransactionERC1155 + - type: string + - token: string + - to: string + - vals: []TransactionERC1155Value + - data?: string + +struct IntentResponse + - code: string + - data: any diff --git a/intentv1/intent_ext.go b/intentv1/intent_ext.go new file mode 100644 index 0000000..a6fe6ce --- /dev/null +++ b/intentv1/intent_ext.go @@ -0,0 +1,192 @@ +package intents + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/sha256" + "fmt" + "math/big" + "strings" + "time" + + "github.com/0xsequence/ethkit/go-ethereum/common" + "github.com/0xsequence/ethkit/go-ethereum/crypto" + "github.com/davecgh/go-spew/spew" + "github.com/gibson042/canonicaljson-go" +) + +const IntentValidTimeInSec = 60 +const IntentAllowedTimeDriftInSec = 5 + +type KeyType int + +const ( + KeyTypeSECP256K1 KeyType = iota + KeyTypeSECP256R1 + KeyTypeUnknown +) + +func (intent *Intent) Hash() ([]byte, error) { + // copy intent and remove signatures + var intentCopy = *intent + intentCopy.Signatures = nil + + // Convert packet to bytes + packetBytes, err := canonicaljson.Marshal(intentCopy) + if err != nil { + return nil, err + } + + // Calculate keccak256 hash + return crypto.Keccak256(packetBytes), nil +} + +func (intent *Intent) IsValid() error { + if len(intent.Signatures) == 0 { + return fmt.Errorf("no signatures") + } + + // check if the intent is expired + if intent.Expires+IntentAllowedTimeDriftInSec < uint64(time.Now().Unix()) { + return fmt.Errorf("intent expired") + } + + // check if the intent is issued in the future + if intent.Issued-IntentAllowedTimeDriftInSec > uint64(time.Now().Unix()) { + return fmt.Errorf("intent issued in the future") + } + + // check if at least one signature is valid + if len(intent.Signers()) == 0 { + return fmt.Errorf("invalid signature") + } + + // the intent is valid + return nil +} + +func (intent *Intent) keyType(sessionId string) KeyType { + // handle empty session ids + if len(sessionId) == 0 { + return KeyTypeUnknown + } + + // handle old session ids + if len(sessionId) == 42 { + return KeyTypeSECP256K1 + } + + // handle key typed session ids + sessionIdBytes := common.FromHex(sessionId) + return KeyType(sessionIdBytes[0]) +} + +func (intent *Intent) isValidSignature(sessionId string, signature string) bool { + switch intent.keyType(sessionId) { + case KeyTypeSECP256K1: + return intent.isValidSignatureP256K1(sessionId, signature) + case KeyTypeSECP256R1: + return intent.isValidSignatureP256R1(sessionId, signature) + default: + return false + } +} + +// isValidSignatureP256K1 checks if the signature is valid for the given secp256k1 session +func (intent *Intent) isValidSignatureP256K1(sessionAddress string, signature string) bool { + // validate session address and signature + if !strings.HasPrefix(signature, "0x") || !strings.HasPrefix(sessionAddress, "0x") { + // invalid params + return false + } + + // validate session address + sessionAddressBytes := common.FromHex(sessionAddress) + if len(sessionAddressBytes) != 21 && len(sessionAddressBytes) != 20 { + // invalid session address + return false + } + + // validate signature + sigBytes := common.FromHex(signature) + if len(sigBytes) != 65 { + // invalid signature + return false + } + + // handle typed session address + if len(sessionAddressBytes) == 21 { + sessionAddressBytes = sessionAddressBytes[1:] + } + + sessionAddress = fmt.Sprintf("0x%s", common.Bytes2Hex(sessionAddressBytes)) + + // Get hash of the packet + hash, err := intent.Hash() + if err != nil { + return false + } + + // Add Ethereum prefix to the hash + prefixedHash := crypto.Keccak256Hash([]byte(fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(hash), hash))) + + if sigBytes[64] == 27 || sigBytes[64] == 28 { + sigBytes[64] -= 27 + } + + // Recover the public key from the signature + pubKey, err := crypto.Ecrecover(prefixedHash.Bytes(), sigBytes) + if err != nil { + return false + } + + addr := common.BytesToAddress(crypto.Keccak256(pubKey[1:])[12:]) + + // Check if the recovered address matches the session address + return strings.ToLower(addr.Hex()) == strings.ToLower(sessionAddress) +} + +// isValidSignatureP256R1 checks if the signature is valid for the given secp256r1 session +func (intent *Intent) isValidSignatureP256R1(publicKey string, signature string) bool { + publicKeyBuff := common.FromHex(publicKey)[1:] + + // public key + // TODO: check if can use ecdh instead of unmarshal + // NOTE: no way to convert ecdh pub key into elliptic pub key? + x, y := elliptic.Unmarshal(elliptic.P256(), publicKeyBuff) + if x == nil || y == nil { + return false + } + + pub := ecdsa.PublicKey{ + Curve: elliptic.P256(), + X: x, + Y: y, + } + + spew.Dump(pub) + + // message hash + messageHash, _ := intent.Hash() + messageHash2 := sha256.Sum256(messageHash) + + // signature + signatureBytes := common.FromHex(signature) + if len(signatureBytes) != 64 { + return false + } + + r := new(big.Int).SetBytes(signatureBytes[:32]) + s := new(big.Int).SetBytes(signatureBytes[32:64]) + return ecdsa.Verify(&pub, messageHash2[:], r, s) +} + +func (intent *Intent) Signers() []string { + var signers []string + for _, signature := range intent.Signatures { + if intent.isValidSignature(signature.SessionId, signature.Signature) { + signers = append(signers, signature.SessionId) + } + } + return signers +} diff --git a/intentv1/intent_transaction_ext.go b/intentv1/intent_transaction_ext.go new file mode 100644 index 0000000..edd7b67 --- /dev/null +++ b/intentv1/intent_transaction_ext.go @@ -0,0 +1 @@ +package intents diff --git a/intentv1/intent_typed.go b/intentv1/intent_typed.go new file mode 100644 index 0000000..55d3319 --- /dev/null +++ b/intentv1/intent_typed.go @@ -0,0 +1,92 @@ +package intents + +import ( + "encoding/json" + "fmt" + "time" +) + +func IntentDataTypeToName[T any](t *T) string { + var data any = t + switch data.(type) { + case *IntentDataOpenSession: + return "openSession" + case *IntentDataCloseSession: + return "closeSession" + case *IntentDataValidateSession: + return "validateSession" + case *IntentDataFinishValidateSession: + return "finishValidateSession" + case *IntentDataListSessions: + return "listSessions" + case *IntentDataGetSession: + return "getSession" + case *IntentDataSign: + return "sign" + case *IntentDataTransaction: + return "transaction" + default: + return "" + } +} + +type IntentTyped[T any] struct { + Intent + Data T +} + +func NewIntentTyped[T any](data T) *IntentTyped[T] { + return &IntentTyped[T]{ + Intent: Intent{ + Version: "1", + Expires: uint64(time.Now().Unix()) + IntentValidTimeInSec, + Issued: uint64(time.Now().Unix()), + Name: IntentDataTypeToName(&data), + Data: data, + }, + Data: data, + } +} + +func NewIntentTypedFromIntent[T any](intent *Intent) (*IntentTyped[T], error) { + switch intent.Data.(type) { + case T: + return &IntentTyped[T]{ + Intent: *intent, + Data: intent.Data.(T), + }, nil + case map[string]any: + data := intent.Data.(map[string]any) + + // convert to json + dataJSON, err := json.Marshal(data) + if err != nil { + return nil, err + } + + // convert to typed data + var typedData T + err = json.Unmarshal(dataJSON, &typedData) + if err != nil { + return nil, err + } + + // check if intent name and data type match + if IntentDataTypeToName(&typedData) != intent.Name { + return nil, fmt.Errorf("intent name and data type mismatch") + } + + return &IntentTyped[T]{ + Intent: *intent, + Data: typedData, + }, nil + default: + return nil, fmt.Errorf("invalid intent data type") + } +} + +func (i *IntentTyped[T]) ToIntent() *Intent { + var intentCopy = i.Intent + intentCopy.Data = i.Data + return &intentCopy +} diff --git a/intentv1/intent_typed_test.go b/intentv1/intent_typed_test.go new file mode 100644 index 0000000..273ad66 --- /dev/null +++ b/intentv1/intent_typed_test.go @@ -0,0 +1,165 @@ +package intents + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "testing" + + "github.com/0xsequence/ethkit/ethwallet" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestIntentNewIntentTyped(t *testing.T) { + t.Run("openSession", func(t *testing.T) { + intent := NewIntentTyped(IntentDataOpenSession{SessionId: "0x1234"}) + assert.Equal(t, "openSession", intent.Name) + }) + + t.Run("closeSession", func(t *testing.T) { + intent := NewIntentTyped(IntentDataCloseSession{SessionId: "0x1234"}) + assert.Equal(t, "closeSession", intent.Name) + }) + + t.Run("validateSession", func(t *testing.T) { + intent := NewIntentTyped(IntentDataValidateSession{SessionId: "0x1234"}) + assert.Equal(t, "validateSession", intent.Name) + }) + + t.Run("finishValidateSession", func(t *testing.T) { + intent := NewIntentTyped(IntentDataFinishValidateSession{SessionId: "0x1234"}) + assert.Equal(t, "finishValidateSession", intent.Name) + }) + + t.Run("listSessions", func(t *testing.T) { + intent := NewIntentTyped(IntentDataListSessions{}) + assert.Equal(t, "listSessions", intent.Name) + }) + + t.Run("getSession", func(t *testing.T) { + intent := NewIntentTyped(IntentDataGetSession{SessionId: "0x1234"}) + assert.Equal(t, "getSession", intent.Name) + }) + + t.Run("sign", func(t *testing.T) { + intent := NewIntentTyped(IntentDataSign{Network: "ethereum", Message: "0x1234"}) + assert.Equal(t, "sign", intent.Name) + }) + + t.Run("transaction", func(t *testing.T) { + intent := NewIntentTyped(IntentDataTransaction{}) + assert.Equal(t, "transaction", intent.Name) + }) + + t.Run("unknown", func(t *testing.T) { + intent := NewIntentTyped(map[string]interface{}{}) + assert.Equal(t, "", intent.Name) + }) +} + +func TestIntentNewIntentTypedFromIntent(t *testing.T) { + t.Run("openSession", func(t *testing.T) { + intent := Intent{ + Version: "1", + Name: "openSession", + Data: map[string]interface{}{"sessionId": "0x1234"}, + } + + intentTyped, err := NewIntentTypedFromIntent[IntentDataOpenSession](&intent) + assert.NoError(t, err) + assert.Equal(t, "openSession", intentTyped.Name) + }) + + t.Run("openSessionNameMismatch", func(t *testing.T) { + intent := Intent{ + Version: "1", + Name: "openSession2", + Data: map[string]interface{}{"sessionId": "0x1234"}, + } + + _, err := NewIntentTypedFromIntent[IntentDataCloseSession](&intent) + assert.ErrorContains(t, err, "mismatch") + }) +} + +func TestIntentIsValid(t *testing.T) { + signer, err := ethwallet.NewWalletFromRandomEntropy() + require.NoError(t, err) + + t.Run("valid", func(t *testing.T) { + intent := NewIntentTyped(IntentDataOpenSession{SessionId: "0x1234"}) + + err = SignIntentWithWallet(signer, intent) + require.NoError(t, err) + + assert.NoError(t, intent.IsValid()) + }) + + t.Run("valid_p256k1Signature", func(t *testing.T) { + intent := NewIntentTyped(IntentDataOpenSession{SessionId: "0x1234"}) + + err = SignIntentWithP256K1(signer, intent) + require.NoError(t, err) + + assert.NoError(t, intent.IsValid()) + }) + + t.Run("valid_p256r1Signature", func(t *testing.T) { + intent := NewIntentTyped(IntentDataOpenSession{SessionId: "0x1234"}) + + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + err = SignIntentWithP256R1(privateKey, intent) + require.NoError(t, err) + + assert.NoError(t, intent.IsValid()) + }) + + t.Run("valid_legacySignature", func(t *testing.T) { + intent := NewIntentTyped(IntentDataOpenSession{SessionId: "0x1234"}) + + err = SignIntentWithWalletLegacy(signer, intent) + require.NoError(t, err) + + assert.NoError(t, intent.IsValid()) + }) + + t.Run("expired", func(t *testing.T) { + intent := NewIntentTyped(IntentDataOpenSession{SessionId: "0x1234"}) + intent.Expires = 0 + + err = SignIntentWithWalletLegacy(signer, intent) + require.NoError(t, err) + + assert.ErrorContains(t, intent.IsValid(), "expired") + }) + + t.Run("issuedInFuture", func(t *testing.T) { + intent := NewIntentTyped(IntentDataOpenSession{SessionId: "0x1234"}) + intent.Issued = uint64(1 << 63) + + err = SignIntentWithWalletLegacy(signer, intent) + require.NoError(t, err) + + assert.ErrorContains(t, intent.IsValid(), "issued in the future") + }) + + t.Run("noSignatures", func(t *testing.T) { + intent := NewIntentTyped(IntentDataOpenSession{SessionId: "0x1234"}) + + assert.ErrorContains(t, intent.IsValid(), "no signatures") + }) + + t.Run("invalidSignature", func(t *testing.T) { + intent := NewIntentTyped(IntentDataOpenSession{SessionId: "0x1234"}) + + err = SignIntentWithWalletLegacy(signer, intent) + require.NoError(t, err) + + intent.Signatures[0].Signature = "0x1234" + + assert.ErrorContains(t, intent.IsValid(), "invalid signature") + }) +} diff --git a/intentv1/sign.go b/intentv1/sign.go new file mode 100644 index 0000000..70ff062 --- /dev/null +++ b/intentv1/sign.go @@ -0,0 +1,77 @@ +package intents + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "fmt" + "strings" + + "github.com/0xsequence/ethkit/ethwallet" + "github.com/0xsequence/ethkit/go-ethereum/common" +) + +func SignIntentWithWalletLegacy[T any](wallet *ethwallet.Wallet, intent *IntentTyped[T]) error { + hash, err := intent.Hash() + if err != nil { + return err + } + + signature, err := wallet.SignMessage(hash) + if err != nil { + return err + } + + intent.Signatures = append(intent.Signatures, &Signature{ + SessionId: strings.ToLower(wallet.Address().String()), + Signature: strings.ToLower(fmt.Sprintf("0x%s", common.Bytes2Hex(signature))), + }) + return nil +} + +func SignIntentWithWallet[T any](wallet *ethwallet.Wallet, intent *IntentTyped[T]) error { + hash, err := intent.Hash() + if err != nil { + return err + } + + signature, err := wallet.SignMessage(hash) + if err != nil { + return err + } + + intent.Signatures = append(intent.Signatures, &Signature{ + SessionId: strings.ToLower(fmt.Sprintf("0x%s", common.Bytes2Hex(append([]byte{byte(KeyTypeSECP256K1)}, wallet.Address().Bytes()...)))), + Signature: strings.ToLower(fmt.Sprintf("0x%s", common.Bytes2Hex(signature))), + }) + return nil +} + +func SignIntentWithP256K1[T any](wallet *ethwallet.Wallet, intent *IntentTyped[T]) error { + return SignIntentWithWallet(wallet, intent) +} + +func SignIntentWithP256R1[T any](privateKey *ecdsa.PrivateKey, intent *IntentTyped[T]) error { + hash, err := intent.Hash() + if err != nil { + return err + } + + sha256Hash := sha256.Sum256(hash) + + r, s, err := ecdsa.Sign(rand.Reader, privateKey, sha256Hash[:]) + if err != nil { + return err + } + + signature := append(r.Bytes(), s.Bytes()...) + + pubKey := elliptic.Marshal(privateKey.Curve, privateKey.PublicKey.X, privateKey.PublicKey.Y) + + intent.Signatures = append(intent.Signatures, &Signature{ + SessionId: strings.ToLower(fmt.Sprintf("0x%s", common.Bytes2Hex(append([]byte{byte(KeyTypeSECP256R1)}, pubKey...)))), + Signature: strings.ToLower(fmt.Sprintf("0x%s", common.Bytes2Hex(signature))), + }) + return nil +}