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