diff --git a/cmd/reclaim_rent/main.go b/cmd/reclaim_rent/main.go new file mode 100644 index 00000000..5ffff268 --- /dev/null +++ b/cmd/reclaim_rent/main.go @@ -0,0 +1,319 @@ +package main + +import ( + "context" + "fmt" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/spf13/cobra" + + "api.audius.co/solana/spl/programs/claimable_tokens" + bin "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/programs/token" + "github.com/gagliardetto/solana-go/rpc" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" +) + +var reclaimRentCmd = &cobra.Command{ + Use: "reclaim_rent ", + Short: "Reclaim rent from empty token accounts for a given mint", + Args: cobra.ExactArgs(1), + RunE: reclaimRent, +} + +func main() { + err := reclaimRentCmd.Execute() + if err != nil { + fmt.Println("Error executing command:", err) + } +} + +func init() { + reclaimRentCmd.Flags().StringP("rpc", "r", "https://api.mainnet-beta.solana.com", "The Solana RPC endpoint to use") + reclaimRentCmd.Flags().StringP("database", "c", "postgres://postgres:postgres@localhost:5432/discovery_provider_1?sslmode=disable", "Database connection string") + reclaimRentCmd.Flags().StringP("keypair", "k", "~/.config/solana/id.json", "The wallet to use as fee payer for transactions") + reclaimRentCmd.Flags().StringP("destination", "d", "", "The recipient of reclaimed rent (defaults to fee payer)") + reclaimRentCmd.Flags().StringP("program", "p", claimable_tokens.ProgramID.String(), "The claimable tokens program ID") +} + +func reclaimRent(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + rpcEndpoint, err := cmd.Flags().GetString("rpc") + if err != nil { + return fmt.Errorf("failed to get rpc flag: %w", err) + } + rpcClient := rpc.New(rpcEndpoint) + + databaseURL, err := cmd.Flags().GetString("database") + if err != nil { + return fmt.Errorf("failed to get database flag: %w", err) + } + + config, err := pgxpool.ParseConfig(databaseURL) + if err != nil { + return fmt.Errorf("failed to parse database URL: %w", err) + } + pool, err := pgxpool.NewWithConfig(ctx, config) + if err != nil { + return fmt.Errorf("failed to create database pool: %w", err) + } + defer pool.Close() + + feePayerFlag, err := cmd.Flags().GetString("keypair") + if err != nil { + return fmt.Errorf("failed to get keypair flag: %w", err) + } + keypair, err := solana.PrivateKeyFromSolanaKeygenFile(feePayerFlag) + if err != nil { + return fmt.Errorf("failed to load keypair: %w", err) + } + + destinationFlag, err := cmd.Flags().GetString("destination") + if err != nil { + return fmt.Errorf("failed to get destination flag: %w", err) + } + var destination solana.PublicKey + if destinationFlag == "" { + destination = keypair.PublicKey() + } else { + destination = solana.MustPublicKeyFromBase58(destinationFlag) + } + + programIDFlag, err := cmd.Flags().GetString("program") + if err != nil { + return fmt.Errorf("failed to get program flag: %w", err) + } + claimable_tokens.SetProgramID(solana.MustPublicKeyFromBase58(programIDFlag)) + + mint := solana.MustPublicKeyFromBase58(args[0]) + + fmt.Println("Reclaiming rent for mint:", args[0]) + + authority, _, err := claimable_tokens.DeriveAuthority(mint) + if err != nil { + return fmt.Errorf("failed to derive authority: %w", err) + } + + offset := 0 + + totalCount, err := getTokenAccountsCountFromDatabase(ctx, pool, mint) + if err != nil { + return fmt.Errorf("failed to get token accounts count from database: %w", err) + } + + limit := 1000 + + for { + accounts, err := getTokenAccountsFromDatabase(ctx, pool, mint, limit, offset) + if err != nil { + return fmt.Errorf("failed to get token accounts from database: %w", err) + } + + if len(accounts) == 0 { + fmt.Println("No more accounts to process.") + break + } + fmt.Printf("Gathered %d accounts from db\n", len(accounts)) + + offset += len(accounts) + + filtered, err := filterAccounts(ctx, rpcClient, accounts) + if err != nil { + return fmt.Errorf("failed to filter accounts: %w", err) + } + + batchSize := 15 + i := 0 + + for { + batch := make([]DatabaseAccount, 0, batchSize) + for j := i; j < i+batchSize && j < len(filtered); j++ { + batch = append(batch, filtered[j]) + } + if len(batch) == 0 { + break + } + txSig, err := processBatch(ctx, rpcClient, batch, authority, destination, keypair) + if err != nil { + return fmt.Errorf("failed to process batch: %w", err) + } + if txSig != nil { + fmt.Printf("Submitted transaction %s to reclaim rent for %d accounts\n", txSig.String(), len(batch)) + } + time.Sleep(time.Second / 500 * 2) // Max 500 req/s (2 req per batch) to avoid rate limiting + + fmt.Printf("Processed %d/%d accounts (%d/%d)\n", i+len(batch), len(filtered), offset/limit+1, (totalCount+999)/limit) + i += batchSize + } + } + return nil +} + +func filterAccounts(ctx context.Context, rpcClient *rpc.Client, batch []DatabaseAccount) ([]DatabaseAccount, error) { + accounts := make([]solana.PublicKey, 0, len(batch)) + for _, acct := range batch { + accounts = append(accounts, solana.MustPublicKeyFromBase58(acct.Account)) + } + + res, err := rpcClient.GetMultipleAccountsWithOpts(ctx, accounts, &rpc.GetMultipleAccountsOpts{ + Encoding: solana.EncodingBase64, + }) + if err != nil { + return nil, fmt.Errorf("failed to get accounts: %w", err) + } + + filtered := make([]DatabaseAccount, 0, len(batch)) + + for i, acct := range res.Value { + if acct == nil { + fmt.Printf("Skipping account %s: account does not exist\n", batch[i].Account) + continue + } + var tokenAccount token.Account + err := bin.NewBorshDecoder(acct.Data.GetBinary()).Decode(&tokenAccount) + if err != nil { + fmt.Printf("Skipping account %s: failed to decode account data (%v)\n", batch[i].Account, err) + continue + } + if tokenAccount.Amount != 0 { + fmt.Printf("Skipping account %s: account balance is not zero\n", batch[i].Account) + continue + } + filtered = append(filtered, batch[i]) + } + return filtered, nil +} + +func processBatch(ctx context.Context, rpcClient *rpc.Client, batch []DatabaseAccount, authority solana.PublicKey, destination solana.PublicKey, keypair solana.PrivateKey) (*solana.Signature, error) { + instructions := make([]solana.Instruction, 0, len(batch)) + for _, acct := range batch { + closeInstruction := claimable_tokens.NewCloseInstructionBuilder(). + SetUserBank(solana.MustPublicKeyFromBase58(acct.Account)). + SetAuthority(authority). + SetDestination(destination). + SetEthAddress(common.HexToAddress(acct.EthereumAddress)) + instructions = append(instructions, closeInstruction.Build()) + } + + if len(instructions) == 0 { + fmt.Println("No valid accounts to process in this batch.") + return nil, nil + } + + blockhashResult, err := rpcClient.GetLatestBlockhash(ctx, rpc.CommitmentFinalized) + if err != nil { + return nil, fmt.Errorf("error getting recent blockhash: %v", err) + } + recentBlockhash := blockhashResult.Value.Blockhash + + tx, err := solana.NewTransaction( + instructions, + recentBlockhash, + solana.TransactionPayer(keypair.PublicKey()), + ) + if err != nil { + return nil, fmt.Errorf("error building transaction: %v", err) + } + + tx.Sign(func(key solana.PublicKey) *solana.PrivateKey { + if key.Equals(keypair.PublicKey()) { + return &keypair + } + return nil + }) + + txSig := tx.Signatures[0] + _, err = rpcClient.SendTransactionWithOpts(ctx, tx, rpc.TransactionOpts{ + SkipPreflight: true, + }) + if err != nil { + return nil, fmt.Errorf("error sending transaction %s: %v", txSig.String(), err) + } + return &txSig, nil +} + +func getEmptyTokenAccounts(ctx context.Context, client *rpc.Client, mint solana.PublicKey, owner solana.PublicKey, pageKey *string) (rpc.GetProgramAccountsV2Result, error) { + mintOffset := uint64(0) + ownerOffset := uint64(32) + balanceOffset := uint64(64) + balance := make([]byte, 8) + + dataSliceOffset := uint64(0) + dataSliceLength := uint64(0) + limit := uint64(10000) + + return client.GetProgramAccountsV2WithOpts(ctx, solana.TokenProgramID, &rpc.GetProgramAccountsV2Opts{ + GetProgramAccountsOpts: rpc.GetProgramAccountsOpts{ + DataSlice: &rpc.DataSlice{ + Offset: &dataSliceOffset, + Length: &dataSliceLength, + }, + Filters: []rpc.RPCFilter{ + { + Memcmp: &rpc.RPCFilterMemcmp{ + Offset: mintOffset, + Bytes: mint[:], + }, + }, + { + Memcmp: &rpc.RPCFilterMemcmp{ + Offset: ownerOffset, + Bytes: owner[:], + }, + }, + { + Memcmp: &rpc.RPCFilterMemcmp{ + Offset: balanceOffset, + Bytes: balance, + }, + }, + { + DataSize: 165, // Standard SPL Token account size + }, + }, + }, + PaginationKey: pageKey, + Limit: &limit, + }) +} + +type DatabaseAccount struct { + Account string + EthereumAddress string +} + +func getTokenAccountsFromDatabase(ctx context.Context, pool *pgxpool.Pool, mint solana.PublicKey, limit, offset int) ([]DatabaseAccount, error) { + sql := ` + SELECT bank_account AS account, ethereum_address + FROM user_bank_accounts + LIMIT $1 OFFSET $2 + ` + rows, err := pool.Query(ctx, sql, limit, offset) + if err != nil { + return nil, fmt.Errorf("failed to query token accounts: %w", err) + } + + accounts, err := pgx.CollectRows(rows, pgx.RowToStructByName[DatabaseAccount]) + if err != nil { + return nil, fmt.Errorf("failed to collect token accounts: %w", err) + } + + return accounts, nil +} + +func getTokenAccountsCountFromDatabase(ctx context.Context, pool *pgxpool.Pool, mint solana.PublicKey) (int, error) { + sql := ` + SELECT COUNT(*) + FROM user_bank_accounts + ` + var count int + err := pool.QueryRow(ctx, sql).Scan(&count) + if err != nil { + return 0, fmt.Errorf("failed to query token accounts count: %w", err) + } + return count, nil +} diff --git a/config/solana_config.go b/config/solana_config.go index 054e9874..97f380f6 100644 --- a/config/solana_config.go +++ b/config/solana_config.go @@ -49,7 +49,7 @@ const ( // Stage StageSolanaRelay = "https://discoveryprovider.staging.audius.co/solana/relay" StageMintAudio = "9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM" - StageMintUSDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZy4z6cQ" + StageMintUSDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" StageRewardManagerProgramID = "CDpzvz7DfgbF95jSSCHLX3ERkugyfgn9Fw8ypNZ1hfXp" StageRewardManagerState = "GaiG9LDYHfZGqeNaoGRzFEnLiwUT7WiC6sA6FDJX9ZPq" StageRewardManagerLookupTable = "ChFCWjeFxM6SRySTfT46zXn2K7m89TJsft4HWzEtkB4J" @@ -60,7 +60,7 @@ const ( // Prod ProdSolanaRelay = "https://discoveryprovider.audius.co/solana/relay" ProdMintAudio = "9LzCMqDgTKYz9Drzqnpgee3SGa89up3a247ypMj2xrqM" - ProdMintUSDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZy4z6cQ" + ProdMintUSDC = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" ProdRewardManagerProgramID = "DDZDcYdQFEMwcu2Mwo75yGFjJ1mUQyyXLWzhZLEVFcei" ProdRewardManagerState = "71hWFVYokLaN1PNYzTAWi13EfJ7Xt9VbSWUKsXUT8mxE" ProdRewardManagerLookupTable = "4UQwpGupH66RgQrWRqmPM9Two6VJEE68VZ7GeqZ3mvVv" diff --git a/go.mod b/go.mod index ea25e267..6fe149ca 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.25.3 require ( connectrpc.com/connect v1.18.1 + github.com/AlecAivazis/survey/v2 v2.3.7 github.com/Doist/unfurlist v0.0.0-20250409100812-515f2735f8e5 github.com/OpenAudio/go-openaudio v1.0.9 github.com/aquasecurity/esquery v0.2.0 @@ -34,6 +35,7 @@ require ( github.com/rpcpool/yellowstone-grpc/examples/golang v0.0.0-20250605231917-29d62ca5d4ae github.com/segmentio/encoding v0.4.1 github.com/speps/go-hashids/v2 v2.0.1 + github.com/spf13/cobra v1.10.1 github.com/stretchr/testify v1.11.1 github.com/test-go/testify v1.1.4 github.com/tidwall/gjson v1.18.0 @@ -49,7 +51,6 @@ require ( require ( filippo.io/edwards25519 v1.0.0-rc.1 // indirect - github.com/AlecAivazis/survey/v2 v2.3.7 // indirect github.com/DataDog/zstd v1.5.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/StackExchange/wmi v1.2.1 // indirect @@ -203,7 +204,6 @@ require ( github.com/shirou/gopsutil/v4 v4.25.1 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect - github.com/spf13/cobra v1.10.1 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/streamingfast/logging v0.0.0-20230608130331-f22c91403091 // indirect github.com/stretchr/objx v0.5.2 // indirect @@ -242,4 +242,4 @@ require ( rsc.io/tmplfunc v0.0.3 // indirect ) -replace github.com/gagliardetto/solana-go => github.com/rickyrombo/solana-go v1.12.1-0.20250714063439-a36a9577196d +replace github.com/gagliardetto/solana-go => github.com/rickyrombo/solana-go v0.0.0-20251201234416-e59646f7798f diff --git a/go.sum b/go.sum index 12737ebb..1c72cc25 100644 --- a/go.sum +++ b/go.sum @@ -16,13 +16,10 @@ github.com/Doist/unfurlist v0.0.0-20250409100812-515f2735f8e5 h1:H6UB7AS+PZjJIET github.com/Doist/unfurlist v0.0.0-20250409100812-515f2735f8e5/go.mod h1:0+psOGlhok8Re5d2R6X+4HYcqQ9VvMNB866cyMoA/Z8= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= -github.com/OpenAudio/go-openaudio v1.0.4 h1:A8ZnVVmyDrCZU7FL87+ROqnuhZwpz4exxAoEsIzOIOk= -github.com/OpenAudio/go-openaudio v1.0.4/go.mod h1:xmx0/cpToJenH1N+CrFgF3aApI1dJBiwoShvlwSbpHE= -github.com/OpenAudio/go-openaudio v1.0.8 h1:Pm38voEqECu5P+fAlw0b9sGZvKYbtqgFMerUPdy+NSg= -github.com/OpenAudio/go-openaudio v1.0.8/go.mod h1:xmx0/cpToJenH1N+CrFgF3aApI1dJBiwoShvlwSbpHE= github.com/OpenAudio/go-openaudio v1.0.9 h1:lyMsJz4n/UfY/nYJPVTW3evNS4ucF+65wZzUl6zVelQ= github.com/OpenAudio/go-openaudio v1.0.9/go.mod h1:xmx0/cpToJenH1N+CrFgF3aApI1dJBiwoShvlwSbpHE= github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= @@ -102,14 +99,14 @@ github.com/containerd/continuity v0.4.2 h1:v3y/4Yz5jwnvqPKJJ+7Wf93fyWoCB3F5EclWG github.com/containerd/continuity v0.4.2/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= github.com/cosmos/gogoproto v1.7.0 h1:79USr0oyXAbxg3rspGh/m4SWNyoz/GLaAh0QlCe2fro= github.com/cosmos/gogoproto v1.7.0/go.mod h1:yWChEv5IUEYURQasfyBW5ffkMHR/90hiHgbNgrtp4j0= -github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= -github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg= github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= github.com/crate-crypto/go-kzg-4844 v1.1.0 h1:EN/u9k2TF6OWSHrCCDBBU6GLNMq88OspHHlMnHfoyU4= github.com/crate-crypto/go-kzg-4844 v1.1.0/go.mod h1:JolLjpSff1tCCJKaJx4psrlEdlXuJEC996PL3tTAFks= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -341,6 +338,7 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 h1:X4egAf/gcS1zATw6wn4Ej8vjuVGxeHdan+bRb2ebyv4= github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc= @@ -568,8 +566,8 @@ github.com/qjebbs/go-jsons v0.0.0-20221222033332-a534c5fc1c4c h1:kmzxiX+OB0knCo1 github.com/qjebbs/go-jsons v0.0.0-20221222033332-a534c5fc1c4c/go.mod h1:wNJrtinHyC3YSf6giEh4FJN8+yZV7nXBjvmfjhBIcw4= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/rickyrombo/solana-go v1.12.1-0.20250714063439-a36a9577196d h1:OfrLB8AyRslh1ui3LZe+/KuORHsjSuU2+ZOz2oJHBTM= -github.com/rickyrombo/solana-go v1.12.1-0.20250714063439-a36a9577196d/go.mod h1:l/qqqIN6qJJPtxW/G1PF4JtcE3Zg2vD2EliZrr9Gn5k= +github.com/rickyrombo/solana-go v0.0.0-20251201234416-e59646f7798f h1:Wa9G8Lq44qqMUhPtggYE4Pf8cJE7r0yZ8EuInLDBPxU= +github.com/rickyrombo/solana-go v0.0.0-20251201234416-e59646f7798f/go.mod h1:l/qqqIN6qJJPtxW/G1PF4JtcE3Zg2vD2EliZrr9Gn5k= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= diff --git a/solana/spl/programs/claimable_tokens/Close.go b/solana/spl/programs/claimable_tokens/Close.go new file mode 100644 index 00000000..84e6c7e7 --- /dev/null +++ b/solana/spl/programs/claimable_tokens/Close.go @@ -0,0 +1,117 @@ +package claimable_tokens + +import ( + "github.com/ethereum/go-ethereum/common" + bin "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/text" + "github.com/gagliardetto/solana-go/text/format" + "github.com/gagliardetto/treeout" +) + +type Close struct { + EthAddress common.Address + + solana.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +var ( + _ solana.AccountsGettable = (*Close)(nil) + _ solana.AccountsSettable = (*Close)(nil) + _ text.EncodableToTree = (*Close)(nil) +) + +func NewCloseInstructionBuilder() *Close { + inst := &Close{ + AccountMetaSlice: make(solana.AccountMetaSlice, 4), + } + inst.AccountMetaSlice[3] = solana.NewAccountMeta(solana.TokenProgramID, false, false) + return inst +} + +func (inst *Close) SetEthAddress(ethAddress common.Address) *Close { + inst.EthAddress = ethAddress + return inst +} + +func (inst *Close) UserBank() *solana.AccountMeta { + return inst.AccountMetaSlice.Get(0) +} + +func (inst *Close) SetUserBank(userBank solana.PublicKey) *Close { + inst.AccountMetaSlice[0] = solana.NewAccountMeta(userBank, true, false) + return inst +} + +func (inst *Close) Authority() *solana.AccountMeta { + return inst.AccountMetaSlice.Get(1) +} + +func (inst *Close) SetAuthority(authority solana.PublicKey) *Close { + inst.AccountMetaSlice[1] = solana.NewAccountMeta(authority, true, false) + return inst +} + +func (inst *Close) Destination() *solana.AccountMeta { + return inst.AccountMetaSlice.Get(2) +} + +func (inst *Close) SetDestination(destination solana.PublicKey) *Close { + inst.AccountMetaSlice[2] = solana.NewAccountMeta(destination, true, false) + return inst +} + +func (inst *Close) TokenProgram() *solana.AccountMeta { + return inst.AccountMetaSlice.Get(3) +} + +func (inst *Close) SetTokenProgram(tokenProgram solana.PublicKey) *Close { + inst.AccountMetaSlice[3] = solana.NewAccountMeta(tokenProgram, false, false) + return inst +} + +func (inst *Close) CloseAuthority() *solana.AccountMeta { + if len(inst.AccountMetaSlice) < 5 { + return nil + } + return inst.AccountMetaSlice.Get(4) +} + +func (inst *Close) SetCloseAuthority(closeAuthority solana.PublicKey) *Close { + if len(inst.AccountMetaSlice) < 5 { + inst.AccountMetaSlice = append(inst.AccountMetaSlice, solana.NewAccountMeta(closeAuthority, true, true)) + return inst + } + inst.AccountMetaSlice[4] = solana.NewAccountMeta(closeAuthority, true, true) + return inst +} + +func (inst *Close) Build() *Instruction { + return &Instruction{BaseVariant: bin.BaseVariant{ + Impl: inst, + TypeID: bin.TypeIDFromUint8(Instruction_Close), + }} +} + +// ----- text.EncodableToTree Implementation ----- + +func (inst *Close) EncodeToTree(parent treeout.Branches) { + parent.Child(format.Program("ClaimableTokens", ProgramID)). + ParentFunc(func(programBranch treeout.Branches) { + programBranch.Child(format.Instruction("Close")). + ParentFunc(func(instructionBranch treeout.Branches) { + instructionBranch.Child("Params").ParentFunc(func(paramsBranch treeout.Branches) { + paramsBranch.Child(format.Param("EthAddress", inst.EthAddress)) + }) + instructionBranch.Child("Accounts").ParentFunc(func(accountsBranch treeout.Branches) { + accountsBranch.Child(format.Account("UserBank", inst.UserBank().PublicKey)) + accountsBranch.Child(format.Account("Authority", inst.Authority().PublicKey)) + accountsBranch.Child(format.Account("Destination", inst.Destination().PublicKey)) + accountsBranch.Child(format.Account("TokenProgram", inst.TokenProgram().PublicKey)) + if inst.CloseAuthority() != nil { + accountsBranch.Child(format.Account("CloseAuthority", inst.CloseAuthority().PublicKey)) + } + }) + }) + }) +} diff --git a/solana/spl/programs/claimable_tokens/CreateTokenAccount.go b/solana/spl/programs/claimable_tokens/CreateTokenAccount.go index 296394cc..9aaa2182 100644 --- a/solana/spl/programs/claimable_tokens/CreateTokenAccount.go +++ b/solana/spl/programs/claimable_tokens/CreateTokenAccount.go @@ -150,7 +150,7 @@ func NewCreateTokenAccountInstruction( mint solana.PublicKey, payer solana.PublicKey, ) (*CreateTokenAccount, error) { - authority, _, err := deriveAuthority(mint) + authority, _, err := DeriveAuthority(mint) if err != nil { return nil, err } diff --git a/solana/spl/programs/claimable_tokens/Transfer.go b/solana/spl/programs/claimable_tokens/Transfer.go index af26b22f..04d4a546 100644 --- a/solana/spl/programs/claimable_tokens/Transfer.go +++ b/solana/spl/programs/claimable_tokens/Transfer.go @@ -169,7 +169,7 @@ func NewTransferInstruction( if err != nil { return nil, err } - authority, _, err := deriveAuthority(mint) + authority, _, err := DeriveAuthority(mint) if err != nil { return nil, err } diff --git a/solana/spl/programs/claimable_tokens/accounts.go b/solana/spl/programs/claimable_tokens/accounts.go index 5cd5ebf9..afe0f21b 100644 --- a/solana/spl/programs/claimable_tokens/accounts.go +++ b/solana/spl/programs/claimable_tokens/accounts.go @@ -15,14 +15,14 @@ func deriveNonce(ethAddress common.Address, authority solana.PublicKey) (solana. return solana.FindProgramAddress([][]byte{authority.Bytes()[:32], seed}, ProgramID) } -func deriveAuthority(mint solana.PublicKey) (solana.PublicKey, uint8, error) { +func DeriveAuthority(mint solana.PublicKey) (solana.PublicKey, uint8, error) { return solana.FindProgramAddress([][]byte{mint.Bytes()[:32]}, ProgramID) } func deriveUserBankAccount(mint solana.PublicKey, ethAddress common.Address) (solana.PublicKey, error) { ethAddressBytes := ethAddress.Bytes() seed := base58.Encode(ethAddressBytes) - authority, _, err := deriveAuthority(mint) + authority, _, err := DeriveAuthority(mint) if err != nil { return solana.PublicKey{}, err } diff --git a/solana/spl/programs/claimable_tokens/instruction.go b/solana/spl/programs/claimable_tokens/instruction.go index bd016456..a1101274 100644 --- a/solana/spl/programs/claimable_tokens/instruction.go +++ b/solana/spl/programs/claimable_tokens/instruction.go @@ -19,6 +19,8 @@ const ( const ( Instruction_CreateTokenAccount uint8 = iota Instruction_Transfer + Instruction_SetAuthority + Instruction_Close ) // Represents a ClaimableTokens program instruction