diff --git a/ocp/currency/data_provider.go b/ocp/currency/data_provider.go index d777a6c..7d30600 100644 --- a/ocp/currency/data_provider.go +++ b/ocp/currency/data_provider.go @@ -3,6 +3,7 @@ package currency import ( "context" "crypto/ed25519" + "fmt" "strings" "sync" "time" @@ -22,6 +23,7 @@ import ( "github.com/code-payments/ocp-server/ocp/data/currency" "github.com/code-payments/ocp-server/solana/currencycreator" timelock_token "github.com/code-payments/ocp-server/solana/timelock/v1" + "github.com/code-payments/ocp-server/usdc" ) // LiveExchangeRateData represents live exchange rate data with its pre-signed response @@ -368,6 +370,16 @@ func (m *MintDataProvider) GetProtoMint(ctx context.Context, mint *common.Accoun }, CreatedAt: timestamppb.New(time.Time{}), } + case usdc.Mint: + protoMetadata = ¤cypb.Mint{ + Address: mint.ToProto(), + Decimals: uint32(usdc.Decimals), + Name: usdc.Name, + Symbol: usdc.Symbol, + Description: " ", + ImageUrl: fmt.Sprintf("%s/%s/icon.png", config.CurrencyAssetsBaseUrl, usdc.Mint), + CreatedAt: timestamppb.New(time.Time{}), + } default: var err error metadataRecord, ok := m.getCachedCurrencyMetadata(mint) diff --git a/ocp/rpc/account/server.go b/ocp/rpc/account/server.go index 40dd642..c0d69c1 100644 --- a/ocp/rpc/account/server.go +++ b/ocp/rpc/account/server.go @@ -23,10 +23,12 @@ import ( "github.com/code-payments/ocp-server/ocp/common" currency_util "github.com/code-payments/ocp-server/ocp/currency" ocp_data "github.com/code-payments/ocp-server/ocp/data" + "github.com/code-payments/ocp-server/ocp/data/account" "github.com/code-payments/ocp-server/ocp/data/action" "github.com/code-payments/ocp-server/ocp/rpc" account_worker "github.com/code-payments/ocp-server/ocp/worker/account" timelock_token_v1 "github.com/code-payments/ocp-server/solana/timelock/v1" + "github.com/code-payments/ocp-server/usdc" ) var ( @@ -166,6 +168,7 @@ func (s *server) GetTokenAccountInfos(ctx context.Context, req *accountpb.GetTok } var hasGiftCardAccount bool + var hasPrimaryAccount bool var allRecords []*common.AccountRecords for _, recordsByType := range allRecordsByMintAndType { for _, batchRecords := range recordsByType { @@ -173,11 +176,25 @@ func (s *server) GetTokenAccountInfos(ctx context.Context, req *accountpb.GetTok if records.General.AccountType == commonpb.AccountType_REMOTE_SEND_GIFT_CARD { hasGiftCardAccount = true } + if records.General.AccountType == commonpb.AccountType_PRIMARY { + hasPrimaryAccount = true + } allRecords = append(allRecords, records) } } } + // Owners with a PRIMARY account also expose their external USDC associated + // token account so clients can observe the off-L2 USDC balance. + if hasPrimaryAccount { + usdcAtaRecords, err := buildUsdcAtaRecords(owner) + if err != nil { + log.With(zap.Error(err)).Warn("failure building usdc ata records") + return nil, status.Error(codes.Internal, "") + } + allRecords = append(allRecords, usdcAtaRecords) + } + // Filter account records based on client request // // todo: this needs tests @@ -487,7 +504,14 @@ func (s *server) getProtoAccountInfo(ctx context.Context, records *common.Accoun var liveReserveState *currencypb.VerifiedLaunchpadCurrencyReserveState switch records.General.AccountType { case commonpb.AccountType_SWAP, commonpb.AccountType_ASSOCIATED_TOKEN_ACCOUNT: - // Unused account types, which likely don't have any mint metadata ATM. + // Well-known mints we know we have metadata for + switch mintAccount.PublicKey().ToBase58() { + case usdc.Mint: + mintMetadata, err = s.mintDataProvider.GetProtoMint(ctx, mintAccount) + if err != nil { + return nil, err + } + } default: mintMetadata, err = s.mintDataProvider.GetProtoMint(ctx, mintAccount) if err != nil { @@ -576,6 +600,28 @@ func (s *server) addRequestingOwnerMetadata(ctx context.Context, resp *accountpb return cloned, nil } +func buildUsdcAtaRecords(owner *common.Account) (*common.AccountRecords, error) { + usdcMint, err := common.NewAccountFromPublicKeyString(usdc.Mint) + if err != nil { + return nil, err + } + + ata, err := owner.ToAssociatedTokenAccount(usdcMint) + if err != nil { + return nil, err + } + + return &common.AccountRecords{ + General: &account.Record{ + OwnerAccount: owner.PublicKey().ToBase58(), + AuthorityAccount: owner.PublicKey().ToBase58(), + TokenAccount: ata.PublicKey().ToBase58(), + MintAccount: usdcMint.PublicKey().ToBase58(), + AccountType: commonpb.AccountType_ASSOCIATED_TOKEN_ACCOUNT, + }, + }, nil +} + func (s *server) updateCachedResponse(resp *accountpb.GetTokenAccountInfosResponse) { for _, ai := range resp.TokenAccountInfos { switch ai.AccountType { diff --git a/ocp/rpc/account/server_test.go b/ocp/rpc/account/server_test.go index feec815..6ea5205 100644 --- a/ocp/rpc/account/server_test.go +++ b/ocp/rpc/account/server_test.go @@ -32,6 +32,7 @@ import ( "github.com/code-payments/ocp-server/solana/currencycreator" timelock_token_v1 "github.com/code-payments/ocp-server/solana/timelock/v1" "github.com/code-payments/ocp-server/testutil" + "github.com/code-payments/ocp-server/usdc" ) type testEnv struct { @@ -187,7 +188,29 @@ func TestGetTokenAccountInfos_UserAccounts_HappyPath(t *testing.T) { resp, err := env.client.GetTokenAccountInfos(env.ctx, req) require.NoError(t, err) assert.Equal(t, accountpb.GetTokenAccountInfosResponse_OK, resp.Result) - assert.Len(t, resp.TokenAccountInfos, 5) + assert.Len(t, resp.TokenAccountInfos, 6) + + usdcMint, err := common.NewAccountFromPublicKeyString(usdc.Mint) + require.NoError(t, err) + usdcAta, err := ownerAccount.ToAssociatedTokenAccount(usdcMint) + require.NoError(t, err) + usdcAtaInfo, ok := resp.TokenAccountInfos[usdcAta.PublicKey().ToBase58()] + require.True(t, ok) + assert.Equal(t, commonpb.AccountType_ASSOCIATED_TOKEN_ACCOUNT, usdcAtaInfo.AccountType) + assert.Equal(t, ownerAccount.PublicKey().ToBytes(), usdcAtaInfo.Owner.Value) + assert.Equal(t, ownerAccount.PublicKey().ToBytes(), usdcAtaInfo.Authority.Value) + assert.Equal(t, usdcMint.PublicKey().ToBytes(), usdcAtaInfo.Mint.Value) + assert.Equal(t, accountpb.TokenAccountInfo_BALANCE_SOURCE_BLOCKCHAIN, usdcAtaInfo.BalanceSource) + assert.Equal(t, accountpb.TokenAccountInfo_MANAGEMENT_STATE_NONE, usdcAtaInfo.ManagementState) + assert.Equal(t, accountpb.TokenAccountInfo_BLOCKCHAIN_STATE_UNKNOWN, usdcAtaInfo.BlockchainState) + require.NotNil(t, usdcAtaInfo.MintMetadata) + assert.Equal(t, usdcMint.PublicKey().ToBytes(), usdcAtaInfo.MintMetadata.Address.Value) + assert.EqualValues(t, usdc.Decimals, usdcAtaInfo.MintMetadata.Decimals) + assert.Equal(t, usdc.Name, usdcAtaInfo.MintMetadata.Name) + assert.Equal(t, usdc.Symbol, usdcAtaInfo.MintMetadata.Symbol) + assert.Nil(t, usdcAtaInfo.MintMetadata.VmMetadata) + assert.Nil(t, usdcAtaInfo.MintMetadata.LaunchpadMetadata) + assert.Nil(t, usdcAtaInfo.LiveReserveState) for _, tc := range []struct { authority *common.Account @@ -606,7 +629,7 @@ func TestGetTokenAccountInfos_BlockchainState(t *testing.T) { resp, err := env.client.GetTokenAccountInfos(env.ctx, req) require.NoError(t, err) assert.Equal(t, accountpb.GetTokenAccountInfosResponse_OK, resp.Result) - assert.Len(t, resp.TokenAccountInfos, 1) + assert.Len(t, resp.TokenAccountInfos, 2) accountInfo, ok := resp.TokenAccountInfos[accountRecords.Timelock.VaultAddress] require.True(t, ok) @@ -674,7 +697,7 @@ func TestGetTokenAccountInfos_ManagementState(t *testing.T) { resp, err := env.client.GetTokenAccountInfos(env.ctx, req) require.NoError(t, err) assert.Equal(t, accountpb.GetTokenAccountInfosResponse_OK, resp.Result) - assert.Len(t, resp.TokenAccountInfos, 1) + assert.Len(t, resp.TokenAccountInfos, 2) accountInfo, ok := resp.TokenAccountInfos[accountRecords.Timelock.VaultAddress] require.True(t, ok) diff --git a/usdc/usdc.go b/usdc/usdc.go index 24c8902..68ecdd7 100644 --- a/usdc/usdc.go +++ b/usdc/usdc.go @@ -1,7 +1,11 @@ package usdc const ( - Mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" + Name = "USDC" + Symbol = "USDC" + + Mint = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" + QuarksPerUsdc = 1000000 Decimals = 6 )