Skip to content

Commit 1cd89ea

Browse files
committed
wip
1 parent b7f0c37 commit 1cd89ea

File tree

9 files changed

+577
-140
lines changed

9 files changed

+577
-140
lines changed

internal/data/embedded_wallet.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,40 @@ func (ew *EmbeddedWalletModel) GetByCredentialID(ctx context.Context, sqlExec db
127127
return &wallet, nil
128128
}
129129

130+
// GetPendingDisbursementAsset returns the asset associated with the pending disbursement for the
131+
// embedded wallet identified by the provided contract address. Pending disbursements are
132+
// determined by payments in an active (ready or pending) state for disbursement payments.
133+
func (ew *EmbeddedWalletModel) GetPendingDisbursementAsset(ctx context.Context, sqlExec db.SQLExecuter, contractAddress string) (*Asset, error) {
134+
if strings.TrimSpace(contractAddress) == "" {
135+
return nil, ErrMissingInput
136+
}
137+
138+
query := fmt.Sprintf(`
139+
SELECT
140+
%s
141+
FROM embedded_wallets ew
142+
JOIN receiver_wallets rw ON rw.id = ew.receiver_wallet_id
143+
JOIN payments p ON p.receiver_wallet_id = rw.id
144+
JOIN disbursements d ON d.id = p.disbursement_id
145+
JOIN assets a ON a.id = d.asset_id
146+
WHERE ew.contract_address = $1
147+
AND p.type = $2
148+
AND p.status = ANY($3)
149+
ORDER BY p.updated_at DESC
150+
LIMIT 1
151+
`, AssetColumnNames("a", "", false))
152+
153+
var asset Asset
154+
if err := sqlExec.GetContext(ctx, &asset, query, contractAddress, PaymentTypeDisbursement, pq.Array(PaymentInProgressStatuses())); err != nil {
155+
if errors.Is(err, sql.ErrNoRows) {
156+
return nil, ErrRecordNotFound
157+
}
158+
return nil, fmt.Errorf("querying pending disbursement asset for contract %s: %w", contractAddress, err)
159+
}
160+
161+
return &asset, nil
162+
}
163+
130164
type EmbeddedWalletInsert struct {
131165
Token string `db:"token"`
132166
WasmHash string `db:"wasm_hash"`

internal/serve/httphandler/passkey_handler.go

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package httphandler
22

33
import (
4+
"context"
45
"encoding/base64"
56
"errors"
67
"net/http"
@@ -198,7 +199,9 @@ func (r RefreshTokenRequest) Validate() *httperror.HTTPError {
198199
}
199200

200201
type RefreshTokenResponse struct {
201-
Token string `json:"token"`
202+
Token string `json:"token"`
203+
IsVerificationPending bool `json:"is_verification_pending"`
204+
PendingAsset *data.Asset `json:"pending_asset,omitempty"`
202205
}
203206

204207
func (h PasskeyHandler) RefreshToken(rw http.ResponseWriter, req *http.Request) {
@@ -229,18 +232,35 @@ func (h PasskeyHandler) RefreshToken(rw http.ResponseWriter, req *http.Request)
229232
return
230233
}
231234

232-
if contractAddress == "" {
233-
var embeddedWallet *data.EmbeddedWallet
234-
embeddedWallet, err = h.EmbeddedWalletService.GetWalletByCredentialID(ctx, credentialID)
235+
embeddedWallet, err := h.EmbeddedWalletService.GetWalletByCredentialID(ctx, credentialID)
236+
if err != nil {
237+
if errors.Is(err, services.ErrInvalidCredentialID) {
238+
httperror.Unauthorized("Invalid credential ID", err, nil).Render(rw)
239+
} else {
240+
httperror.InternalError(ctx, "Failed to lookup wallet", err, nil).Render(rw)
241+
}
242+
return
243+
}
244+
if embeddedWallet.ContractAddress != "" {
245+
contractAddress = embeddedWallet.ContractAddress
246+
}
247+
isVerificationPending, err := h.IsVerificationPending(ctx, embeddedWallet)
248+
if err != nil {
249+
if errors.Is(err, services.ErrInvalidReceiverWalletID) {
250+
httperror.InternalError(ctx, "Receiver wallet not found", err, nil).Render(rw)
251+
} else {
252+
httperror.InternalError(ctx, "Failed to evaluate verification requirement", err, nil).Render(rw)
253+
}
254+
return
255+
}
256+
257+
var asset *data.Asset
258+
if contractAddress != "" {
259+
asset, err = h.EmbeddedWalletService.GetPendingDisbursementAsset(ctx, contractAddress)
235260
if err != nil {
236-
if errors.Is(err, services.ErrInvalidCredentialID) {
237-
httperror.Unauthorized("Invalid credential ID", err, nil).Render(rw)
238-
} else {
239-
httperror.InternalError(ctx, "Failed to lookup wallet", err, nil).Render(rw)
240-
}
261+
httperror.InternalError(ctx, "Failed to retrieve pending disbursement asset", err, nil).Render(rw)
241262
return
242263
}
243-
contractAddress = embeddedWallet.ContractAddress
244264
}
245265

246266
expiresAt := time.Now().Add(WalletTokenExpiration)
@@ -251,8 +271,27 @@ func (h PasskeyHandler) RefreshToken(rw http.ResponseWriter, req *http.Request)
251271
}
252272

253273
resp := RefreshTokenResponse{
254-
Token: refreshedToken,
274+
Token: refreshedToken,
275+
IsVerificationPending: isVerificationPending,
276+
PendingAsset: asset,
255277
}
256278

257279
httpjson.Render(rw, resp, httpjson.JSON)
258280
}
281+
282+
func (h PasskeyHandler) IsVerificationPending(ctx context.Context, embeddedWallet *data.EmbeddedWallet) (bool, error) {
283+
if embeddedWallet == nil || !embeddedWallet.RequiresVerification {
284+
return false, nil
285+
}
286+
287+
if embeddedWallet.ReceiverWalletID == "" {
288+
return false, nil
289+
}
290+
291+
receiverWallet, err := h.EmbeddedWalletService.GetReceiverWalletByID(ctx, embeddedWallet.ReceiverWalletID)
292+
if err != nil {
293+
return false, err
294+
}
295+
296+
return receiverWallet.Status == data.ReadyReceiversWalletStatus, nil
297+
}

internal/serve/httphandler/passkey_handler_test.go

Lines changed: 179 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,28 @@ func Test_PasskeyHandler_RefreshToken(t *testing.T) {
680680
Return(credentialID, contractAddress, nil).
681681
Once()
682682

683+
embeddedWallet := &data.EmbeddedWallet{
684+
ContractAddress: contractAddress,
685+
CredentialID: credentialID,
686+
RequiresVerification: true,
687+
ReceiverWalletID: "rw-1",
688+
}
689+
mockEmbeddedWalletService.
690+
On("GetWalletByCredentialID", mock.Anything, credentialID).
691+
Return(embeddedWallet, nil).
692+
Once()
693+
694+
mockEmbeddedWalletService.
695+
On("GetReceiverWalletByID", mock.Anything, "rw-1").
696+
Return(&data.ReceiverWallet{ID: "rw-1", Status: data.ReadyReceiversWalletStatus}, nil).
697+
Once()
698+
699+
pendingAsset := &data.Asset{ID: "asset-1", Code: "USDC", Issuer: "GDUKMGUGDZQK6YH6Q7"}
700+
mockEmbeddedWalletService.
701+
On("GetPendingDisbursementAsset", mock.Anything, contractAddress).
702+
Return(pendingAsset, nil).
703+
Once()
704+
683705
var capturedExpiresAt time.Time
684706
mockJWTManager.
685707
On("GenerateToken", mock.Anything, credentialID, contractAddress, mock.AnythingOfType("time.Time")).
@@ -700,11 +722,131 @@ func Test_PasskeyHandler_RefreshToken(t *testing.T) {
700722
err = json.Unmarshal(rr.Body.Bytes(), &respBody)
701723
require.NoError(t, err)
702724
assert.Equal(t, "refreshed-token", respBody.Token)
725+
assert.True(t, respBody.IsVerificationPending)
726+
require.NotNil(t, respBody.PendingAsset)
727+
assert.Equal(t, pendingAsset.Code, respBody.PendingAsset.Code)
728+
assert.Equal(t, pendingAsset.Issuer, respBody.PendingAsset.Issuer)
703729

704730
expectedExpiry := time.Now().Add(WalletTokenExpiration)
705731
assert.WithinDuration(t, expectedExpiry, capturedExpiresAt, 5*time.Second)
706732
}
707733

734+
func Test_PasskeyHandler_RefreshToken_ReceiverAlreadyRegistered(t *testing.T) {
735+
mockWebAuthnService := walletMocks.NewMockWebAuthnService(t)
736+
mockEmbeddedWalletService := servicesMocks.NewMockEmbeddedWalletService(t)
737+
mockJWTManager := walletMocks.NewMockWalletJWTManager(t)
738+
handler := PasskeyHandler{
739+
WebAuthnService: mockWebAuthnService,
740+
WalletJWTManager: mockJWTManager,
741+
EmbeddedWalletService: mockEmbeddedWalletService,
742+
}
743+
744+
rr := httptest.NewRecorder()
745+
requestBody, err := json.Marshal(RefreshTokenRequest{
746+
Token: "valid-token",
747+
})
748+
require.NoError(t, err)
749+
ctx := context.Background()
750+
751+
contractAddress := "CBGTG3VGUMVDZE6O4CRZ2LBCFP7O5XY2VQQQU7AVXLVDQHZLVQFRMHKX"
752+
credentialID := "test-credential-id"
753+
754+
mockJWTManager.
755+
On("ValidateToken", mock.Anything, "valid-token").
756+
Return(credentialID, contractAddress, nil).
757+
Once()
758+
759+
embeddedWallet := &data.EmbeddedWallet{
760+
ContractAddress: contractAddress,
761+
CredentialID: credentialID,
762+
RequiresVerification: true,
763+
ReceiverWalletID: "rw-registered",
764+
}
765+
mockEmbeddedWalletService.
766+
On("GetWalletByCredentialID", mock.Anything, credentialID).
767+
Return(embeddedWallet, nil).
768+
Once()
769+
770+
mockEmbeddedWalletService.
771+
On("GetReceiverWalletByID", mock.Anything, "rw-registered").
772+
Return(&data.ReceiverWallet{ID: "rw-registered", Status: data.RegisteredReceiversWalletStatus}, nil).
773+
Once()
774+
mockEmbeddedWalletService.
775+
On("GetPendingDisbursementAsset", mock.Anything, contractAddress).
776+
Return((*data.Asset)(nil), nil).
777+
Once()
778+
779+
mockJWTManager.
780+
On("GenerateToken", mock.Anything, credentialID, contractAddress, mock.AnythingOfType("time.Time")).
781+
Return("refreshed-token", nil).
782+
Once()
783+
784+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "/embedded-wallets/passkey/authentication/refresh", strings.NewReader(string(requestBody)))
785+
require.NoError(t, err)
786+
req.Header.Set("Content-Type", "application/json")
787+
http.HandlerFunc(handler.RefreshToken).ServeHTTP(rr, req)
788+
789+
assert.Equal(t, http.StatusOK, rr.Result().StatusCode)
790+
791+
var respBody RefreshTokenResponse
792+
err = json.Unmarshal(rr.Body.Bytes(), &respBody)
793+
require.NoError(t, err)
794+
assert.Equal(t, "refreshed-token", respBody.Token)
795+
assert.False(t, respBody.IsVerificationPending)
796+
assert.Nil(t, respBody.PendingAsset)
797+
}
798+
799+
func Test_PasskeyHandler_RefreshToken_AssetLookupError(t *testing.T) {
800+
mockWebAuthnService := walletMocks.NewMockWebAuthnService(t)
801+
mockEmbeddedWalletService := servicesMocks.NewMockEmbeddedWalletService(t)
802+
mockJWTManager := walletMocks.NewMockWalletJWTManager(t)
803+
handler := PasskeyHandler{
804+
WebAuthnService: mockWebAuthnService,
805+
WalletJWTManager: mockJWTManager,
806+
EmbeddedWalletService: mockEmbeddedWalletService,
807+
}
808+
809+
rr := httptest.NewRecorder()
810+
requestBody, err := json.Marshal(RefreshTokenRequest{Token: "valid-token"})
811+
require.NoError(t, err)
812+
ctx := context.Background()
813+
814+
contractAddress := "CBGTG3VGUMVDZE6O4CRZ2LBCFP7O5XY2VQQQU7AVXLVDQHZLVQFRMHKX"
815+
credentialID := "test-credential-id"
816+
assetErr := errors.New("asset lookup failed")
817+
818+
mockJWTManager.
819+
On("ValidateToken", mock.Anything, "valid-token").
820+
Return(credentialID, contractAddress, nil).
821+
Once()
822+
823+
embeddedWallet := &data.EmbeddedWallet{
824+
ContractAddress: contractAddress,
825+
CredentialID: credentialID,
826+
RequiresVerification: true,
827+
ReceiverWalletID: "rw-asset",
828+
}
829+
mockEmbeddedWalletService.
830+
On("GetWalletByCredentialID", mock.Anything, credentialID).
831+
Return(embeddedWallet, nil).
832+
Once()
833+
mockEmbeddedWalletService.
834+
On("GetReceiverWalletByID", mock.Anything, "rw-asset").
835+
Return(&data.ReceiverWallet{ID: "rw-asset", Status: data.ReadyReceiversWalletStatus}, nil).
836+
Once()
837+
mockEmbeddedWalletService.
838+
On("GetPendingDisbursementAsset", mock.Anything, contractAddress).
839+
Return((*data.Asset)(nil), assetErr).
840+
Once()
841+
842+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "/embedded-wallets/passkey/authentication/refresh", strings.NewReader(string(requestBody)))
843+
require.NoError(t, err)
844+
req.Header.Set("Content-Type", "application/json")
845+
http.HandlerFunc(handler.RefreshToken).ServeHTTP(rr, req)
846+
847+
assert.Equal(t, http.StatusInternalServerError, rr.Result().StatusCode)
848+
}
849+
708850
func Test_PasskeyHandler_RefreshToken_InvalidRequestBody(t *testing.T) {
709851
mockWebAuthnService := walletMocks.NewMockWebAuthnService(t)
710852
mockEmbeddedWalletService := servicesMocks.NewMockEmbeddedWalletService(t)
@@ -851,6 +993,27 @@ func Test_PasskeyHandler_RefreshToken_GenerateTokenErrors(t *testing.T) {
851993
Return(credentialID, contractAddress, nil).
852994
Once()
853995

996+
embeddedWallet := &data.EmbeddedWallet{
997+
ContractAddress: contractAddress,
998+
CredentialID: credentialID,
999+
RequiresVerification: true,
1000+
ReceiverWalletID: "rw-2",
1001+
}
1002+
mockEmbeddedWalletService.
1003+
On("GetWalletByCredentialID", mock.Anything, credentialID).
1004+
Return(embeddedWallet, nil).
1005+
Once()
1006+
1007+
mockEmbeddedWalletService.
1008+
On("GetReceiverWalletByID", mock.Anything, "rw-2").
1009+
Return(&data.ReceiverWallet{ID: "rw-2", Status: data.ReadyReceiversWalletStatus}, nil).
1010+
Once()
1011+
1012+
mockEmbeddedWalletService.
1013+
On("GetPendingDisbursementAsset", mock.Anything, contractAddress).
1014+
Return((*data.Asset)(nil), nil).
1015+
Once()
1016+
8541017
mockJWTManager.
8551018
On("GenerateToken", mock.Anything, credentialID, contractAddress, mock.AnythingOfType("time.Time")).
8561019
Return("", tc.generateError).
@@ -892,12 +1055,23 @@ func Test_PasskeyHandler_RefreshToken_UpdatesContractAddress(t *testing.T) {
8921055
Return(credentialID, oldContractAddress, nil).
8931056
Once()
8941057

1058+
embeddedWallet := &data.EmbeddedWallet{
1059+
CredentialID: credentialID,
1060+
ContractAddress: newContractAddress,
1061+
RequiresVerification: true,
1062+
ReceiverWalletID: "rw-3",
1063+
}
8951064
mockEmbeddedWalletService.
8961065
On("GetWalletByCredentialID", mock.Anything, credentialID).
897-
Return(&data.EmbeddedWallet{
898-
CredentialID: credentialID,
899-
ContractAddress: newContractAddress,
900-
}, nil).
1066+
Return(embeddedWallet, nil).
1067+
Once()
1068+
mockEmbeddedWalletService.
1069+
On("GetReceiverWalletByID", mock.Anything, "rw-3").
1070+
Return(&data.ReceiverWallet{ID: "rw-3", Status: data.ReadyReceiversWalletStatus}, nil).
1071+
Once()
1072+
mockEmbeddedWalletService.
1073+
On("GetPendingDisbursementAsset", mock.Anything, newContractAddress).
1074+
Return((*data.Asset)(nil), nil).
9011075
Once()
9021076

9031077
mockJWTManager.
@@ -916,6 +1090,7 @@ func Test_PasskeyHandler_RefreshToken_UpdatesContractAddress(t *testing.T) {
9161090
err = json.Unmarshal(rr.Body.Bytes(), &respBody)
9171091
require.NoError(t, err)
9181092
assert.Equal(t, "refreshed-token-with-address", respBody.Token)
1093+
assert.True(t, respBody.IsVerificationPending)
9191094
}
9201095

9211096
func Test_PasskeyHandler_RefreshToken_WalletLookupError(t *testing.T) {

internal/serve/serve.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,6 @@ func (opts *ServeOptions) SetupDependencies() error {
198198
WebAuthVerifyContractID: opts.Sep45ContractID,
199199
ServerSigningKeypair: signingKP,
200200
BaseURL: opts.BaseURL,
201-
ClientAttributionRequired: opts.Sep10ClientAttributionRequired,
202201
AllowHTTPRetry: allowHTTPRetry,
203202
})
204203
if sep45Err != nil {

0 commit comments

Comments
 (0)