transfer.go raw

   1  package find
   2  
   3  import (
   4  	"fmt"
   5  	"time"
   6  
   7  	"next.orly.dev/pkg/nostr/encoders/event"
   8  	"next.orly.dev/pkg/nostr/encoders/hex"
   9  	"next.orly.dev/pkg/nostr/interfaces/signer"
  10  )
  11  
  12  // CreateTransferProposal creates a complete transfer proposal with authorization from previous owner
  13  func CreateTransferProposal(name string, prevOwnerSigner, newOwnerSigner signer.I) (*event.E, error) {
  14  	// Normalize name
  15  	name = NormalizeName(name)
  16  
  17  	// Validate name
  18  	if err := ValidateName(name); err != nil {
  19  		return nil, fmt.Errorf("invalid name: %w", err)
  20  	}
  21  
  22  	// Get public keys
  23  	prevOwnerPubkey := hex.Enc(prevOwnerSigner.Pub())
  24  	newOwnerPubkey := hex.Enc(newOwnerSigner.Pub())
  25  
  26  	// Create timestamp for the transfer
  27  	timestamp := time.Now()
  28  
  29  	// Sign the transfer authorization with previous owner's key
  30  	prevSig, err := SignTransferAuth(name, newOwnerPubkey, timestamp, prevOwnerSigner)
  31  	if err != nil {
  32  		return nil, fmt.Errorf("failed to create transfer authorization: %w", err)
  33  	}
  34  
  35  	// Create the transfer proposal event signed by new owner
  36  	proposal, err := NewRegistrationProposalWithTransfer(name, prevOwnerPubkey, prevSig, newOwnerSigner)
  37  	if err != nil {
  38  		return nil, fmt.Errorf("failed to create transfer proposal: %w", err)
  39  	}
  40  
  41  	return proposal, nil
  42  }
  43  
  44  // ValidateTransferProposal validates a transfer proposal against the current owner
  45  func ValidateTransferProposal(proposal *RegistrationProposal, currentOwner string) error {
  46  	// Check that this is a transfer action
  47  	if proposal.Action != ActionTransfer {
  48  		return fmt.Errorf("not a transfer action: %s", proposal.Action)
  49  	}
  50  
  51  	// Check that prev_owner is set
  52  	if proposal.PrevOwner == "" {
  53  		return fmt.Errorf("missing prev_owner in transfer proposal")
  54  	}
  55  
  56  	// Check that prev_sig is set
  57  	if proposal.PrevSig == "" {
  58  		return fmt.Errorf("missing prev_sig in transfer proposal")
  59  	}
  60  
  61  	// Verify that prev_owner matches current owner
  62  	if proposal.PrevOwner != currentOwner {
  63  		return fmt.Errorf("prev_owner %s does not match current owner %s",
  64  			proposal.PrevOwner, currentOwner)
  65  	}
  66  
  67  	// Get new owner from proposal event
  68  	newOwnerPubkey := hex.Enc(proposal.Event.Pubkey)
  69  
  70  	// Verify the transfer authorization signature
  71  	// Use proposal creation time as timestamp
  72  	timestamp := time.Unix(proposal.Event.CreatedAt, 0)
  73  
  74  	ok, err := VerifyTransferAuth(proposal.Name, newOwnerPubkey, proposal.PrevOwner,
  75  		timestamp, proposal.PrevSig)
  76  	if err != nil {
  77  		return fmt.Errorf("transfer authorization verification failed: %w", err)
  78  	}
  79  
  80  	if !ok {
  81  		return fmt.Errorf("invalid transfer authorization signature")
  82  	}
  83  
  84  	return nil
  85  }
  86  
  87  // PrepareTransferAuth prepares the transfer authorization data that needs to be signed
  88  // This is a helper for wallets/clients that want to show what they're signing
  89  func PrepareTransferAuth(name, newOwner string, timestamp time.Time) TransferAuthorization {
  90  	return TransferAuthorization{
  91  		Name:      NormalizeName(name),
  92  		NewOwner:  newOwner,
  93  		Timestamp: timestamp,
  94  	}
  95  }
  96  
  97  // AuthorizeTransfer creates a transfer authorization signature
  98  // This is meant to be used by the current owner to authorize a transfer to a new owner
  99  func AuthorizeTransfer(name, newOwnerPubkey string, ownerSigner signer.I) (prevSig string, timestamp time.Time, err error) {
 100  	// Normalize name
 101  	name = NormalizeName(name)
 102  
 103  	// Validate name
 104  	if err := ValidateName(name); err != nil {
 105  		return "", time.Time{}, fmt.Errorf("invalid name: %w", err)
 106  	}
 107  
 108  	// Create timestamp
 109  	timestamp = time.Now()
 110  
 111  	// Sign the authorization
 112  	prevSig, err = SignTransferAuth(name, newOwnerPubkey, timestamp, ownerSigner)
 113  	if err != nil {
 114  		return "", time.Time{}, fmt.Errorf("failed to sign transfer auth: %w", err)
 115  	}
 116  
 117  	return prevSig, timestamp, nil
 118  }
 119  
 120  // CreateTransferProposalWithAuth creates a transfer proposal using a pre-existing authorization
 121  // This is useful when the previous owner has already provided their signature
 122  func CreateTransferProposalWithAuth(name, prevOwnerPubkey, prevSig string, newOwnerSigner signer.I) (*event.E, error) {
 123  	// Normalize name
 124  	name = NormalizeName(name)
 125  
 126  	// Validate name
 127  	if err := ValidateName(name); err != nil {
 128  		return nil, fmt.Errorf("invalid name: %w", err)
 129  	}
 130  
 131  	// Create the transfer proposal event
 132  	proposal, err := NewRegistrationProposalWithTransfer(name, prevOwnerPubkey, prevSig, newOwnerSigner)
 133  	if err != nil {
 134  		return nil, fmt.Errorf("failed to create transfer proposal: %w", err)
 135  	}
 136  
 137  	return proposal, nil
 138  }
 139  
 140  // VerifyTransferProposalSignature verifies both the event signature and transfer authorization
 141  func VerifyTransferProposalSignature(proposal *RegistrationProposal) error {
 142  	// Verify the event signature itself
 143  	if err := VerifyEvent(proposal.Event); err != nil {
 144  		return fmt.Errorf("invalid event signature: %w", err)
 145  	}
 146  
 147  	// If this is a transfer, verify the transfer authorization
 148  	if proposal.Action == ActionTransfer {
 149  		// Get new owner from proposal event
 150  		newOwnerPubkey := hex.Enc(proposal.Event.Pubkey)
 151  
 152  		// Use proposal creation time as timestamp
 153  		timestamp := time.Unix(proposal.Event.CreatedAt, 0)
 154  
 155  		// Verify transfer auth
 156  		ok, err := VerifyTransferAuth(proposal.Name, newOwnerPubkey, proposal.PrevOwner,
 157  			timestamp, proposal.PrevSig)
 158  		if err != nil {
 159  			return fmt.Errorf("transfer authorization verification failed: %w", err)
 160  		}
 161  
 162  		if !ok {
 163  			return fmt.Errorf("invalid transfer authorization signature")
 164  		}
 165  	}
 166  
 167  	return nil
 168  }
 169