diff --git a/api/server_test.go b/api/server_test.go index a34f82d1..067eab9f 100644 --- a/api/server_test.go +++ b/api/server_test.go @@ -44,7 +44,8 @@ func TestMain(m *testing.M) { } app = NewApiServer(config.Config{ - DbUrl: "postgres://postgres:example@localhost:21300/test", + DbUrl: "postgres://postgres:example@localhost:21300/test", + DelegatePrivateKey: "0633fddb74e32b3cbc64382e405146319c11a1a52dc96598e557c5dbe2f31468", }) // seed db diff --git a/api/spl/programs/claimable_tokens/accounts.go b/api/spl/programs/claimable_tokens/accounts.go new file mode 100644 index 00000000..727e2fae --- /dev/null +++ b/api/spl/programs/claimable_tokens/accounts.go @@ -0,0 +1,33 @@ +package claimable_tokens + +import ( + "encoding/hex" + "strings" + + "github.com/gagliardetto/solana-go" + "github.com/mr-tron/base58" +) + +func deriveAuthority(mint solana.PublicKey) (solana.PublicKey, uint8, error) { + return solana.FindProgramAddress([][]byte{mint.Bytes()[:32]}, ProgramID) +} + +func DeriveUserBankAccount(mint solana.PublicKey, ethAddress string) (solana.PublicKey, error) { + ethAddressBytes, err := hex.DecodeString(strings.TrimPrefix(ethAddress, "0x")) + if err != nil { + return solana.PublicKey{}, err + } + + seed := base58.Encode(ethAddressBytes) + authority, _, err := deriveAuthority(mint) + if err != nil { + return solana.PublicKey{}, err + } + + pubkey, err := solana.CreateWithSeed(authority, seed, solana.TokenProgramID) + if err != nil { + return solana.PublicKey{}, err + } + + return pubkey, nil +} diff --git a/api/spl/programs/claimable_tokens/accounts_test.go b/api/spl/programs/claimable_tokens/accounts_test.go new file mode 100644 index 00000000..1a3b2fa4 --- /dev/null +++ b/api/spl/programs/claimable_tokens/accounts_test.go @@ -0,0 +1,19 @@ +package claimable_tokens_test + +import ( + "testing" + + "bridgerton.audius.co/api/spl/programs/claimable_tokens" + "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" +) + +func TestDeriveUserBankAccount(t *testing.T) { + mint := solana.MustPublicKeyFromBase58("9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM") + ethAddress := "0xa507da823bf0c5dc44a759d0d398b7f52097da19" + expectedUserBankAccount := solana.MustPublicKeyFromBase58("9oJLynXRLkWZkTXXExPXVbza5n8CzTZLvtJ1Y3pEJ2Pk") + + userBankAccount, err := claimable_tokens.DeriveUserBankAccount(mint, ethAddress) + require.NoError(t, err) + require.Equal(t, expectedUserBankAccount.String(), userBankAccount.String()) +} diff --git a/api/spl/programs/claimable_tokens/instruction.go b/api/spl/programs/claimable_tokens/instruction.go new file mode 100644 index 00000000..4cb2c58f --- /dev/null +++ b/api/spl/programs/claimable_tokens/instruction.go @@ -0,0 +1,9 @@ +package claimable_tokens + +import "github.com/gagliardetto/solana-go" + +var ProgramID = solana.MustPublicKeyFromBase58("Ewkv3JahEFRKkcJmpoKB7pXbnUHwjAyXiwEo4ZY2rezQ") + +func SetProgramID(pubkey solana.PublicKey) { + ProgramID = pubkey +} diff --git a/api/spl/programs/reward_manager/instruction.go b/api/spl/programs/reward_manager/instruction.go index 209aae8e..f580468c 100644 --- a/api/spl/programs/reward_manager/instruction.go +++ b/api/spl/programs/reward_manager/instruction.go @@ -16,7 +16,7 @@ const ( DisbursementSeedPrefix = "T_" ) -var ProgramID = solana.MustPublicKeyFromBase58("CDpzvz7DfgbF95jSSCHLX3ERkugyfgn9Fw8ypNZ1hfXp") +var ProgramID = solana.MustPublicKeyFromBase58("DDZDcYdQFEMwcu2Mwo75yGFjJ1mUQyyXLWzhZLEVFcei") func SetProgramID(pubkey solana.PublicKey) { ProgramID = pubkey diff --git a/config/config.go b/config/config.go index 31acdf01..e0e7c4d7 100644 --- a/config/config.go +++ b/config/config.go @@ -1,6 +1,7 @@ package config import ( + "log" "os" _ "github.com/joho/godotenv/autoload" @@ -15,6 +16,8 @@ type Config struct { PythonUpstreams []string NetworkTakeRate float64 StakingBridgeUsdcPayoutWallet string + SolanaConfig SolanaConfig + AntiAbuseOracles []string } var Cfg = Config{ @@ -25,21 +28,43 @@ var Cfg = Config{ AxiomDataset: os.Getenv("axiomDataset"), NetworkTakeRate: 10, StakingBridgeUsdcPayoutWallet: "7vGA3fcjvxa3A11MAxmyhFtYowPLLCNyvoxxgN3NN2Vf", + SolanaConfig: SolCfg, } func init() { - if os.Getenv("ENV") == "stage" { + switch env := os.Getenv("ENV"); env { + case "dev": + fallthrough + case "development": + fallthrough + case "": + Cfg.AntiAbuseOracles = []string{"http://audius-protocol-discovery-provider-1"} + case "stage": + fallthrough + case "staging": + if Cfg.DelegatePrivateKey == "" { + log.Fatalf("Missing required %s env var: delegatePrivateKey", env) + } + Cfg.AntiAbuseOracles = []string{"https://discoveryprovider.staging.audius.co"} Cfg.PythonUpstreams = []string{ "https://discoveryprovider.staging.audius.co", "https://discoveryprovider2.staging.audius.co", "https://discoveryprovider3.staging.audius.co", "https://discoveryprovider5.staging.audius.co", } - } else { + case "prod": + fallthrough + case "production": + if Cfg.DelegatePrivateKey == "" { + log.Fatalf("Missing required %s env var: delegatePrivateKey", env) + } + Cfg.AntiAbuseOracles = []string{"https://discoveryprovider.audius.co"} Cfg.PythonUpstreams = []string{ "https://discoveryprovider.audius.co", "https://discoveryprovider2.audius.co", "https://discoveryprovider3.audius.co", } + default: + log.Fatalf("Unknown environment: %s", env) } } diff --git a/config/solana_config.go b/config/solana_config.go new file mode 100644 index 00000000..a300b8da --- /dev/null +++ b/config/solana_config.go @@ -0,0 +1,108 @@ +package config + +import ( + "log" + "os" + "strings" + + "bridgerton.audius.co/api/spl/programs/claimable_tokens" + "bridgerton.audius.co/api/spl/programs/reward_manager" + "github.com/gagliardetto/solana-go" +) + +type SolanaConfig struct { + RpcProviders []string + FeePayers []solana.Wallet + SolanaRelay string + + MintAudio solana.PublicKey + + RewardManagerProgramID solana.PublicKey + RewardManagerState solana.PublicKey + RewardManagerLookupTable solana.PublicKey + + ClaimableTokensProgramID solana.PublicKey +} + +var SolCfg = SolanaConfig{ + RpcProviders: strings.Split(os.Getenv("solanaRpcProviders"), ","), +} + +const ( + // Dev + DevSolanaRelay = "http://audius-protocol-discovery-provider-1/solana/relay" + DevMintAudio = "37RCjhgV1qGV2Q54EHFScdxZ22ydRMdKMtVgod47fDP3" + DevRewardManagerProgramID = "testLsJKtyABc9UXJF8JWFKf1YH4LmqCWBC42c6akPb" + DevRewardManagerState = "DJPzVothq58SmkpRb1ATn5ddN2Rpv1j2TcGvM3XsHf1c" + DevRewardManagerLookupTable = "GNHKVSmHvoRBt1JJCxz7RSMfzDQGDGhGEjmhHyxb3K5J" + DevClaimableTokensProgramID = "testHKV1B56fbvop4w6f2cTGEub9dRQ2Euta5VmqdX9" + + // Stage + StageSolanaRelay = "https://discoveryprovider.staging.audius.co/solana/relay" + StageMintAudio = "BELGiMZQ34SDE6x2FUaML2UHDAgBLS64xvhXjX5tBBZo" + StageRewardManagerProgramID = "CDpzvz7DfgbF95jSSCHLX3ERkugyfgn9Fw8ypNZ1hfXp" + StageRewardManagerState = "GaiG9LDYHfZGqeNaoGRzFEnLiwUT7WiC6sA6FDJX9ZPq" + StageRewardManagerLookupTable = "ChFCWjeFxM6SRySTfT46zXn2K7m89TJsft4HWzEtkB4J" + StageClaimableTokensProgramID = "2sjQNmUfkV6yKKi4dPR8gWRgtyma5aiymE3aXL2RAZww" + + // Prod + ProdSolanaRelay = "https://discoveryprovider.audius.co/solana/relay" + ProdMintAudio = "9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM" + ProdRewardManagerProgramID = "DDZDcYdQFEMwcu2Mwo75yGFjJ1mUQyyXLWzhZLEVFcei" + ProdRewardManagerState = "71hWFVYokLaN1PNYzTAWi13EfJ7Xt9VbSWUKsXUT8mxE" + ProdRewardManagerLookupTable = "4UQwpGupH66RgQrWRqmPM9Two6VJEE68VZ7GeqZ3mvVv" + ProdClaimableTokensProgramID = "Ewkv3JahEFRKkcJmpoKB7pXbnUHwjAyXiwEo4ZY2rezQ" +) + +func init() { + keyString := os.Getenv("solanaFeePayerKeys") + if keyString != "" { + walletKeys := strings.Split(keyString, ",") + SolCfg.FeePayers = make([]solana.Wallet, len(walletKeys)) + for i, privkeyString := range walletKeys { + privkey := solana.MustPrivateKeyFromBase58(privkeyString) + SolCfg.FeePayers[i] = solana.Wallet{ + PrivateKey: privkey, + } + } + } else { + SolCfg.FeePayers = make([]solana.Wallet, 0) + } + + switch env := os.Getenv("ENV"); env { + case "dev": + fallthrough + case "development": + fallthrough + case "": + SolCfg.SolanaRelay = DevSolanaRelay + SolCfg.MintAudio = solana.MustPublicKeyFromBase58(DevMintAudio) + SolCfg.RewardManagerProgramID = solana.MustPublicKeyFromBase58(DevRewardManagerProgramID) + SolCfg.RewardManagerState = solana.MustPublicKeyFromBase58(DevRewardManagerState) + SolCfg.RewardManagerLookupTable = solana.MustPublicKeyFromBase58(DevRewardManagerLookupTable) + SolCfg.ClaimableTokensProgramID = solana.MustPublicKeyFromBase58(DevClaimableTokensProgramID) + case "stage": + fallthrough + case "staging": + SolCfg.SolanaRelay = StageSolanaRelay + SolCfg.MintAudio = solana.MustPublicKeyFromBase58(StageMintAudio) + SolCfg.RewardManagerProgramID = solana.MustPublicKeyFromBase58(StageRewardManagerProgramID) + SolCfg.RewardManagerState = solana.MustPublicKeyFromBase58(StageRewardManagerState) + SolCfg.RewardManagerLookupTable = solana.MustPublicKeyFromBase58(StageRewardManagerLookupTable) + SolCfg.ClaimableTokensProgramID = solana.MustPublicKeyFromBase58(StageClaimableTokensProgramID) + case "prod": + fallthrough + case "production": + SolCfg.SolanaRelay = ProdSolanaRelay + SolCfg.MintAudio = solana.MustPublicKeyFromBase58(ProdMintAudio) + SolCfg.RewardManagerProgramID = solana.MustPublicKeyFromBase58(ProdRewardManagerProgramID) + SolCfg.RewardManagerState = solana.MustPublicKeyFromBase58(ProdRewardManagerState) + SolCfg.RewardManagerLookupTable = solana.MustPublicKeyFromBase58(ProdRewardManagerLookupTable) + SolCfg.ClaimableTokensProgramID = solana.MustPublicKeyFromBase58(ProdClaimableTokensProgramID) + default: + log.Fatalf("Unknown environment: %s", env) + } + + reward_manager.SetProgramID(SolCfg.RewardManagerProgramID) + claimable_tokens.SetProgramID(SolCfg.ClaimableTokensProgramID) +}