Skip to content

Commit 9831d37

Browse files
committed
Support POOL accounts with a new PublicDistribution intent
1 parent c4302fc commit 9831d37

File tree

36 files changed

+1253
-346
lines changed

36 files changed

+1253
-346
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ require (
66
filippo.io/edwards25519 v1.1.0
77
github.com/aws/aws-sdk-go-v2 v0.17.0
88
github.com/bits-and-blooms/bloom/v3 v3.1.0
9-
github.com/code-payments/code-protobuf-api v1.19.1-0.20250610140050-4cadbcc86f16
9+
github.com/code-payments/code-protobuf-api v1.19.1-0.20250618155621-c66659ab4ff5
1010
github.com/code-payments/code-vm-indexer v0.1.11-0.20241028132209-23031e814fba
1111
github.com/emirpasic/gods v1.12.0
1212
github.com/envoyproxy/protoc-gen-validate v1.2.1

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht
8080
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
8181
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
8282
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
83-
github.com/code-payments/code-protobuf-api v1.19.1-0.20250610140050-4cadbcc86f16 h1:drAMKRdbyObW8E4H6xc1pKIDxoFYgpaTdMlEnIKBIJ0=
84-
github.com/code-payments/code-protobuf-api v1.19.1-0.20250610140050-4cadbcc86f16/go.mod h1:ee6TzKbgMS42ZJgaFEMG3c4R3dGOiffHSu6MrY7WQvs=
83+
github.com/code-payments/code-protobuf-api v1.19.1-0.20250618155621-c66659ab4ff5 h1:fsZBRUCPGSZQngq/1rsJcEwhiYrZokr0wketRRTgGmI=
84+
github.com/code-payments/code-protobuf-api v1.19.1-0.20250618155621-c66659ab4ff5/go.mod h1:ee6TzKbgMS42ZJgaFEMG3c4R3dGOiffHSu6MrY7WQvs=
8585
github.com/code-payments/code-vm-indexer v0.1.11-0.20241028132209-23031e814fba h1:Bkp+gmeb6Y2PWXfkSCTMBGWkb2P1BujRDSjWeI+0j5I=
8686
github.com/code-payments/code-vm-indexer v0.1.11-0.20241028132209-23031e814fba/go.mod h1:jSiifpiBpyBQ8q2R0MGEbkSgWC6sbdRTyDBntmW+j1E=
8787
github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw=

pkg/code/aml/guard.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ func (g *Guard) AllowMoneyMovement(ctx context.Context, intentRecord *intent.Rec
6363
case intent.ReceivePaymentsPublicly:
6464
// Public receives are always allowed
6565
return true, nil
66+
case intent.PublicDistribution:
67+
// Public distributions are always allowed
68+
return true, nil
6669
default:
6770
err := errors.New("intent record must be a send or receive payment")
6871
tracer.OnError(err)

pkg/code/antispam/guard.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package antispam
33
import (
44
"context"
55

6+
transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2"
7+
68
"github.com/code-payments/code-server/pkg/code/common"
79
"github.com/code-payments/code-server/pkg/metrics"
810
)
@@ -15,11 +17,11 @@ func NewGuard(integration Integration) *Guard {
1517
return &Guard{integration: integration}
1618
}
1719

18-
func (g *Guard) AllowOpenAccounts(ctx context.Context, owner *common.Account) (bool, error) {
20+
func (g *Guard) AllowOpenAccounts(ctx context.Context, owner *common.Account, accountSet transactionpb.OpenAccountsMetadata_AccountSet) (bool, error) {
1921
tracer := metrics.TraceMethodCall(ctx, metricsStructName, "AllowOpenAccounts")
2022
defer tracer.End()
2123

22-
allow, reason, err := g.integration.AllowOpenAccounts(ctx, owner)
24+
allow, reason, err := g.integration.AllowOpenAccounts(ctx, owner, accountSet)
2325
if err != nil {
2426
return false, err
2527
}
@@ -70,3 +72,17 @@ func (g *Guard) AllowReceivePayments(ctx context.Context, owner *common.Account,
7072
}
7173
return allow, nil
7274
}
75+
76+
func (g *Guard) AllowDistribution(ctx context.Context, owner *common.Account, isPublic bool) (bool, error) {
77+
tracer := metrics.TraceMethodCall(ctx, metricsStructName, "AllowDistribution")
78+
defer tracer.End()
79+
80+
allow, reason, err := g.integration.AllowDistribution(ctx, owner, isPublic)
81+
if err != nil {
82+
return false, err
83+
}
84+
if !allow {
85+
recordDenialEvent(ctx, actionDistribution, reason)
86+
}
87+
return allow, nil
88+
}

pkg/code/antispam/integration.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,23 @@ package antispam
33
import (
44
"context"
55

6+
transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2"
7+
68
"github.com/code-payments/code-server/pkg/code/common"
79
)
810

911
// Integration is an antispam guard integration that apps can implement to check
1012
// whether operations of interest are allowed to be performed.
1113
type Integration interface {
12-
AllowOpenAccounts(ctx context.Context, owner *common.Account) (bool, string, error)
14+
AllowOpenAccounts(ctx context.Context, owner *common.Account, accountSet transactionpb.OpenAccountsMetadata_AccountSet) (bool, string, error)
1315

1416
AllowWelcomeBonus(ctx context.Context, owner *common.Account) (bool, string, error)
1517

1618
AllowSendPayment(ctx context.Context, owner, destination *common.Account, isPublic bool) (bool, string, error)
1719

1820
AllowReceivePayments(ctx context.Context, owner *common.Account, isPublic bool) (bool, string, error)
21+
22+
AllowDistribution(ctx context.Context, owner *common.Account, isPublic bool) (bool, string, error)
1923
}
2024

2125
type allowEverythingIntegration struct {
@@ -26,7 +30,7 @@ func NewAllowEverything() Integration {
2630
return &allowEverythingIntegration{}
2731
}
2832

29-
func (i *allowEverythingIntegration) AllowOpenAccounts(ctx context.Context, owner *common.Account) (bool, string, error) {
33+
func (i *allowEverythingIntegration) AllowOpenAccounts(ctx context.Context, owner *common.Account, accountSet transactionpb.OpenAccountsMetadata_AccountSet) (bool, string, error) {
3034
return true, "", nil
3135
}
3236

@@ -41,3 +45,7 @@ func (i *allowEverythingIntegration) AllowSendPayment(ctx context.Context, owner
4145
func (i *allowEverythingIntegration) AllowReceivePayments(ctx context.Context, owner *common.Account, isPublic bool) (bool, string, error) {
4246
return true, "", nil
4347
}
48+
49+
func (i *allowEverythingIntegration) AllowDistribution(ctx context.Context, owner *common.Account, isPublic bool) (bool, string, error) {
50+
return true, "", nil
51+
}

pkg/code/antispam/metrics.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const (
1414
actionOpenAccounts = "OpenAccounts"
1515
actionSendPayment = "SendPayment"
1616
actionReceivePayments = "ReceivePayments"
17+
actionDistribution = "Distribution"
1718

1819
actionWelcomeBonus = "WelcomeBonus"
1920
)

pkg/code/async/account/gift_card.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ func InitiateProcessToAutoReturnGiftCard(ctx context.Context, data code_data.Pro
211211
return err
212212
}
213213

214-
return balanceLock.OnCommit(ctx, data)
214+
return balanceLock.OnNewBalanceVersion(ctx, data)
215215
})
216216
}
217217

pkg/code/async/sequencer/intent_handler.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,37 @@ func (h *ReceivePaymentsPubliclyIntentHandler) OnActionUpdated(ctx context.Conte
125125
return nil
126126
}
127127

128+
type PublicDistributionIntentHandler struct {
129+
data code_data.Provider
130+
}
131+
132+
func NewPublicDistributionIntentHandler(data code_data.Provider) IntentHandler {
133+
return &PublicDistributionIntentHandler{
134+
data: data,
135+
}
136+
}
137+
138+
func (h *PublicDistributionIntentHandler) OnActionUpdated(ctx context.Context, intentId string) error {
139+
actionRecords, err := h.data.GetAllActionsByIntent(ctx, intentId)
140+
if err != nil {
141+
return err
142+
}
143+
144+
for _, actionRecord := range actionRecords {
145+
// Intent is failed if at least one transfer or withdraw action fails
146+
if actionRecord.State == action.StateFailed {
147+
return markIntentFailed(ctx, h.data, intentId)
148+
}
149+
150+
if actionRecord.State != action.StateConfirmed {
151+
return nil
152+
}
153+
}
154+
155+
// Intent is confirmed when all transfer and withdraw actions are confirmed
156+
return markIntentConfirmed(ctx, h.data, intentId)
157+
}
158+
128159
func validateIntentState(record *intent.Record, states ...intent.State) error {
129160
for _, validState := range states {
130161
if record.State == validState {
@@ -176,5 +207,6 @@ func getIntentHandlers(data code_data.Provider) map[intent.Type]IntentHandler {
176207
handlersByType[intent.OpenAccounts] = NewOpenAccountsIntentHandler(data)
177208
handlersByType[intent.SendPublicPayment] = NewSendPublicPaymentIntentHandler(data)
178209
handlersByType[intent.ReceivePaymentsPublicly] = NewReceivePaymentsPubliclyIntentHandler(data)
210+
handlersByType[intent.PublicDistribution] = NewPublicDistributionIntentHandler(data)
179211
return handlersByType
180212
}

pkg/code/balance/lock.go

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ package balance
22

33
import (
44
"context"
5+
"errors"
56

67
"github.com/code-payments/code-server/pkg/code/common"
78
code_data "github.com/code-payments/code-server/pkg/code/data"
9+
"github.com/code-payments/code-server/pkg/code/data/balance"
810
)
911

1012
// OptimisticVersionLock is an optimistic version lock on an account's cached
@@ -28,7 +30,46 @@ func GetOptimisticVersionLock(ctx context.Context, data code_data.Provider, vaul
2830
}, nil
2931
}
3032

31-
// OnCommit is called in the DB transaction updating the account's cached balance
32-
func (l *OptimisticVersionLock) OnCommit(ctx context.Context, data code_data.Provider) error {
33+
// OnNewBalanceVersion is called in the DB transaction updating the account's
34+
// cached balance
35+
func (l *OptimisticVersionLock) OnNewBalanceVersion(ctx context.Context, data code_data.Provider) error {
3336
return data.AdvanceCachedBalanceVersion(ctx, l.vault.PublicKey().ToBase58(), l.currentVersion)
3437
}
38+
39+
// RequireSameBalanceVerion is called in the DB transaction requireing the
40+
// account's cached balance not be changed
41+
func (l *OptimisticVersionLock) RequireSameBalanceVerion(ctx context.Context, data code_data.Provider) error {
42+
latestVersion, err := data.GetCachedBalanceVersion(ctx, l.vault.PublicKey().ToBase58())
43+
if err != nil {
44+
return err
45+
}
46+
if latestVersion < l.currentVersion {
47+
return errors.New("unexpected balance version detected")
48+
}
49+
if l.currentVersion != latestVersion {
50+
return balance.ErrStaleCachedBalanceVersion
51+
}
52+
return nil
53+
}
54+
55+
// OpenCloseStatusLock is a lock on an account's open/close status
56+
type OpenCloseStatusLock struct {
57+
vault *common.Account
58+
}
59+
60+
func NewOpenCloseStatusLock(vault *common.Account) *OpenCloseStatusLock {
61+
return &OpenCloseStatusLock{
62+
vault: vault,
63+
}
64+
}
65+
66+
// OnPaymentToAccount is called in the DB transaction making a payment to the
67+
// account that may be closed
68+
func (l *OpenCloseStatusLock) OnPaymentToAccount(ctx context.Context, data code_data.Provider) error {
69+
return data.CheckNotClosedForBalanceUpdate(ctx, l.vault.PublicKey().ToBase58())
70+
}
71+
72+
// OnClose is called in the DB transaction closing the account
73+
func (l *OpenCloseStatusLock) OnClose(ctx context.Context, data code_data.Provider) error {
74+
return data.MarkAsClosedForBalanceUpdate(ctx, l.vault.PublicKey().ToBase58())
75+
}

pkg/code/common/owner_test.go

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ func TestGetLatestTokenAccountRecordsForOwner(t *testing.T) {
136136
authority3 := newRandomTestAccount(t)
137137
authority4 := newRandomTestAccount(t)
138138
authority5 := newRandomTestAccount(t)
139+
authority6 := newRandomTestAccount(t)
140+
authority7 := newRandomTestAccount(t)
139141

140142
for _, authorityAndType := range []struct {
141143
account *Account
@@ -195,9 +197,30 @@ func TestGetLatestTokenAccountRecordsForOwner(t *testing.T) {
195197
}
196198
require.NoError(t, data.CreateAccountInfo(ctx, swapAccountInfoRecord))
197199

200+
for i, authority := range []*Account{
201+
authority6,
202+
authority7,
203+
} {
204+
timelockAccounts, err := authority.GetTimelockAccounts(vmAccount, coreMintAccount)
205+
require.NoError(t, err)
206+
207+
timelockRecord := timelockAccounts.ToDBRecord()
208+
require.NoError(t, data.SaveTimelock(ctx, timelockRecord))
209+
210+
accountInfoRecord := &account.Record{
211+
OwnerAccount: owner.PublicKey().ToBase58(),
212+
AuthorityAccount: timelockRecord.VaultOwner,
213+
TokenAccount: timelockRecord.VaultAddress,
214+
MintAccount: coreMintAccount.PublicKey().ToBase58(),
215+
AccountType: commonpb.AccountType_POOL,
216+
Index: uint64(i),
217+
}
218+
require.NoError(t, data.CreateAccountInfo(ctx, accountInfoRecord))
219+
}
220+
198221
actual, err = GetLatestTokenAccountRecordsForOwner(ctx, data, owner)
199222
require.NoError(t, err)
200-
require.Len(t, actual, 4)
223+
require.Len(t, actual, 5)
201224

202225
records, ok := actual[commonpb.AccountType_BUCKET_1_KIN]
203226
require.True(t, ok)
@@ -238,4 +261,20 @@ func TestGetLatestTokenAccountRecordsForOwner(t *testing.T) {
238261
assert.Equal(t, records[0].General.AuthorityAccount, authority5.PublicKey().ToBase58())
239262
assert.Equal(t, records[0].General.TokenAccount, swapAta.PublicKey().ToBase58())
240263
assert.Equal(t, records[0].General.AccountType, commonpb.AccountType_SWAP)
264+
265+
records, ok = actual[commonpb.AccountType_POOL]
266+
require.True(t, ok)
267+
require.Len(t, records, 2)
268+
269+
assert.Equal(t, records[0].General.AuthorityAccount, authority6.PublicKey().ToBase58())
270+
assert.Equal(t, records[0].General.AccountType, commonpb.AccountType_POOL)
271+
assert.Equal(t, records[0].Timelock.VaultOwner, authority6.PublicKey().ToBase58())
272+
assert.Equal(t, records[0].General.TokenAccount, records[0].Timelock.VaultAddress)
273+
assert.EqualValues(t, records[0].General.Index, 0)
274+
275+
assert.Equal(t, records[1].General.AuthorityAccount, authority7.PublicKey().ToBase58())
276+
assert.Equal(t, records[1].General.AccountType, commonpb.AccountType_POOL)
277+
assert.Equal(t, records[1].Timelock.VaultOwner, authority7.PublicKey().ToBase58())
278+
assert.Equal(t, records[1].General.TokenAccount, records[1].Timelock.VaultAddress)
279+
assert.EqualValues(t, records[1].General.Index, 1)
241280
}

pkg/code/data/account/acccount_info.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ var AllAccountTypes = []commonpb.AccountType{
2424
commonpb.AccountType_REMOTE_SEND_GIFT_CARD,
2525
commonpb.AccountType_RELATIONSHIP,
2626
commonpb.AccountType_SWAP,
27+
commonpb.AccountType_POOL,
2728
}
2829

2930
type Record struct {
@@ -180,6 +181,10 @@ func (r *Record) Validate() error {
180181
if r.OwnerAccount == r.AuthorityAccount {
181182
return errors.New("owner cannot be authority for swap account")
182183
}
184+
case commonpb.AccountType_POOL:
185+
if r.OwnerAccount == r.AuthorityAccount {
186+
return errors.New("owner cannot be authority pool account")
187+
}
183188
default:
184189
return errors.Errorf("unhandled account type: %s", r.AccountType.String())
185190
}

pkg/code/data/account/memory/store.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,10 @@ func (s *store) GetLatestByOwnerAddress(_ context.Context, address string) (map[
275275

276276
items := s.findByOwnerAddress(address)
277277
for _, accountType := range account.AllAccountTypes {
278+
if accountType == commonpb.AccountType_POOL {
279+
continue
280+
}
281+
278282
items := s.filterByType(items, accountType)
279283
if len(items) == 0 {
280284
continue
@@ -292,6 +296,11 @@ func (s *store) GetLatestByOwnerAddress(_ context.Context, address string) (map[
292296
}
293297
}
294298

299+
items = s.filterByType(items, commonpb.AccountType_POOL)
300+
if len(items) > 0 {
301+
res[commonpb.AccountType_POOL] = cloneRecords(items)
302+
}
303+
295304
if len(res) == 0 {
296305
return nil, account.ErrAccountInfoNotFound
297306
}

pkg/code/data/account/postgres/model.go

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -210,27 +210,47 @@ func dbGetByAuthorityAddress(ctx context.Context, db *sqlx.DB, address string) (
210210
}
211211

212212
func dbGetLatestByOwnerAddress(ctx context.Context, db *sqlx.DB, address string) ([]*model, error) {
213-
var res []*model
213+
var res1 []*model
214214

215-
query := `SELECT DISTINCT ON (account_type, relationship_to) id, owner_account, authority_account, token_account, mint_account, account_type, index, relationship_to, requires_deposit_sync, deposits_last_synced_at, requires_auto_return_check, requires_swap_retry, last_swap_retry_at, created_at FROM ` + tableName + `
216-
WHERE owner_account = $1
215+
query1 := `SELECT DISTINCT ON (account_type, relationship_to) id, owner_account, authority_account, token_account, mint_account, account_type, index, relationship_to, requires_deposit_sync, deposits_last_synced_at, requires_auto_return_check, requires_swap_retry, last_swap_retry_at, created_at FROM ` + tableName + `
216+
WHERE owner_account = $1 AND account_type NOT IN ($2)
217217
ORDER BY account_type, relationship_to, index DESC
218218
`
219219

220220
err := db.SelectContext(
221221
ctx,
222-
&res,
223-
query,
222+
&res1,
223+
query1,
224224
address,
225+
commonpb.AccountType_POOL,
225226
)
226-
if err != nil {
227-
return nil, pgutil.CheckNoRows(err, account.ErrAccountInfoNotFound)
227+
if err != nil && !pgutil.IsNoRows(err) {
228+
return nil, err
229+
}
230+
231+
var res2 []*model
232+
233+
query2 := `SELECT id, owner_account, authority_account, token_account, mint_account, account_type, index, relationship_to, requires_deposit_sync, deposits_last_synced_at, requires_auto_return_check, requires_swap_retry, last_swap_retry_at, created_at FROM ` + tableName + `
234+
WHERE owner_account = $1 AND account_type IN ($2)
235+
ORDER BY index ASC
236+
`
237+
err = db.SelectContext(
238+
ctx,
239+
&res2,
240+
query2,
241+
address,
242+
commonpb.AccountType_POOL,
243+
)
244+
if err != nil && !pgutil.IsNoRows(err) {
245+
return nil, err
228246
}
229247

248+
var res []*model
249+
res = append(res, res1...)
250+
res = append(res, res2...)
230251
if len(res) == 0 {
231252
return nil, account.ErrAccountInfoNotFound
232253
}
233-
234254
return res, nil
235255
}
236256

0 commit comments

Comments
 (0)