Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions ocp/data/currency/memory/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ func (s *store) SaveMetadata(ctx context.Context, data *currency.MetadataRecord)
}

for _, item := range s.metadataRecords {
if strings.EqualFold(item.Name, data.Name) {
if strings.EqualFold(item.Name, data.Name) && item.State != currency.MetadataStateAbandoned {
return currency.ErrDuplicateCurrency
}
}
Expand Down Expand Up @@ -336,7 +336,7 @@ func (s *store) IsNameAvailable(_ context.Context, name string) (bool, error) {
defer s.mu.Unlock()

for _, item := range s.metadataRecords {
if strings.EqualFold(item.Name, name) {
if strings.EqualFold(item.Name, name) && item.State != currency.MetadataStateAbandoned {
return false, nil
}
}
Expand Down
6 changes: 6 additions & 0 deletions ocp/data/currency/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const (
MetadataStateExecutingInitialPurchase
MetadataStateCompletingInitialization
MetadataStateFinalValidation
MetadataStateAbandoning
MetadataStateAbandoned
)

type SocialLinkType uint8
Expand Down Expand Up @@ -350,6 +352,10 @@ func (s MetadataState) String() string {
return "completing_initialization"
case MetadataStateFinalValidation:
return "final_validation"
case MetadataStateAbandoning:
return "abandoning"
case MetadataStateAbandoned:
return "abandoned"
}
return "unknown"
}
3 changes: 2 additions & 1 deletion ocp/data/currency/postgres/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -552,8 +552,9 @@ func dbCountMetadataByState(ctx context.Context, db *sqlx.DB, state currency.Met
func dbIsNameAvailable(ctx context.Context, db *sqlx.DB, name string) (bool, error) {
var count uint64
err := db.GetContext(ctx, &count,
`SELECT COUNT(*) FROM `+metadataTableName+` WHERE LOWER(name) = LOWER($1)`,
`SELECT COUNT(*) FROM `+metadataTableName+` WHERE LOWER(name) = LOWER($1) AND state != $2`,
name,
currency.MetadataStateAbandoned,
)
if err != nil {
return false, err
Expand Down
2 changes: 1 addition & 1 deletion ocp/data/currency/postgres/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const (
created_by TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL
);
CREATE UNIQUE INDEX ocp__core_currencymetadata__name__idx ON ocp__core_currencymetadata (LOWER(name));
CREATE UNIQUE INDEX ocp__core_currencymetadata__name__idx ON ocp__core_currencymetadata (LOWER(name)) WHERE state != 8;
CREATE TABLE ocp__core_currencyreserve (
id serial NOT NULL PRIMARY KEY,
Expand Down
109 changes: 109 additions & 0 deletions ocp/data/currency/tests/tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ func RunTests(t *testing.T, s currency.Store, teardown func()) {
testMetadataSaveWithVersioning,
testMetadataUniqueNameConstraint,
testIsNameAvailable,
testAbandonedCurrencyNameReuse,
testGetAllMetadataByState,
testGetAllMints,
testCountMints,
Expand Down Expand Up @@ -349,6 +350,114 @@ func testIsNameAvailable(t *testing.T, s currency.Store) {
assert.True(t, available)
}

func testAbandonedCurrencyNameReuse(t *testing.T, s currency.Store) {
ctx := context.Background()

record := &currency.MetadataRecord{
Name: "AbandonedCurrency",
Symbol: "AB1",
Description: "A currency that will be abandoned",
ImageUrl: "https://example.com/ab1.png",
BillColors: []string{"#000000"},
SocialLinks: []currency.SocialLink{{Type: currency.SocialLinkTypeWebsite, Value: "https://example.com"}},

Seed: "abandonedseed1",
Authority: "abandonedauth1",

Mint: "abandonedmint111111111111111111111111111111111",
MintBump: 255,
Decimals: currencycreator.DefaultMintDecimals,

CurrencyConfig: "abandonedconfig1111111111111111111111111111",
CurrencyConfigBump: 255,

LiquidityPool: "abandonedpool11111111111111111111111111111111",
LiquidityPoolBump: 255,

VaultMint: "abandonedvmint1111111111111111111111111111111",
VaultMintBump: 255,

VaultCore: "abandonedvcore1111111111111111111111111111111",
VaultCoreBump: 255,

SellFeeBps: currencycreator.DefaultSellFeeBps,

Alt: "abandonedalt1111111111111111111111111111111111",

CreatedBy: "abandonedcreator1",
CreatedAt: time.Now(),
}

require.NoError(t, s.SaveMetadata(ctx, record))

// Name should not be available while active
available, err := s.IsNameAvailable(ctx, "AbandonedCurrency")
require.NoError(t, err)
assert.False(t, available)

// Case-insensitive should also not be available
available, err = s.IsNameAvailable(ctx, "abandonedcurrency")
require.NoError(t, err)
assert.False(t, available)

// Transition to abandoned state
record.State = currency.MetadataStateAbandoned
require.NoError(t, s.SaveMetadata(ctx, record))

// Name should now be available
available, err = s.IsNameAvailable(ctx, "AbandonedCurrency")
require.NoError(t, err)
assert.True(t, available)

// Case-insensitive should also be available
available, err = s.IsNameAvailable(ctx, "abandonedcurrency")
require.NoError(t, err)
assert.True(t, available)

// Should be able to create a new currency with the same name
record2 := &currency.MetadataRecord{
Name: "AbandonedCurrency",
Symbol: "AB2",
Description: "Reusing the abandoned name",
ImageUrl: "https://example.com/ab2.png",
BillColors: []string{"#FFFFFF"},
SocialLinks: []currency.SocialLink{{Type: currency.SocialLinkTypeWebsite, Value: "https://example2.com"}},

Seed: "abandonedseed2",
Authority: "abandonedauth2",

Mint: "abandonedmint222222222222222222222222222222222",
MintBump: 255,
Decimals: currencycreator.DefaultMintDecimals,

CurrencyConfig: "abandonedconfig2222222222222222222222222222",
CurrencyConfigBump: 255,

LiquidityPool: "abandonedpool22222222222222222222222222222222",
LiquidityPoolBump: 255,

VaultMint: "abandonedvmint2222222222222222222222222222222",
VaultMintBump: 255,

VaultCore: "abandonedvcore2222222222222222222222222222222",
VaultCoreBump: 255,

SellFeeBps: currencycreator.DefaultSellFeeBps,

Alt: "abandonedalt2222222222222222222222222222222222",

CreatedBy: "abandonedcreator2",
CreatedAt: time.Now(),
}

require.NoError(t, s.SaveMetadata(ctx, record2))

// New currency's name should no longer be available
available, err = s.IsNameAvailable(ctx, "AbandonedCurrency")
require.NoError(t, err)
assert.False(t, available)
}

func testGetAllMetadataByState(t *testing.T, s currency.Store) {
t.Run("testGetAllMetadataByState", func(t *testing.T) {
ctx := context.Background()
Expand Down
15 changes: 11 additions & 4 deletions ocp/worker/currency/launcher/config.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package launcher

import (
"time"

"github.com/code-payments/ocp-server/config"
"github.com/code-payments/ocp-server/config/env"
)
Expand All @@ -13,11 +15,15 @@ const (

BatchSizeConfigEnvName = envConfigPrefix + "WORKER_BATCH_SIZE"
defaultBatchSize = 100

InitialPurchaseTimeoutConfigEnvName = envConfigPrefix + "INITIAL_PURCHASE_TIMEOUT"
defaultInitialPurchaseTimeout = 10 * time.Minute
)

type conf struct {
subsidizer config.String
batchSize config.Uint64
subsidizer config.String
batchSize config.Uint64
initialPurchaseTimeout config.Duration
}

// ConfigProvider defines how config values are pulled
Expand All @@ -27,8 +33,9 @@ type ConfigProvider func() *conf
func WithEnvConfigs() ConfigProvider {
return func() *conf {
return &conf{
subsidizer: env.NewStringConfig(SubsidizerConfigEnvName, defaultSubsidizer),
batchSize: env.NewUint64Config(BatchSizeConfigEnvName, defaultBatchSize),
subsidizer: env.NewStringConfig(SubsidizerConfigEnvName, defaultSubsidizer),
batchSize: env.NewUint64Config(BatchSizeConfigEnvName, defaultBatchSize),
initialPurchaseTimeout: env.NewDurationConfig(InitialPurchaseTimeoutConfigEnvName, defaultInitialPurchaseTimeout),
}
}
}
1 change: 1 addition & 0 deletions ocp/worker/currency/launcher/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func (p *runtime) metricsGaugeWorker(ctx context.Context) error {
currency.MetadataStateCompletingInitialization,
currency.MetadataStateFinalValidation,
currency.MetadataStateAvailable,
currency.MetadataStateAbandoning,
} {
count, err := p.data.GetCurrencyMetadataCountByState(ctx, state)
if err != nil {
Expand Down
2 changes: 2 additions & 0 deletions ocp/worker/currency/launcher/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ func New(log *zap.Logger, data ocp_data.Provider, configProvider ConfigProvider)

func (p *runtime) Start(ctx context.Context, interval time.Duration) error {
for _, state := range []currency.MetadataState{
currency.MetadataStateWaitingForInitialPurchase,
currency.MetadataStateFundingAuthority,
currency.MetadataStateCompletingInitialization,
currency.MetadataStateFinalValidation,
currency.MetadataStateAbandoning,
} {
go func(state currency.MetadataState) {
err := p.worker(ctx, state, interval)
Expand Down
50 changes: 50 additions & 0 deletions ocp/worker/currency/launcher/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,26 @@ func (p *runtime) markCurrencyMetadataAvailable(ctx context.Context, record *cur
return p.data.SaveCurrencyMetadata(ctx, record)
}

func (p *runtime) markCurrencyMetadataAbandoning(ctx context.Context, record *currency.MetadataRecord) error {
err := p.validateCurrencyMetadataState(record, currency.MetadataStateWaitingForInitialPurchase)
if err != nil {
return err
}

record.State = currency.MetadataStateAbandoning
return p.data.SaveCurrencyMetadata(ctx, record)
}

func (p *runtime) markCurrencyMetadataAbandoned(ctx context.Context, record *currency.MetadataRecord) error {
err := p.validateCurrencyMetadataState(record, currency.MetadataStateAbandoning)
if err != nil {
return err
}

record.State = currency.MetadataStateAbandoned
return p.data.SaveCurrencyMetadata(ctx, record)
}

func (p *runtime) putInitialReserveState(ctx context.Context, record *currency.MetadataRecord) error {
// Note: The live reserve state is initialized by the swap worker on initial purchase

Expand Down Expand Up @@ -199,6 +219,36 @@ func validateMinimumAuthorityFunding(ctx context.Context, data ocp_data.Provider
}
}

func returnAuthorityFundsToSubsidizer(ctx context.Context, data ocp_data.Provider, subsidizer, authority *common.Account) error {
ai, _, err := data.GetBlockchainAccountInfo(ctx, authority.PublicKey().ToBase58(), solana.CommitmentFinalized)
if err == solana.ErrNoAccountInfo {
return nil
} else if err != nil {
return errors.Wrap(err, "error getting authority account info")
}

if ai.Lamports == 0 {
return nil
}

bh, err := data.GetBlockchainLatestBlockhash(ctx)
if err != nil {
return errors.Wrap(err, "error getting latest blockhash")
}

txn, err := transaction_util.MakeSolanaTransferTransaction(subsidizer, authority, subsidizer, ai.Lamports, bh)
if err != nil {
return errors.Wrap(err, "error making solana transfer transaction")
}

err = txn.Sign(subsidizer.PrivateKey().ToBytes(), authority.PrivateKey().ToBytes())
if err != nil {
return errors.Wrap(err, "error signing transaction")
}

return transaction_util.SubmitAndWaitForFinalization(ctx, data, &txn)
}

func fundAuthority(ctx context.Context, data ocp_data.Provider, subsidizer, account *common.Account, amount uint64) error {
bh, err := data.GetBlockchainLatestBlockhash(ctx)
if err != nil {
Expand Down
42 changes: 42 additions & 0 deletions ocp/worker/currency/launcher/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,16 @@ func (p *runtime) handle(ctx context.Context, record *currency.MetadataRecord) e

var err error
switch record.State {
case currency.MetadataStateWaitingForInitialPurchase:
err = p.handleStateWaitingForInitialPurchase(ctx, record)
case currency.MetadataStateFundingAuthority:
err = p.handleStateFundingAuthority(ctx, record)
case currency.MetadataStateCompletingInitialization:
err = p.handleStateCompletingInitialization(ctx, record)
case currency.MetadataStateFinalValidation:
err = p.handleStateFinalValidation(ctx, record)
case currency.MetadataStateAbandoning:
err = p.handleStateAbandoning(ctx, record)
}
if err != nil {
log.With(zap.Error(err)).Warn("failure processing currency to launch")
Expand All @@ -108,6 +112,20 @@ func (p *runtime) handle(ctx context.Context, record *currency.MetadataRecord) e
return nil
}

func (p *runtime) handleStateWaitingForInitialPurchase(ctx context.Context, currencyMetadataRecord *currency.MetadataRecord) error {
err := p.validateCurrencyMetadataState(currencyMetadataRecord, currency.MetadataStateWaitingForInitialPurchase)
if err != nil {
return err
}

timeout := p.conf.initialPurchaseTimeout.Get(ctx)
if time.Since(currencyMetadataRecord.CreatedAt) < timeout {
return nil
}

return p.markCurrencyMetadataAbandoning(ctx, currencyMetadataRecord)
}

// Note: Assumes unique authority per currency
func (p *runtime) handleStateFundingAuthority(ctx context.Context, currencyMetadataRecord *currency.MetadataRecord) error {
err := p.validateCurrencyMetadataState(currencyMetadataRecord, currency.MetadataStateFundingAuthority)
Expand Down Expand Up @@ -375,3 +393,27 @@ func (p *runtime) handleStateFinalValidation(ctx context.Context, currencyMetada
return p.initializeCreatorAcccount(ctx, currencyMetadataRecord, accounts)
})
}

func (p *runtime) handleStateAbandoning(ctx context.Context, currencyMetadataRecord *currency.MetadataRecord) error {
err := p.validateCurrencyMetadataState(currencyMetadataRecord, currency.MetadataStateAbandoning)
if err != nil {
return err
}

authorityVaultRecord, err := p.data.GetKey(ctx, currencyMetadataRecord.Authority)
if err != nil {
return errors.Wrap(err, "error getting authority vault record")
}

authority, err := common.NewAccountFromPrivateKeyString(authorityVaultRecord.PrivateKey)
if err != nil {
return errors.Wrap(err, "invalid authority private key")
}

err = returnAuthorityFundsToSubsidizer(ctx, p.data, p.subsidizer, authority)
if err != nil {
return errors.Wrap(err, "error returning authority funds to subsidizer")
}

return p.markCurrencyMetadataAbandoned(ctx, currencyMetadataRecord)
}
Loading
Loading