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
5 changes: 5 additions & 0 deletions api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,11 @@ func NewApiServer(config config.Config) *ApiServer {
g.Post("/rewards/claim", app.v1ClaimRewards)
g.Post("/rewards/code", app.v1CreateRewardCode)

// Prizes
g.Get("/prizes", app.v1Prizes)
g.Post("/prizes/claim", app.v1PrizesClaim)
g.Get("/wallet/:wallet/prizes", app.v1WalletPrizes)

// Resolve
g.Get("/resolve", app.v1Resolve)

Expand Down
138 changes: 82 additions & 56 deletions api/v1_create_reward_code.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,69 +129,19 @@ func (app *ApiServer) v1CreateRewardCode(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate code: "+err.Error())
}

var rewardAddress string

// Only create reward pool if deterministic secret is configured
if app.config.LaunchpadDeterministicSecret != "" {
mintPubKey, err := solana.PublicKeyFromBase58(req.Mint)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid mint address: "+err.Error())
}

claimAuthority, claimAuthorityPrivateKey, err := utils.DeriveEthAddressForMint(
[]byte("claimAuthority"),
app.config.LaunchpadDeterministicSecret,
mintPubKey,
)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to derive Ethereum key: "+err.Error())
}

// Convert the private key to the format expected by the SDK
privateKey, err := common.EthToEthKey(claimAuthorityPrivateKey)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to convert private key: "+err.Error())
}

// Create OpenAudio SDK instance and set the private key
oap := sdk.NewOpenAudioSDK(app.config.AudiusdURL)
oap.SetPrivKey(privateKey)

// Get current chain status to calculate deadline
statusResp, err := oap.Core.GetStatus(context.Background(), connect.NewRequest(&v1.GetStatusRequest{}))
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get chain status: "+err.Error())
}

currentHeight := statusResp.Msg.ChainInfo.CurrentHeight
deadline := currentHeight + 100
rewardID := fmt.Sprintf("%s", code)

reward, err := oap.Rewards.CreateReward(context.Background(), &v1.CreateReward{
RewardId: rewardID,
Name: fmt.Sprintf("Launchpad Reward %s", code),
Amount: uint64(req.Amount),
ClaimAuthorities: []*v1.ClaimAuthority{
{Address: claimAuthority, Name: "Launchpad"},
},
DeadlineBlockHeight: deadline,
})
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create reward pool: "+err.Error())
}

rewardAddress = reward.Address
} else {
rewardAddress = ""
}

var codeSignature string
if signatureIsSingleUse {
codeSignature = req.Signature
} else {
codeSignature = ""
}

// Use shared function to create reward code
rewardAddress, err := app.createRewardCode(context.Background(), code, req.Mint, req.Amount, "Launchpad")
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create reward code: "+err.Error())
}

// Insert the reward code into the database
sql := `
INSERT INTO reward_codes (code, mint, reward_address, amount, remaining_uses, signature)
Expand All @@ -217,3 +167,79 @@ func (app *ApiServer) v1CreateRewardCode(c *fiber.Ctx) error {

return c.Status(fiber.StatusCreated).JSON(response)
}

// createRewardCode creates or reuses a reward pool and returns the reward address.
// This is shared business logic used by both v1CreateRewardCode and prize claim flow.
func (app *ApiServer) createRewardCode(ctx context.Context, code, mint string, amount int64, rewardName string) (string, error) {
var rewardAddress string

// Only create reward pool if deterministic secret is configured
if app.config.LaunchpadDeterministicSecret != "" {
// Check for existing reward address for this mint (reuse pattern)
var existingRewardAddress string
err := app.pool.QueryRow(ctx, `
SELECT reward_address FROM reward_codes
WHERE mint = $1 AND reward_address IS NOT NULL AND reward_address != ''
LIMIT 1
`, mint).Scan(&existingRewardAddress)

if err == nil && existingRewardAddress != "" {
// Reuse existing reward pool
rewardAddress = existingRewardAddress
} else {
// Create new reward pool
mintPubKey, err := solana.PublicKeyFromBase58(mint)
if err != nil {
return "", fmt.Errorf("invalid mint address: %w", err)
}

claimAuthority, claimAuthorityPrivateKey, err := utils.DeriveEthAddressForMint(
[]byte("claimAuthority"),
app.config.LaunchpadDeterministicSecret,
mintPubKey,
)
if err != nil {
return "", fmt.Errorf("failed to derive Ethereum key: %w", err)
}

// Convert the private key to the format expected by the SDK
privateKey, err := common.EthToEthKey(claimAuthorityPrivateKey)
if err != nil {
return "", fmt.Errorf("failed to convert private key: %w", err)
}

// Create OpenAudio SDK instance and set the private key
oap := sdk.NewOpenAudioSDK(app.config.AudiusdURL)
oap.SetPrivKey(privateKey)

// Get current chain status to calculate deadline
statusResp, err := oap.Core.GetStatus(ctx, connect.NewRequest(&v1.GetStatusRequest{}))
if err != nil {
return "", fmt.Errorf("failed to get chain status: %w", err)
}

currentHeight := statusResp.Msg.ChainInfo.CurrentHeight
deadline := currentHeight + 100
rewardID := code

reward, err := oap.Rewards.CreateReward(ctx, &v1.CreateReward{
RewardId: rewardID,
Name: fmt.Sprintf("%s Reward %s", rewardName, code),
Amount: uint64(amount),
ClaimAuthorities: []*v1.ClaimAuthority{
{Address: claimAuthority, Name: rewardName},
},
DeadlineBlockHeight: deadline,
})
if err != nil {
return "", fmt.Errorf("failed to create reward pool: %w", err)
}

rewardAddress = reward.Address
}
} else {
rewardAddress = ""
}

return rewardAddress, nil
}
Loading