Skip to content

Commit 2bfc055

Browse files
authored
SDP-1877: Allow embedded wallet to skip verification (#934)
### What As title. Changes include: 1. Add `receiver_wallet_id` and `requires_verification` to `embedded_wallets` table to track wallet status and future verification 2. Update disbursement_handler take care csv validation when verification is not required 3. During `SendInvite`, update `receiver_wallet_id` and `requires_verification` in `embedded_wallets` table 4. Embedded wallet will be auto register if no verification is required upon embedded wallet creation 5. API gating and tests ### Why Embedded wallet support ### Known limitations verification required path to be implemented ### Checklist - [x] Title follows `SDP-1234: Add new feature` or `Chore: Refactor package xyz` format. The Jira ticket code was included if available. - [x] PR has a focused scope and doesn't mix features with refactoring - [x] Tests are included (if applicable) - [ ] `CHANGELOG.md` is updated (if applicable) - [ ] CONFIG/SECRETS changes are updated in helmcharts and deployments (if applicable) - [ ] Preview deployment works as expected - [ ] Ready for production
1 parent efccc28 commit 2bfc055

13 files changed

+652
-70
lines changed

db/migrations/sdp-migrations/2025-05-18.1-create-embedded-wallet.sql

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ CREATE TABLE embedded_wallets (
1212
wasm_hash VARCHAR(64),
1313
contract_address VARCHAR(56),
1414
public_key VARCHAR(130),
15+
receiver_wallet_id VARCHAR(36) REFERENCES receiver_wallets (id),
16+
requires_verification BOOLEAN NOT NULL DEFAULT FALSE,
1517
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
1618
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
1719
wallet_status embedded_wallet_status NOT NULL DEFAULT 'PENDING'::embedded_wallet_status
@@ -25,4 +27,4 @@ DROP TRIGGER refresh_embedded_wallets_updated_at ON embedded_wallets;
2527

2628
DROP TABLE embedded_wallets CASCADE;
2729

28-
DROP TYPE embedded_wallet_status;
30+
DROP TYPE embedded_wallet_status;

internal/data/assets.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -254,10 +254,11 @@ func (a *AssetModel) SoftDelete(ctx context.Context, sqlExec db.SQLExecuter, id
254254
}
255255

256256
type ReceiverWalletAsset struct {
257-
WalletID string `db:"wallet_id"`
258-
ReceiverWallet ReceiverWallet `db:"receiver_wallet"`
259-
Asset Asset `db:"asset"`
260-
DisbursementReceiverRegistrationMsgTemplate *string `json:"-" db:"receiver_registration_message_template"`
257+
WalletID string `db:"wallet_id"`
258+
ReceiverWallet ReceiverWallet `db:"receiver_wallet"`
259+
Asset Asset `db:"asset"`
260+
DisbursementReceiverRegistrationMsgTemplate *string `json:"-" db:"receiver_registration_message_template"`
261+
VerificationField VerificationType `db:"verification_field"`
261262
}
262263

263264
// GetAssetsPerReceiverWallet returns the assets associated with a READY payment for each receiver
@@ -276,6 +277,7 @@ func (a *AssetModel) GetAssetsPerReceiverWallet(ctx context.Context, receiverWal
276277
p.id AS payment_id,
277278
rw.wallet_id,
278279
COALESCE(d.receiver_registration_message_template, '') as receiver_registration_message_template,
280+
COALESCE(d.verification_field::text, '') as verification_field,
279281
p.asset_id
280282
FROM
281283
payments p
@@ -285,7 +287,7 @@ func (a *AssetModel) GetAssetsPerReceiverWallet(ctx context.Context, receiverWal
285287
WHERE
286288
p.status = $1
287289
GROUP BY
288-
p.id, p.asset_id, rw.wallet_id, d.receiver_registration_message_template
290+
p.id, p.asset_id, rw.wallet_id, d.receiver_registration_message_template, d.verification_field
289291
ORDER BY
290292
p.updated_at DESC
291293
), messages_resent_since_invitation AS (
@@ -311,6 +313,7 @@ func (a *AssetModel) GetAssetsPerReceiverWallet(ctx context.Context, receiverWal
311313
SELECT DISTINCT
312314
lpw.wallet_id,
313315
lpw.receiver_registration_message_template,
316+
lpw.verification_field,
314317
rw.id AS "receiver_wallet.id",
315318
rw.invitation_sent_at AS "receiver_wallet.invitation_sent_at",
316319
COALESCE(mrsi.total_invitation_resent_attempts, 0) AS "receiver_wallet.total_invitation_resent_attempts",

internal/data/assets_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,48 +567,56 @@ func Test_GetAssetsPerReceiverWallet(t *testing.T) {
567567
Status: ReadyDisbursementStatus,
568568
Asset: asset1,
569569
ReceiverRegistrationMessageTemplate: "Disbursement SMS Registration Message Template A1",
570+
VerificationField: VerificationTypeDateOfBirth,
570571
})
571572
disbursementA2 := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
572573
Wallet: walletA,
573574
Status: ReadyDisbursementStatus,
574575
Asset: asset2,
575576
ReceiverRegistrationMessageTemplate: "Disbursement SMS Registration Message Template A2",
577+
VerificationField: VerificationTypeNationalID,
576578
})
577579
disbursementB1 := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
578580
Wallet: walletB,
579581
Status: ReadyDisbursementStatus,
580582
Asset: asset1,
581583
ReceiverRegistrationMessageTemplate: "Disbursement SMS Registration Message Template B1",
584+
VerificationField: VerificationTypePin,
582585
})
583586
disbursementB2 := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
584587
Wallet: walletB,
585588
Status: ReadyDisbursementStatus,
586589
Asset: asset2,
587590
ReceiverRegistrationMessageTemplate: "Disbursement SMS Registration Message Template B2",
591+
VerificationField: VerificationTypeYearMonth,
588592
})
589593
disbursementC1 := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
590594
Wallet: walletC,
591595
Status: ReadyDisbursementStatus,
592596
Asset: asset1,
593597
ReceiverRegistrationMessageTemplate: "Disbursement SMS Registration Message Template C1",
598+
VerificationField: VerificationTypeDateOfBirth,
594599
})
595600
disbursementC2 := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
596601
Wallet: walletC,
597602
Status: ReadyDisbursementStatus,
598603
Asset: asset2,
599604
ReceiverRegistrationMessageTemplate: "Disbursement SMS Registration Message Template C2",
605+
VerificationField: VerificationTypeNationalID,
600606
})
601607
disbursementD1 := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
602608
Wallet: walletD,
603609
Status: ReadyDisbursementStatus,
604610
Asset: asset1,
605611
ReceiverRegistrationMessageTemplate: "Disbursement SMS Registration Message Template D1",
612+
VerificationField: VerificationTypePin,
606613
})
607614
disbursementD2 := CreateDisbursementFixture(t, ctx, dbConnectionPool, models.Disbursements, &Disbursement{
608615
Wallet: walletD,
609616
Status: ReadyDisbursementStatus,
610617
Asset: asset2,
611618
ReceiverRegistrationMessageTemplate: "Disbursement SMS Registration Message Template D2",
619+
VerificationField: VerificationTypeYearMonth,
612620
})
613621

614622
// 2. Create receivers, and receiver wallets:
@@ -771,6 +779,7 @@ func Test_GetAssetsPerReceiverWallet(t *testing.T) {
771779
WalletID: walletA.ID,
772780
Asset: *asset1,
773781
DisbursementReceiverRegistrationMsgTemplate: &disbursementA1.ReceiverRegistrationMessageTemplate,
782+
VerificationField: disbursementA1.VerificationField,
774783
},
775784
{
776785
ReceiverWallet: ReceiverWallet{
@@ -785,6 +794,7 @@ func Test_GetAssetsPerReceiverWallet(t *testing.T) {
785794
WalletID: walletA.ID,
786795
Asset: *asset2,
787796
DisbursementReceiverRegistrationMsgTemplate: &disbursementA2.ReceiverRegistrationMessageTemplate,
797+
VerificationField: disbursementA2.VerificationField,
788798
},
789799
{
790800
ReceiverWallet: ReceiverWallet{
@@ -798,6 +808,7 @@ func Test_GetAssetsPerReceiverWallet(t *testing.T) {
798808
WalletID: walletB.ID,
799809
Asset: *asset1,
800810
DisbursementReceiverRegistrationMsgTemplate: &disbursementB1.ReceiverRegistrationMessageTemplate,
811+
VerificationField: disbursementB1.VerificationField,
801812
},
802813
{
803814
ReceiverWallet: ReceiverWallet{
@@ -811,6 +822,7 @@ func Test_GetAssetsPerReceiverWallet(t *testing.T) {
811822
WalletID: walletB.ID,
812823
Asset: *asset2,
813824
DisbursementReceiverRegistrationMsgTemplate: &disbursementB2.ReceiverRegistrationMessageTemplate,
825+
VerificationField: disbursementB2.VerificationField,
814826
},
815827
{
816828
ReceiverWallet: ReceiverWallet{
@@ -824,6 +836,7 @@ func Test_GetAssetsPerReceiverWallet(t *testing.T) {
824836
WalletID: walletC.ID,
825837
Asset: *asset1,
826838
DisbursementReceiverRegistrationMsgTemplate: &disbursementC1.ReceiverRegistrationMessageTemplate,
839+
VerificationField: disbursementC1.VerificationField,
827840
},
828841
{
829842
ReceiverWallet: ReceiverWallet{
@@ -837,6 +850,7 @@ func Test_GetAssetsPerReceiverWallet(t *testing.T) {
837850
WalletID: walletC.ID,
838851
Asset: *asset2,
839852
DisbursementReceiverRegistrationMsgTemplate: &disbursementC2.ReceiverRegistrationMessageTemplate,
853+
VerificationField: disbursementC2.VerificationField,
840854
},
841855
{
842856
ReceiverWallet: ReceiverWallet{
@@ -850,6 +864,7 @@ func Test_GetAssetsPerReceiverWallet(t *testing.T) {
850864
WalletID: walletD.ID,
851865
Asset: *asset1,
852866
DisbursementReceiverRegistrationMsgTemplate: &disbursementD1.ReceiverRegistrationMessageTemplate,
867+
VerificationField: disbursementD1.VerificationField,
853868
},
854869
{
855870
ReceiverWallet: ReceiverWallet{
@@ -863,6 +878,7 @@ func Test_GetAssetsPerReceiverWallet(t *testing.T) {
863878
WalletID: walletD.ID,
864879
Asset: *asset2,
865880
DisbursementReceiverRegistrationMsgTemplate: &disbursementD2.ReceiverRegistrationMessageTemplate,
881+
VerificationField: disbursementD2.VerificationField,
866882
},
867883
}
868884

internal/data/disbursement_instructions.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ func (di DisbursementInstructionModel) ProcessAll(ctx context.Context, dbTx db.D
119119
}
120120
return fmt.Errorf("registering supplied wallets: %w", err)
121121
}
122-
} else {
122+
} else if opts.Disbursement.VerificationField != "" {
123123
err = di.processReceiverVerifications(ctx, dbTx, receiversByIDMap, opts.Instructions, opts.Disbursement, registrationContactType.ReceiverContactType)
124124
if err != nil {
125125
return fmt.Errorf("processing receiver verifications: %w", err)

internal/data/embedded_wallet.go

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,16 @@ func (status EmbeddedWalletStatus) Validate() error {
4747
}
4848

4949
type EmbeddedWallet struct {
50-
Token string `json:"token" db:"token"`
51-
WasmHash string `json:"wasm_hash" db:"wasm_hash"`
52-
ContractAddress string `json:"contract_address" db:"contract_address"`
53-
CredentialID string `json:"credential_id" db:"credential_id"`
54-
PublicKey string `json:"public_key" db:"public_key"`
55-
CreatedAt *time.Time `json:"created_at" db:"created_at"`
56-
UpdatedAt *time.Time `json:"updated_at" db:"updated_at"`
57-
WalletStatus EmbeddedWalletStatus `json:"wallet_status" db:"wallet_status"`
50+
Token string `json:"token" db:"token"`
51+
WasmHash string `json:"wasm_hash" db:"wasm_hash"`
52+
ContractAddress string `json:"contract_address" db:"contract_address"`
53+
CredentialID string `json:"credential_id" db:"credential_id"`
54+
PublicKey string `json:"public_key" db:"public_key"`
55+
ReceiverWalletID string `json:"receiver_wallet_id" db:"receiver_wallet_id"`
56+
RequiresVerification bool `json:"requires_verification" db:"requires_verification"`
57+
CreatedAt *time.Time `json:"created_at" db:"created_at"`
58+
UpdatedAt *time.Time `json:"updated_at" db:"updated_at"`
59+
WalletStatus EmbeddedWalletStatus `json:"wallet_status" db:"wallet_status"`
5860
}
5961

6062
type EmbeddedWalletModel struct {
@@ -70,15 +72,16 @@ func EmbeddedWalletColumnNames(tableReference, resultAlias string) string {
7072
"created_at",
7173
"updated_at",
7274
"wallet_status",
75+
"requires_verification",
7376
},
7477
CoalesceStringColumns: []string{
7578
"wasm_hash",
7679
"contract_address",
7780
"credential_id",
7881
"public_key",
82+
"receiver_wallet_id",
7983
},
8084
}.Build()
81-
8285
return strings.Join(columns, ", ")
8386
}
8487

@@ -125,9 +128,10 @@ func (ew *EmbeddedWalletModel) GetByCredentialID(ctx context.Context, sqlExec db
125128
}
126129

127130
type EmbeddedWalletInsert struct {
128-
Token string `db:"token"`
129-
WasmHash string `db:"wasm_hash"`
130-
WalletStatus EmbeddedWalletStatus `db:"wallet_status"`
131+
Token string `db:"token"`
132+
WasmHash string `db:"wasm_hash"`
133+
RequiresVerification bool `db:"requires_verification"`
134+
WalletStatus EmbeddedWalletStatus `db:"wallet_status"`
131135
}
132136

133137
func (ewi EmbeddedWalletInsert) Validate() error {
@@ -160,15 +164,17 @@ func (ew *EmbeddedWalletModel) Insert(ctx context.Context, sqlExec db.SQLExecute
160164
INSERT INTO embedded_wallets (
161165
token,
162166
wasm_hash,
167+
requires_verification,
163168
wallet_status
164169
) VALUES (
165-
$1, $2, $3
170+
$1, $2, $3, $4
166171
) RETURNING %s`, EmbeddedWalletColumnNames("", ""))
167172

168173
var wallet EmbeddedWallet
169174
err := sqlExec.GetContext(ctx, &wallet, query,
170175
insert.Token,
171176
insert.WasmHash,
177+
insert.RequiresVerification,
172178
insert.WalletStatus)
173179
if err != nil {
174180
return nil, fmt.Errorf("inserting embedded wallet: %w", err)
@@ -178,11 +184,13 @@ func (ew *EmbeddedWalletModel) Insert(ctx context.Context, sqlExec db.SQLExecute
178184
}
179185

180186
type EmbeddedWalletUpdate struct {
181-
WasmHash string `db:"wasm_hash"`
182-
ContractAddress string `db:"contract_address"`
183-
CredentialID string `db:"credential_id"`
184-
PublicKey string `db:"public_key"`
185-
WalletStatus EmbeddedWalletStatus `db:"wallet_status"`
187+
WasmHash string `db:"wasm_hash"`
188+
ContractAddress string `db:"contract_address"`
189+
CredentialID string `db:"credential_id"`
190+
PublicKey string `db:"public_key"`
191+
WalletStatus EmbeddedWalletStatus `db:"wallet_status"`
192+
ReceiverWalletID string `db:"receiver_wallet_id"`
193+
RequiresVerification *bool `db:"requires_verification"`
186194
}
187195

188196
func (ewu EmbeddedWalletUpdate) Validate() error {

internal/data/embedded_wallet_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/stellar/stellar-disbursement-platform-backend/db"
1313
"github.com/stellar/stellar-disbursement-platform-backend/db/dbtest"
14+
"github.com/stellar/stellar-disbursement-platform-backend/internal/utils"
1415
)
1516

1617
func Test_EmbeddedWalletColumnNames(t *testing.T) {
@@ -27,10 +28,12 @@ func Test_EmbeddedWalletColumnNames(t *testing.T) {
2728
"created_at",
2829
"updated_at",
2930
"wallet_status",
31+
"requires_verification",
3032
`COALESCE(wasm_hash, '') AS "wasm_hash"`,
3133
`COALESCE(contract_address, '') AS "contract_address"`,
3234
`COALESCE(credential_id, '') AS "credential_id"`,
3335
`COALESCE(public_key, '') AS "public_key"`,
36+
`COALESCE(receiver_wallet_id, '') AS "receiver_wallet_id"`,
3437
}, ", "),
3538
},
3639
{
@@ -41,10 +44,12 @@ func Test_EmbeddedWalletColumnNames(t *testing.T) {
4144
"ew.created_at",
4245
"ew.updated_at",
4346
"ew.wallet_status",
47+
"ew.requires_verification",
4448
`COALESCE(ew.wasm_hash, '') AS "wasm_hash"`,
4549
`COALESCE(ew.contract_address, '') AS "contract_address"`,
4650
`COALESCE(ew.credential_id, '') AS "credential_id"`,
4751
`COALESCE(ew.public_key, '') AS "public_key"`,
52+
`COALESCE(ew.receiver_wallet_id, '') AS "receiver_wallet_id"`,
4853
}, ", "),
4954
},
5055
{
@@ -55,10 +60,12 @@ func Test_EmbeddedWalletColumnNames(t *testing.T) {
5560
`ew.created_at AS "embedded_wallets.created_at"`,
5661
`ew.updated_at AS "embedded_wallets.updated_at"`,
5762
`ew.wallet_status AS "embedded_wallets.wallet_status"`,
63+
`ew.requires_verification AS "embedded_wallets.requires_verification"`,
5864
`COALESCE(ew.wasm_hash, '') AS "embedded_wallets.wasm_hash"`,
5965
`COALESCE(ew.contract_address, '') AS "embedded_wallets.contract_address"`,
6066
`COALESCE(ew.credential_id, '') AS "embedded_wallets.credential_id"`,
6167
`COALESCE(ew.public_key, '') AS "embedded_wallets.public_key"`,
68+
`COALESCE(ew.receiver_wallet_id, '') AS "embedded_wallets.receiver_wallet_id"`,
6269
}, ", "),
6370
},
6471
}
@@ -105,6 +112,8 @@ func Test_EmbeddedWalletModel_GetByToken(t *testing.T) {
105112
assert.Equal(t, expectedWasmHash, wallet.WasmHash)
106113
assert.Equal(t, expectedContractAddress, wallet.ContractAddress)
107114
assert.Equal(t, PendingWalletStatus, wallet.WalletStatus)
115+
assert.Equal(t, "", wallet.ReceiverWalletID)
116+
assert.False(t, wallet.RequiresVerification)
108117
assert.NotNil(t, wallet.CreatedAt)
109118
assert.NotNil(t, wallet.UpdatedAt)
110119
})
@@ -321,6 +330,20 @@ func Test_EmbeddedWalletModel_Update(t *testing.T) {
321330
assert.True(t, updatedWallet.UpdatedAt.After(*updatedWallet.CreatedAt))
322331
})
323332

333+
t.Run("successfully toggles requires verification flag", func(t *testing.T) {
334+
createdWallet := CreateEmbeddedWalletFixture(t, ctx, dbConnectionPool, "", "hash3", "contract3", "", "", PendingWalletStatus)
335+
update := EmbeddedWalletUpdate{
336+
RequiresVerification: utils.Ptr(true),
337+
}
338+
339+
err := embeddedWalletModel.Update(ctx, dbConnectionPool, createdWallet.Token, update)
340+
require.NoError(t, err)
341+
342+
updatedWallet, getErr := embeddedWalletModel.GetByToken(ctx, dbConnectionPool, createdWallet.Token)
343+
require.NoError(t, getErr)
344+
assert.True(t, updatedWallet.RequiresVerification)
345+
})
346+
324347
t.Run("returns error when updating to duplicate credential_id", func(t *testing.T) {
325348
duplicateCredentialID := "duplicate-credential-id"
326349

internal/data/fixtures.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -453,8 +453,10 @@ $7, $8, $9, $10 ,$11, $12)
453453
}
454454

455455
func DeleteAllReceiverWalletsFixtures(t *testing.T, ctx context.Context, sqlExec db.SQLExecuter) {
456-
const query = "DELETE FROM receiver_wallets"
457-
_, err := sqlExec.ExecContext(ctx, query)
456+
DeleteAllEmbeddedWalletsFixtures(t, ctx, sqlExec)
457+
458+
const deleteReceiverWallets = "DELETE FROM receiver_wallets"
459+
_, err := sqlExec.ExecContext(ctx, deleteReceiverWallets)
458460
require.NoError(t, err)
459461
}
460462

@@ -718,14 +720,14 @@ func CreateEmbeddedWalletFixture(t *testing.T, ctx context.Context, sqlExec db.S
718720

719721
q := fmt.Sprintf(`
720722
INSERT INTO embedded_wallets
721-
(token, wasm_hash, contract_address, credential_id, public_key, wallet_status)
723+
(token, wasm_hash, contract_address, credential_id, public_key, wallet_status, receiver_wallet_id)
722724
VALUES
723-
($1, $2, $3, $4, $5, $6)
725+
($1, $2, $3, $4, $5, $6, $7)
724726
RETURNING %s
725727
`, EmbeddedWalletColumnNames("", ""))
726728
wallet := EmbeddedWallet{}
727729

728-
err := sqlExec.GetContext(ctx, &wallet, q, token, utils.SQLNullString(wasmHash), utils.SQLNullString(contractAddress), utils.SQLNullString(credentialID), utils.SQLNullString(publicKey), status)
730+
err := sqlExec.GetContext(ctx, &wallet, q, token, utils.SQLNullString(wasmHash), utils.SQLNullString(contractAddress), utils.SQLNullString(credentialID), utils.SQLNullString(publicKey), status, utils.SQLNullString(""))
729731
require.NoError(t, err)
730732
return &wallet
731733
}

0 commit comments

Comments
 (0)