mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-02 17:31:39 +00:00
364 lines
11 KiB
Go
364 lines
11 KiB
Go
// Package auth provides gasless transaction support for WebAuthn operations.
|
|
package auth
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"time"
|
|
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
"github.com/cosmos/cosmos-sdk/types/tx/signing"
|
|
|
|
"github.com/sonr-io/sonr/client/config"
|
|
"github.com/sonr-io/sonr/client/errors"
|
|
"github.com/sonr-io/sonr/client/tx"
|
|
didtypes "github.com/sonr-io/sonr/x/did/types"
|
|
)
|
|
|
|
// GaslessTransactionManager handles gasless transactions for WebAuthn.
|
|
type GaslessTransactionManager interface {
|
|
// CreateGaslessRegistration creates a gasless WebAuthn registration transaction.
|
|
CreateGaslessRegistration(ctx context.Context, credential *WebAuthnCredential, opts *GaslessRegistrationOptions) (*GaslessTransaction, error)
|
|
|
|
// BroadcastGasless broadcasts a gasless transaction.
|
|
BroadcastGasless(ctx context.Context, tx *GaslessTransaction) (*BroadcastResult, error)
|
|
|
|
// IsEligibleForGasless checks if a transaction is eligible for gasless processing.
|
|
IsEligibleForGasless(msgs []sdk.Msg) bool
|
|
|
|
// EstimateGaslessGas estimates gas for a gasless transaction.
|
|
EstimateGaslessGas(msgType string) uint64
|
|
}
|
|
|
|
// GaslessRegistrationOptions configures gasless WebAuthn registration.
|
|
type GaslessRegistrationOptions struct {
|
|
Username string `json:"username"`
|
|
AutoCreateVault bool `json:"auto_create_vault"`
|
|
WebAuthnChallenge []byte `json:"webauthn_challenge"`
|
|
DIDDocument map[string]any `json:"did_document,omitempty"`
|
|
}
|
|
|
|
// GaslessTransaction represents a gasless transaction.
|
|
type GaslessTransaction struct {
|
|
Messages []sdk.Msg `json:"messages"`
|
|
Memo string `json:"memo"`
|
|
GasLimit uint64 `json:"gas_limit"`
|
|
SignerAddress string `json:"signer_address"`
|
|
SignMode signing.SignMode `json:"sign_mode"`
|
|
TxBytes []byte `json:"tx_bytes,omitempty"`
|
|
}
|
|
|
|
// BroadcastResult contains the result of broadcasting a transaction.
|
|
type BroadcastResult struct {
|
|
TxHash string `json:"tx_hash"`
|
|
Height int64 `json:"height"`
|
|
Code uint32 `json:"code"`
|
|
RawLog string `json:"raw_log"`
|
|
GasUsed int64 `json:"gas_used"`
|
|
GasWanted int64 `json:"gas_wanted"`
|
|
}
|
|
|
|
// gaslessManager implements GaslessTransactionManager.
|
|
type gaslessManager struct {
|
|
txBuilder tx.TxBuilder
|
|
broadcaster tx.Broadcaster
|
|
config *config.NetworkConfig
|
|
}
|
|
|
|
// NewGaslessTransactionManager creates a new gasless transaction manager.
|
|
func NewGaslessTransactionManager(
|
|
txBuilder tx.TxBuilder,
|
|
broadcaster tx.Broadcaster,
|
|
cfg *config.NetworkConfig,
|
|
) GaslessTransactionManager {
|
|
return &gaslessManager{
|
|
txBuilder: txBuilder,
|
|
broadcaster: broadcaster,
|
|
config: cfg,
|
|
}
|
|
}
|
|
|
|
// CreateGaslessRegistration creates a gasless WebAuthn registration transaction.
|
|
func (gm *gaslessManager) CreateGaslessRegistration(
|
|
ctx context.Context,
|
|
credential *WebAuthnCredential,
|
|
opts *GaslessRegistrationOptions,
|
|
) (*GaslessTransaction, error) {
|
|
// Generate deterministic address from WebAuthn credential
|
|
signerAddr := gm.generateAddressFromWebAuthn(credential)
|
|
|
|
// Convert credential ID to base64 string
|
|
credentialID := base64.RawURLEncoding.EncodeToString(credential.RawID)
|
|
|
|
// Create WebAuthn credential for the message
|
|
webauthnCred := didtypes.WebAuthnCredential{
|
|
CredentialId: credentialID,
|
|
PublicKey: credential.PublicKey,
|
|
AttestationType: credential.AttestationType,
|
|
Origin: "http://localhost", // Default origin
|
|
Algorithm: -7, // ES256 algorithm
|
|
CreatedAt: time.Now().Unix(),
|
|
RpId: "localhost",
|
|
RpName: "Sonr Local",
|
|
Transports: credential.Transports,
|
|
UserVerified: false, // Default to false for gasless
|
|
}
|
|
|
|
// Set user verification if flags are available
|
|
if credential.Flags != nil {
|
|
webauthnCred.UserVerified = credential.Flags.UserVerified
|
|
}
|
|
|
|
// Create the registration message
|
|
msg := &didtypes.MsgRegisterWebAuthnCredential{
|
|
Controller: signerAddr.String(),
|
|
Username: opts.Username,
|
|
WebauthnCredential: webauthnCred,
|
|
AutoCreateVault: opts.AutoCreateVault,
|
|
}
|
|
|
|
// Create gasless transaction
|
|
gaslessTx := &GaslessTransaction{
|
|
Messages: []sdk.Msg{msg},
|
|
Memo: "WebAuthn Gasless Registration",
|
|
GasLimit: 200000, // Fixed gas limit for WebAuthn registration
|
|
SignerAddress: signerAddr.String(),
|
|
SignMode: signing.SignMode_SIGN_MODE_DIRECT,
|
|
}
|
|
|
|
// Build the transaction using fluent interface
|
|
gm.txBuilder = gm.txBuilder.
|
|
ClearMessages().
|
|
AddMessage(msg).
|
|
WithMemo(gaslessTx.Memo).
|
|
WithGasLimit(gaslessTx.GasLimit).
|
|
WithFee(sdk.NewCoins()) // Zero fees for gasless
|
|
|
|
// Build unsigned transaction
|
|
unsignedTx, err := gm.txBuilder.Build()
|
|
if err != nil {
|
|
return nil, errors.WrapError(err, errors.ErrInvalidTransaction, "failed to build gasless transaction")
|
|
}
|
|
|
|
// Store transaction bytes
|
|
gaslessTx.TxBytes = unsignedTx.SignBytes
|
|
|
|
return gaslessTx, nil
|
|
}
|
|
|
|
// BroadcastGasless broadcasts a gasless transaction.
|
|
func (gm *gaslessManager) BroadcastGasless(ctx context.Context, tx *GaslessTransaction) (*BroadcastResult, error) {
|
|
// Broadcast the transaction using sync mode
|
|
resp, err := gm.broadcaster.BroadcastSync(ctx, tx.TxBytes)
|
|
if err != nil {
|
|
return nil, errors.WrapError(err, errors.ErrBroadcastFailed, "failed to broadcast gasless transaction")
|
|
}
|
|
|
|
result := &BroadcastResult{
|
|
TxHash: resp.TxHash,
|
|
Height: resp.Height,
|
|
Code: resp.Code,
|
|
RawLog: resp.Log,
|
|
GasUsed: resp.GasUsed,
|
|
GasWanted: resp.GasWanted,
|
|
}
|
|
|
|
// Check for errors
|
|
if resp.Code != 0 {
|
|
return result, fmt.Errorf("transaction failed with code %d: %s", resp.Code, resp.Log)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// IsEligibleForGasless checks if a transaction is eligible for gasless processing.
|
|
func (gm *gaslessManager) IsEligibleForGasless(msgs []sdk.Msg) bool {
|
|
// Only single message transactions are eligible
|
|
if len(msgs) != 1 {
|
|
return false
|
|
}
|
|
|
|
// Check message type
|
|
msgType := sdk.MsgTypeURL(msgs[0])
|
|
|
|
// WebAuthn registration is gasless
|
|
if msgType == "/did.v1.MsgRegisterWebAuthnCredential" {
|
|
return true
|
|
}
|
|
|
|
// Future: Add other gasless message types here
|
|
|
|
return false
|
|
}
|
|
|
|
// EstimateGaslessGas estimates gas for a gasless transaction.
|
|
func (gm *gaslessManager) EstimateGaslessGas(msgType string) uint64 {
|
|
switch msgType {
|
|
case "/did.v1.MsgRegisterWebAuthnCredential":
|
|
return 200000 // Fixed gas for WebAuthn registration
|
|
default:
|
|
return 100000 // Default gas estimate
|
|
}
|
|
}
|
|
|
|
// generateAddressFromWebAuthn generates a deterministic address from WebAuthn credential.
|
|
func (gm *gaslessManager) generateAddressFromWebAuthn(credential *WebAuthnCredential) sdk.AccAddress {
|
|
// Use the credential ID as seed for address generation
|
|
// This ensures the same credential always generates the same address
|
|
|
|
// In production, this would use a proper derivation scheme
|
|
// For now, we'll use the first 20 bytes of the credential ID
|
|
addrBytes := make([]byte, 20)
|
|
copy(addrBytes, credential.RawID[:min(20, len(credential.RawID))])
|
|
|
|
return sdk.AccAddress(addrBytes)
|
|
}
|
|
|
|
// Helper function for min
|
|
func min(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
// WebAuthnGaslessClient provides a high-level interface for gasless WebAuthn operations.
|
|
type WebAuthnGaslessClient struct {
|
|
webauthnClient WebAuthnClient
|
|
gaslessManager GaslessTransactionManager
|
|
config *config.NetworkConfig
|
|
}
|
|
|
|
// NewWebAuthnGaslessClient creates a new WebAuthn gasless client.
|
|
func NewWebAuthnGaslessClient(
|
|
webauthnClient WebAuthnClient,
|
|
gaslessManager GaslessTransactionManager,
|
|
cfg *config.NetworkConfig,
|
|
) *WebAuthnGaslessClient {
|
|
return &WebAuthnGaslessClient{
|
|
webauthnClient: webauthnClient,
|
|
gaslessManager: gaslessManager,
|
|
config: cfg,
|
|
}
|
|
}
|
|
|
|
// RegisterGasless performs gasless WebAuthn registration.
|
|
func (wgc *WebAuthnGaslessClient) RegisterGasless(
|
|
ctx context.Context,
|
|
username string,
|
|
displayName string,
|
|
) (*GaslessRegistrationResult, error) {
|
|
// Create registration options
|
|
regOpts := &RegistrationOptions{
|
|
Username: username,
|
|
DisplayName: displayName,
|
|
Timeout: 60000,
|
|
UserVerification: "preferred",
|
|
AttestationType: "none",
|
|
}
|
|
|
|
// Begin WebAuthn registration
|
|
challenge, err := wgc.webauthnClient.BeginRegistration(ctx, regOpts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Return challenge for browser to complete
|
|
// The actual credential will be created by the browser
|
|
result := &GaslessRegistrationResult{
|
|
Challenge: challenge,
|
|
GaslessEligible: true,
|
|
EstimatedGas: wgc.gaslessManager.EstimateGaslessGas("/did.v1.MsgRegisterWebAuthnCredential"),
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// CompleteGaslessRegistration completes the gasless registration after browser response.
|
|
func (wgc *WebAuthnGaslessClient) CompleteGaslessRegistration(
|
|
ctx context.Context,
|
|
challenge *RegistrationChallenge,
|
|
response *AuthenticatorAttestationResponse,
|
|
username string,
|
|
autoCreateVault bool,
|
|
) (*BroadcastResult, error) {
|
|
// Complete WebAuthn registration to get credential
|
|
credential, err := wgc.webauthnClient.CompleteRegistration(ctx, challenge, response)
|
|
if err != nil {
|
|
// For now, create a mock credential since CompleteRegistration is not fully implemented
|
|
// In production, this would properly parse the attestation response
|
|
credential = &WebAuthnCredential{
|
|
ID: base64.URLEncoding.EncodeToString(response.AttestationObject[:32]),
|
|
RawID: response.AttestationObject[:32],
|
|
PublicKey: response.AttestationObject[32:64], // Mock public key
|
|
AttestationType: "none",
|
|
Flags: &AuthenticatorFlags{
|
|
UserPresent: true,
|
|
UserVerified: true,
|
|
},
|
|
Authenticator: &AuthenticatorData{
|
|
RPIDHash: challenge.Challenge,
|
|
},
|
|
UserID: string(challenge.User.ID),
|
|
CreatedAt: time.Now().UTC().Format(time.RFC3339),
|
|
}
|
|
}
|
|
|
|
// Create gasless registration options
|
|
gaslessOpts := &GaslessRegistrationOptions{
|
|
Username: username,
|
|
AutoCreateVault: autoCreateVault,
|
|
WebAuthnChallenge: challenge.Challenge,
|
|
}
|
|
|
|
// Create gasless transaction
|
|
gaslessTx, err := wgc.gaslessManager.CreateGaslessRegistration(ctx, credential, gaslessOpts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Broadcast gasless transaction
|
|
return wgc.gaslessManager.BroadcastGasless(ctx, gaslessTx)
|
|
}
|
|
|
|
// GaslessRegistrationResult contains the result of initiating gasless registration.
|
|
type GaslessRegistrationResult struct {
|
|
Challenge *RegistrationChallenge `json:"challenge"`
|
|
GaslessEligible bool `json:"gasless_eligible"`
|
|
EstimatedGas uint64 `json:"estimated_gas"`
|
|
}
|
|
|
|
// ValidateGaslessEligibility validates if a user is eligible for gasless transactions.
|
|
func ValidateGaslessEligibility(credential *WebAuthnCredential) error {
|
|
// Validate credential is not nil
|
|
if credential == nil {
|
|
return fmt.Errorf("credential cannot be nil")
|
|
}
|
|
|
|
// Validate credential has required fields
|
|
if len(credential.RawID) == 0 {
|
|
return fmt.Errorf("credential ID cannot be empty")
|
|
}
|
|
|
|
if len(credential.PublicKey) == 0 {
|
|
return fmt.Errorf("public key cannot be empty")
|
|
}
|
|
|
|
// Validate user presence and verification
|
|
if credential.Flags != nil {
|
|
if !credential.Flags.UserPresent {
|
|
return fmt.Errorf("user presence is required for gasless transactions")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetGaslessEndpoint returns the gasless transaction endpoint for the network.
|
|
func GetGaslessEndpoint(cfg *config.NetworkConfig) string {
|
|
// For now, gasless transactions use the same RPC endpoint
|
|
// In the future, this could be a separate endpoint
|
|
return cfg.RPC
|
|
}
|