Files
sonr/client/auth/gasless.go
T
Prad NukalaandGitHub 13e6c3e84d Master (#1262)
* clear

* feat: Add everything

* fix: Commenht
2025-10-03 14:45:52 -04:00

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
}