Skip to content

Commit 7dc8cf0

Browse files
committed
Support mult-mint and multi-vm withdrawals
1 parent 5fdbe3a commit 7dc8cf0

File tree

10 files changed

+350
-282
lines changed

10 files changed

+350
-282
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.20250919185855-0de34de496c2
9+
github.com/code-payments/code-protobuf-api v1.19.1-0.20250924165402-83d54412d56c
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.20250919185855-0de34de496c2 h1:IQJnBLjMUNgL2Drqck86/rtsgL38uCQ+vgEUzJbUsL0=
84-
github.com/code-payments/code-protobuf-api v1.19.1-0.20250919185855-0de34de496c2/go.mod h1:ee6TzKbgMS42ZJgaFEMG3c4R3dGOiffHSu6MrY7WQvs=
83+
github.com/code-payments/code-protobuf-api v1.19.1-0.20250924165402-83d54412d56c h1:jdUYprpI1fYewZSvzw1MyFzv19l364cwasLkTPOkTP4=
84+
github.com/code-payments/code-protobuf-api v1.19.1-0.20250924165402-83d54412d56c/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/async/sequencer/fulfillment_handler.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -824,7 +824,7 @@ func isAccountInitialized(ctx context.Context, data code_data.Provider, address
824824
timelockRecord, err := data.GetTimelockByVault(ctx, address)
825825
if err == timelock.ErrTimelockNotFound {
826826
// Likely not a Code timelock account, so defer to the blockchain
827-
_, err := data.GetBlockchainTokenAccountInfo(ctx, address, solana.CommitmentFinalized)
827+
_, err := data.GetBlockchainAccountInfo(ctx, address, solana.CommitmentFinalized)
828828
if err == solana.ErrNoAccountInfo || err == token.ErrAccountNotFound {
829829
return false, nil
830830
} else if err != nil {

pkg/code/common/account.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -432,9 +432,9 @@ func (a *TimelockAccounts) GetInitializeInstruction(vmAuthority, memory *Account
432432
), nil
433433
}
434434

435-
// ValidateExternalTokenAccount validates an address is an external token account for the core mint
436-
func ValidateExternalTokenAccount(ctx context.Context, data code_data.Provider, tokenAccount *Account) (bool, string, error) {
437-
_, err := data.GetBlockchainTokenAccountInfo(ctx, tokenAccount.PublicKey().ToBase58(), solana.CommitmentFinalized)
435+
// ValidateExternalTokenAccount validates an address is an external token account for the provided mint
436+
func ValidateExternalTokenAccount(ctx context.Context, data code_data.Provider, tokenAccount, mintAccount *Account) (bool, string, error) {
437+
_, err := data.GetBlockchainTokenAccountInfo(ctx, tokenAccount.PublicKey().ToBase58(), mintAccount.PublicKey().ToBase58(), solana.CommitmentFinalized)
438438
switch err {
439439
case nil:
440440
// Double check there were no race conditions between other SubmitIntent
@@ -452,7 +452,7 @@ func ValidateExternalTokenAccount(ctx context.Context, data code_data.Provider,
452452
case solana.ErrNoAccountInfo, token.ErrAccountNotFound:
453453
return false, fmt.Sprintf("%s doesn't exist on the blockchain", tokenAccount.PublicKey().ToBase58()), nil
454454
case token.ErrInvalidTokenAccount:
455-
return false, fmt.Sprintf("%s is not a core mint account", tokenAccount.PublicKey().ToBase58()), nil
455+
return false, fmt.Sprintf("%s is not of %s mint", tokenAccount.PublicKey().ToBase58(), mintAccount.PublicKey().ToBase58()), nil
456456
default:
457457
// Unfortunate if Solana is down, but this only impacts withdraw flows,
458458
// and we need to guarantee this isn't going to something that's not

pkg/code/data/blockchain.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ type BlockchainData interface {
3232
GetBlockchainLatestBlockhash(ctx context.Context) (solana.Blockhash, error)
3333
GetBlockchainSignatureStatuses(ctx context.Context, signatures []solana.Signature) ([]*solana.SignatureStatus, error)
3434
GetBlockchainSlot(ctx context.Context, commitment solana.Commitment) (uint64, error)
35-
GetBlockchainTokenAccountInfo(ctx context.Context, account string, commitment solana.Commitment) (*token.Account, error)
35+
GetBlockchainTokenAccountInfo(ctx context.Context, account, mint string, commitment solana.Commitment) (*token.Account, error)
3636
GetBlockchainTokenAccountsByOwner(ctx context.Context, account string) ([]ed25519.PublicKey, error)
3737
GetBlockchainTransaction(ctx context.Context, sig string, commitment solana.Commitment) (*solana.ConfirmedTransaction, error)
3838
GetBlockchainTransactionTokenBalances(ctx context.Context, sig string) (*solana.TransactionTokenBalances, error)
@@ -46,7 +46,7 @@ type BlockchainProvider struct {
4646

4747
func NewBlockchainProvider(solanaEndpoint string) (BlockchainData, error) {
4848
sc := solana.New(solanaEndpoint)
49-
tc := token.NewClient(sc, config.CoreMintPublicKeyBytes)
49+
tc := token.NewClient(sc)
5050

5151
return &BlockchainProvider{
5252
sc: sc,
@@ -117,15 +117,21 @@ func (dp *BlockchainProvider) GetBlockchainAccountDataAfterBlock(ctx context.Con
117117
}
118118
return data, block, err
119119
}
120-
func (dp *BlockchainProvider) GetBlockchainTokenAccountInfo(ctx context.Context, account string, commitment solana.Commitment) (*token.Account, error) {
120+
func (dp *BlockchainProvider) GetBlockchainTokenAccountInfo(ctx context.Context, account, mint string, commitment solana.Commitment) (*token.Account, error) {
121121
tracer := metrics.TraceMethodCall(ctx, blockchainProviderMetricsName, "GetBlockchainTokenAccountInfo")
122122
defer tracer.End()
123123

124124
accountId, err := base58.Decode(account)
125125
if err != nil {
126126
return nil, err
127127
}
128-
res, err := dp.tc.GetAccount(accountId, commitment)
128+
129+
mintId, err := base58.Decode(mint)
130+
if err != nil {
131+
return nil, err
132+
}
133+
134+
res, err := dp.tc.GetAccount(accountId, mintId, commitment)
129135

130136
if err != nil {
131137
tracer.OnError(err)
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package transaction_v2
2+
3+
import (
4+
"context"
5+
"time"
6+
7+
"google.golang.org/grpc/codes"
8+
"google.golang.org/grpc/status"
9+
10+
commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1"
11+
transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2"
12+
13+
async_account "github.com/code-payments/code-server/pkg/code/async/account"
14+
"github.com/code-payments/code-server/pkg/code/balance"
15+
"github.com/code-payments/code-server/pkg/code/common"
16+
"github.com/code-payments/code-server/pkg/code/data/account"
17+
"github.com/code-payments/code-server/pkg/code/data/action"
18+
"github.com/code-payments/code-server/pkg/grpc/client"
19+
)
20+
21+
func (s *transactionServer) VoidGiftCard(ctx context.Context, req *transactionpb.VoidGiftCardRequest) (*transactionpb.VoidGiftCardResponse, error) {
22+
log := s.log.WithField("method", "VoidGiftCard")
23+
log = client.InjectLoggingMetadata(ctx, log)
24+
25+
owner, err := common.NewAccountFromProto(req.Owner)
26+
if err != nil {
27+
log.WithError(err).Warn("invalid owner account")
28+
return nil, status.Error(codes.Internal, "")
29+
}
30+
log = log.WithField("owner_account", owner.PublicKey().ToBase58())
31+
32+
giftCardVault, err := common.NewAccountFromProto(req.GiftCardVault)
33+
if err != nil {
34+
log.WithError(err).Warn("invalid owner account")
35+
return nil, status.Error(codes.Internal, "")
36+
}
37+
log = log.WithField("gift_card_vault_account", giftCardVault.PublicKey().ToBase58())
38+
39+
signature := req.Signature
40+
req.Signature = nil
41+
if err := s.auth.Authenticate(ctx, owner, req, signature); err != nil {
42+
return nil, err
43+
}
44+
45+
accountInfoRecord, err := s.data.GetAccountInfoByTokenAddress(ctx, giftCardVault.PublicKey().ToBase58())
46+
switch err {
47+
case nil:
48+
if accountInfoRecord.AccountType != commonpb.AccountType_REMOTE_SEND_GIFT_CARD {
49+
return &transactionpb.VoidGiftCardResponse{
50+
Result: transactionpb.VoidGiftCardResponse_NOT_FOUND,
51+
}, nil
52+
}
53+
case account.ErrAccountInfoNotFound:
54+
return &transactionpb.VoidGiftCardResponse{
55+
Result: transactionpb.VoidGiftCardResponse_NOT_FOUND,
56+
}, nil
57+
default:
58+
log.WithError(err).Warn("failure getting gift card account info")
59+
return nil, status.Error(codes.Internal, "")
60+
}
61+
62+
giftCardIssuedIntentRecord, err := s.data.GetOriginalGiftCardIssuedIntent(ctx, giftCardVault.PublicKey().ToBase58())
63+
if err != nil {
64+
log.WithError(err).Warn("failure getting gift card issued intent record")
65+
return nil, status.Error(codes.Internal, "")
66+
} else if giftCardIssuedIntentRecord.InitiatorOwnerAccount != owner.PublicKey().ToBase58() {
67+
return &transactionpb.VoidGiftCardResponse{
68+
Result: transactionpb.VoidGiftCardResponse_DENIED,
69+
}, nil
70+
}
71+
72+
if time.Since(accountInfoRecord.CreatedAt) >= async_account.GiftCardExpiry {
73+
return &transactionpb.VoidGiftCardResponse{
74+
Result: transactionpb.VoidGiftCardResponse_OK,
75+
}, nil
76+
}
77+
78+
globalBalanceLock, err := balance.GetOptimisticVersionLock(ctx, s.data, giftCardVault)
79+
if err != nil {
80+
log.WithError(err).Warn("failure getting balance lock")
81+
return nil, status.Error(codes.Internal, "")
82+
}
83+
84+
localAccountLock := s.getLocalAccountLock(giftCardVault)
85+
localAccountLock.Lock()
86+
defer localAccountLock.Unlock()
87+
88+
claimedActionRecord, err := s.data.GetGiftCardClaimedAction(ctx, giftCardVault.PublicKey().ToBase58())
89+
if err == nil {
90+
mintAccount, err := common.NewAccountFromPublicKeyString(accountInfoRecord.MintAccount)
91+
if err != nil {
92+
log.WithError(err).Warn("invalid mint account")
93+
return nil, status.Error(codes.Internal, "")
94+
}
95+
96+
vmConfig, err := common.GetVmConfigForMint(ctx, s.data, mintAccount)
97+
if err != nil {
98+
log.WithError(err).Warn("failure getting vm config")
99+
return nil, status.Error(codes.Internal, "")
100+
}
101+
102+
ownerTimelockAccounts, err := owner.GetTimelockAccounts(vmConfig)
103+
if err != nil {
104+
log.WithError(err).Warn("failure getting owner timelock accounts")
105+
return nil, status.Error(codes.Internal, "")
106+
}
107+
108+
if *claimedActionRecord.Destination != ownerTimelockAccounts.Vault.PublicKey().ToBase58() {
109+
return &transactionpb.VoidGiftCardResponse{
110+
Result: transactionpb.VoidGiftCardResponse_CLAIMED_BY_OTHER_USER,
111+
}, nil
112+
}
113+
return &transactionpb.VoidGiftCardResponse{
114+
Result: transactionpb.VoidGiftCardResponse_OK,
115+
}, nil
116+
} else if err != action.ErrActionNotFound {
117+
log.WithError(err).Warn("failure getting gift card claimed action")
118+
return nil, status.Error(codes.Internal, "")
119+
}
120+
121+
err = async_account.InitiateProcessToAutoReturnGiftCard(ctx, s.data, giftCardVault, true, globalBalanceLock)
122+
if err != nil {
123+
log.WithError(err).Warn("failure scheduling auto-return action")
124+
return nil, status.Error(codes.Internal, "")
125+
}
126+
127+
// It's ok if this fails, the auto-return worker will just process this account
128+
// idempotently at a later time
129+
async_account.MarkAutoReturnCheckComplete(ctx, s.data, accountInfoRecord)
130+
131+
return &transactionpb.VoidGiftCardResponse{
132+
Result: transactionpb.VoidGiftCardResponse_OK,
133+
}, nil
134+
}

0 commit comments

Comments
 (0)