major refactor cli
This commit is contained in:
parent
5069c6cf4c
commit
8d95c0fd72
|
@ -1,4 +1,6 @@
|
|||
.idea
|
||||
dist/
|
||||
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
|
||||
solana-vault.json
|
19
account.go
19
account.go
|
@ -31,3 +31,22 @@ func AccountFromPrivateKeyBase58(privateKey string) (*Account, error) {
|
|||
func (a *Account) PublicKey() PublicKey {
|
||||
return a.PrivateKey.PublicKey()
|
||||
}
|
||||
|
||||
type AccountMeta struct {
|
||||
PublicKey PublicKey
|
||||
IsSigner bool
|
||||
IsWritable bool
|
||||
}
|
||||
|
||||
func (a *AccountMeta) less(act *AccountMeta) bool {
|
||||
if a.IsSigner && !act.IsSigner {
|
||||
return true
|
||||
} else if !a.IsSigner && act.IsSigner {
|
||||
return false
|
||||
}
|
||||
|
||||
if a.IsWritable {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package solana
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/magiconair/properties/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
@ -18,3 +19,63 @@ func TestNewAccount(t *testing.T) {
|
|||
require.Equal(t, privateKey, a2.PrivateKey)
|
||||
require.Equal(t, public, a2.PublicKey())
|
||||
}
|
||||
|
||||
func Test_AccountMeta_less(t *testing.T) {
|
||||
pkey := MustPublicKeyFromBase58("SysvarS1otHashes111111111111111111111111111")
|
||||
tests := []struct {
|
||||
name string
|
||||
left *AccountMeta
|
||||
right *AccountMeta
|
||||
expect bool
|
||||
}{
|
||||
{
|
||||
name: "accounts are equal",
|
||||
left: &AccountMeta{PublicKey: pkey, IsSigner: false, IsWritable: false},
|
||||
right: &AccountMeta{PublicKey: pkey, IsSigner: false, IsWritable: false},
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
name: "left is a signer, right is not a signer",
|
||||
left: &AccountMeta{PublicKey: pkey, IsSigner: true, IsWritable: false},
|
||||
right: &AccountMeta{PublicKey: pkey, IsSigner: false, IsWritable: false},
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
name: "left is not a signer, right is a signer",
|
||||
left: &AccountMeta{PublicKey: pkey, IsSigner: false, IsWritable: false},
|
||||
right: &AccountMeta{PublicKey: pkey, IsSigner: true, IsWritable: false},
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
name: "left is writable, right is not writable",
|
||||
left: &AccountMeta{PublicKey: pkey, IsSigner: false, IsWritable: true},
|
||||
right: &AccountMeta{PublicKey: pkey, IsSigner: false, IsWritable: false},
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
name: "left is not writable, right is writable",
|
||||
left: &AccountMeta{PublicKey: pkey, IsSigner: false, IsWritable: false},
|
||||
right: &AccountMeta{PublicKey: pkey, IsSigner: false, IsWritable: true},
|
||||
expect: false,
|
||||
},
|
||||
{
|
||||
name: "both are signers and left is writable, right is not writable",
|
||||
left: &AccountMeta{PublicKey: pkey, IsSigner: true, IsWritable: true},
|
||||
right: &AccountMeta{PublicKey: pkey, IsSigner: true, IsWritable: false},
|
||||
expect: true,
|
||||
},
|
||||
{
|
||||
name: "both are signers andleft is not writable, right is writable",
|
||||
left: &AccountMeta{PublicKey: pkey, IsSigner: true, IsWritable: false},
|
||||
right: &AccountMeta{PublicKey: pkey, IsSigner: true, IsWritable: true},
|
||||
expect: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expect, test.left.less(test.right))
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -16,11 +16,11 @@ package cmd
|
|||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
var splCmd = &cobra.Command{
|
||||
Use: "spl",
|
||||
Short: "SPL Tokens related Instructions",
|
||||
var tokenCmd = &cobra.Command{
|
||||
Use: "token",
|
||||
Short: "Tokens related Instructions",
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(splCmd)
|
||||
RootCmd.AddCommand(tokenCmd)
|
||||
}
|
|
@ -18,11 +18,11 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var splGetCmd = &cobra.Command{
|
||||
var tokenGetCmd = &cobra.Command{
|
||||
Use: "get",
|
||||
Short: "Retrieves SPL token objects",
|
||||
Short: "Retrieves token objects",
|
||||
}
|
||||
|
||||
func init() {
|
||||
splCmd.AddCommand(splGetCmd)
|
||||
tokenCmd.AddCommand(tokenGetCmd)
|
||||
}
|
|
@ -23,21 +23,21 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var splGetMintCmd = &cobra.Command{
|
||||
var tokenGetMintCmd = &cobra.Command{
|
||||
Use: "mint {mint_addr}",
|
||||
Short: "Retrieves mint information",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
ctx := cmd.Context()
|
||||
|
||||
mintAddr, err := solana.PublicKeyFromBase58(args[0])
|
||||
mintAddress, err := solana.PublicKeyFromBase58(args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("decoding mint addr: %w", err)
|
||||
}
|
||||
|
||||
client := getClient()
|
||||
|
||||
acct, err := client.GetAccountInfo(ctx, mintAddr)
|
||||
acct, err := client.GetAccountInfo(ctx, mintAddress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't get account data: %w", err)
|
||||
}
|
||||
|
@ -54,8 +54,6 @@ var splGetMintCmd = &cobra.Command{
|
|||
|
||||
var out []string
|
||||
|
||||
//out = append(out, fmt.Sprintf("Data length | %d", len(acct.Value.Data)))
|
||||
|
||||
out = append(out, fmt.Sprintf("Supply | %d", mint.Supply))
|
||||
out = append(out, fmt.Sprintf("Decimals | %d", mint.Decimals))
|
||||
|
||||
|
@ -78,5 +76,5 @@ var splGetMintCmd = &cobra.Command{
|
|||
}
|
||||
|
||||
func init() {
|
||||
splGetCmd.AddCommand(splGetMintCmd)
|
||||
tokenGetCmd.AddCommand(tokenGetMintCmd)
|
||||
}
|
|
@ -18,11 +18,11 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var splListCmd = &cobra.Command{
|
||||
var tokenListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "Retrieves SPL token objects",
|
||||
Short: "Retrieves token objects",
|
||||
}
|
||||
|
||||
func init() {
|
||||
splCmd.AddCommand(splListCmd)
|
||||
tokenCmd.AddCommand(tokenListCmd)
|
||||
}
|
|
@ -16,35 +16,44 @@ package cmd
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/dfuse-io/solana-go/programs/token"
|
||||
"github.com/ryanuber/columnize"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var splListMintsCmd = &cobra.Command{
|
||||
var tokenListMintsCmd = &cobra.Command{
|
||||
Use: "mints",
|
||||
Short: "Lists mints",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// TODO: implement a different network argument,
|
||||
// later. Ultimately, get on chain. We have a database here!
|
||||
rpcCli := getClient()
|
||||
|
||||
mints, err := token.KnownMints("mainnet")
|
||||
mints, err := token.FetchMints(cmd.Context(), rpcCli)
|
||||
if err != nil {
|
||||
return fmt.Errorf("listing mints: %w", err)
|
||||
return fmt.Errorf("unable to retrieve mints: %w", err)
|
||||
}
|
||||
out := []string{"Symbol | Token address | Token name"}
|
||||
|
||||
out := []string{"Supply | Decimals | Token Authority | Freeze Authority"}
|
||||
for _, m := range mints {
|
||||
out = append(out, fmt.Sprintf("%s | %s | %s", m.TokenSymbol, m.MintAddress, m.TokenName))
|
||||
line := []string{fmt.Sprintf("%d", m.Supply), fmt.Sprintf("%d", m.Decimals)}
|
||||
if m.MintAuthorityOption != 0 {
|
||||
line = append(line, fmt.Sprintf("%s", m.MintAuthority))
|
||||
} else {
|
||||
line = append(line, "No mint authority")
|
||||
}
|
||||
if m.FreezeAuthorityOption != 0 {
|
||||
line = append(line, fmt.Sprintf("%s", m.FreezeAuthority))
|
||||
} else {
|
||||
line = append(line, "No freeze authority")
|
||||
}
|
||||
out = append(out, strings.Join(line, " | "))
|
||||
}
|
||||
|
||||
fmt.Println(columnize.Format(out, nil))
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
splListCmd.AddCommand(splListMintsCmd)
|
||||
tokenListCmd.AddCommand(tokenListMintsCmd)
|
||||
}
|
|
@ -15,15 +15,10 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/dfuse-io/solana-go/rpc"
|
||||
|
||||
bin "github.com/dfuse-io/binary"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/dfuse-io/solana-go"
|
||||
"github.com/dfuse-io/solana-go/programs/system"
|
||||
|
@ -36,6 +31,7 @@ var tokenRegisterCmd = &cobra.Command{
|
|||
Short: "register meta data for a token",
|
||||
Args: cobra.ExactArgs(5),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
vault := mustGetWallet()
|
||||
client := getClient()
|
||||
|
||||
tokenAddress, err := solana.PublicKeyFromBase58(args[1])
|
||||
|
@ -48,120 +44,56 @@ var tokenRegisterCmd = &cobra.Command{
|
|||
symbol, err := tokenregistry.SymbolFromString(args[4])
|
||||
errorCheck("invalid symbol", err)
|
||||
|
||||
tokenMetaDataAccount := solana.NewAccount()
|
||||
|
||||
alexKey := solana.MustPublicKeyFromBase58("9hFtYBYmBJCVguRYs9pBTWKYAFoKfjYR7zBPpEkVsmD")
|
||||
|
||||
//todo: remove
|
||||
airDrop, err := client.RequestAirdrop(context.Background(), &alexKey, 10_000_000_000, rpc.CommitmentMax)
|
||||
errorCheck("air drop", err)
|
||||
fmt.Println("air drop hash:", airDrop)
|
||||
|
||||
tokenRegistryProgramIDAddress := tokenregistry.ProgramID()
|
||||
keys := []solana.PublicKey{
|
||||
alexKey,
|
||||
tokenMetaDataAccount.PublicKey(),
|
||||
tokenRegistryProgramIDAddress,
|
||||
tokenAddress,
|
||||
system.PROGRAM_ID,
|
||||
}
|
||||
alexKeyIndex := uint8(0)
|
||||
tokenMetaDataAddressIndex := uint8(1)
|
||||
tokenRegistryProgramAccountIndex := uint8(2)
|
||||
tokenAddressIndex := uint8(3)
|
||||
systemIDIndex := uint8(4)
|
||||
|
||||
size := 145
|
||||
lamport, err := client.GetMinimumBalanceForRentExemption(context.Background(), size)
|
||||
errorCheck("get minimum balance for rent exemption ", err)
|
||||
|
||||
fmt.Println("minimum lamport for rent exemption:", lamport)
|
||||
|
||||
from := alexKeyIndex
|
||||
to := tokenMetaDataAddressIndex
|
||||
metaDataAccountCreationInstruction, err := system.NewCreateAccount(
|
||||
bin.Uint64(lamport), bin.Uint64(size), tokenRegistryProgramIDAddress, systemIDIndex, from, to,
|
||||
)
|
||||
errorCheck("new create account instruction", err)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if err := bin.NewEncoder(buf).Encode(metaDataAccountCreationInstruction); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println("create account instruction hex:", hex.EncodeToString(buf.Bytes()))
|
||||
|
||||
programIdIndex := tokenRegistryProgramAccountIndex
|
||||
mintMetaIdx := tokenMetaDataAddressIndex
|
||||
ownerIdx := alexKeyIndex
|
||||
tokenIdx := tokenAddressIndex
|
||||
|
||||
/// 0. `[writable]` The register data's account to initialize
|
||||
/// 1. `[signer]` The registry's owner
|
||||
/// 2. `[]` The mint address to link with this registration
|
||||
registerToken, err := tokenregistry.NewRegisterToken(logo, name, symbol, programIdIndex, mintMetaIdx, ownerIdx, tokenIdx)
|
||||
errorCheck("new register token instruction", err)
|
||||
|
||||
_ = registerToken
|
||||
|
||||
instructions := []solana.CompiledInstruction{
|
||||
*metaDataAccountCreationInstruction,
|
||||
*registerToken,
|
||||
pkeyStr := viper.GetString("token-regiser-cmd-registrar")
|
||||
if pkeyStr == "" {
|
||||
fmt.Errorf("unable to continue without a specified registrar")
|
||||
}
|
||||
|
||||
blockHashResult, err := client.GetRecentBlockhash(context.Background(), rpc.CommitmentMax)
|
||||
errorCheck("get block recent block hash", err)
|
||||
registrarPubKey, err := solana.PublicKeyFromBase58(pkeyStr)
|
||||
errorCheck(fmt.Sprintf("invalid registrar key %q", pkeyStr), err)
|
||||
|
||||
message := solana.Message{
|
||||
Header: solana.MessageHeader{
|
||||
NumRequiredSignatures: 2,
|
||||
NumReadonlySignedAccounts: 0,
|
||||
NumReadonlyunsignedAccounts: 2,
|
||||
},
|
||||
AccountKeys: keys,
|
||||
RecentBlockhash: blockHashResult.Value.Blockhash,
|
||||
Instructions: instructions,
|
||||
}
|
||||
|
||||
buf = new(bytes.Buffer)
|
||||
err = bin.NewEncoder(buf).Encode(message)
|
||||
errorCheck("message encoding", err)
|
||||
dataToSign := buf.Bytes()
|
||||
fmt.Println("Data to sign:", buf)
|
||||
|
||||
v := mustGetWallet()
|
||||
var signature solana.Signature
|
||||
var signed bool
|
||||
for _, privateKey := range v.KeyBag {
|
||||
if privateKey.PublicKey() == alexKey {
|
||||
signature, err = privateKey.Sign(dataToSign)
|
||||
errorCheck("signe message", err)
|
||||
signed = true
|
||||
found := false
|
||||
for _, privateKey := range vault.KeyBag {
|
||||
if privateKey.PublicKey() == registrarPubKey {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
fmt.Println("signing completed")
|
||||
|
||||
if !signed {
|
||||
fmt.Println("unable to find matching private key for signing")
|
||||
os.Exit(1)
|
||||
if !found {
|
||||
return fmt.Errorf("registrar key must be present in the vault to register a token")
|
||||
}
|
||||
|
||||
fmt.Println("signature:", signature.String())
|
||||
tokenMetaAccount := solana.NewAccount()
|
||||
|
||||
tokenMetaAccountSignature, err := tokenMetaDataAccount.PrivateKey.Sign(dataToSign)
|
||||
errorCheck("tokenMetaAccountSignature", err)
|
||||
lamport, err := client.GetMinimumBalanceForRentExemption(context.Background(), tokenregistry.TOKEN_META_SIZE)
|
||||
errorCheck("get minimum balance for rent exemption ", err)
|
||||
|
||||
trx := &solana.Transaction{
|
||||
Signatures: []solana.Signature{signature, tokenMetaAccountSignature},
|
||||
Message: message,
|
||||
}
|
||||
tokenRegistryProgramID := tokenregistry.ProgramID()
|
||||
|
||||
createAccountInstruction := system.NewCreateAccountInstruction(uint64(lamport), tokenregistry.TOKEN_META_SIZE, tokenRegistryProgramID, registrarPubKey, tokenMetaAccount.PublicKey())
|
||||
registerTokenInstruction := tokenregistry.NewRegisterTokenInstruction(logo, name, symbol, tokenMetaAccount.PublicKey(), registrarPubKey, tokenAddress)
|
||||
|
||||
trx, err := solana.TransactionWithInstructions([]solana.TransactionInstruction{createAccountInstruction, registerTokenInstruction}, nil)
|
||||
errorCheck("unable to craft transaction", err)
|
||||
|
||||
_, err = trx.Sign(func(key solana.PublicKey) *solana.PrivateKey {
|
||||
for _, k := range vault.KeyBag {
|
||||
if k.PublicKey() == key {
|
||||
return &k
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
errorCheck("unable to sign transaction", err)
|
||||
|
||||
trxHash, err := client.SendTransaction(cmd.Context(), trx)
|
||||
|
||||
trxHash, err := client.SendTransaction(context.Background(), trx)
|
||||
//trxHash, err := client.SimulateTransaction(context.Background(), trx)
|
||||
fmt.Println("sent transaction hash:", trxHash, " error:", err)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
splCmd.AddCommand(tokenRegisterCmd)
|
||||
tokenCmd.AddCommand(tokenRegisterCmd)
|
||||
tokenRegisterCmd.PersistentFlags().String("registrar", "9hFtYBYmBJCVguRYs9pBTWKYAFoKfjYR7zBPpEkVsmD", "The public key that will register the token")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright 2020 dfuse Platform Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmd
|
||||
|
||||
import "github.com/spf13/cobra"
|
||||
|
||||
var tokenRegistryCmd = &cobra.Command{
|
||||
Use: "token-registry",
|
||||
Short: "Token Registry related Instructions",
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(tokenRegistryCmd)
|
||||
}
|
|
@ -15,44 +15,43 @@
|
|||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
bin "github.com/dfuse-io/binary"
|
||||
"github.com/dfuse-io/solana-go/rpc"
|
||||
|
||||
"github.com/dfuse-io/solana-go"
|
||||
"github.com/dfuse-io/solana-go/programs/tokenregistry"
|
||||
_ "github.com/dfuse-io/solana-go/programs/tokenregistry"
|
||||
"github.com/dfuse-io/solana-go/text"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var getTokenMetaCmd = &cobra.Command{
|
||||
Use: "meta {account}",
|
||||
Short: "Retrieve token meta for a specific account",
|
||||
var tokenRegistryGetCmd = &cobra.Command{
|
||||
Use: "get {mint-address}",
|
||||
Short: "Retrieve token meta for a specific token meta account",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client := getClient()
|
||||
ctx := context.Background()
|
||||
|
||||
address := args[0]
|
||||
pubKey, err := solana.PublicKeyFromBase58(address)
|
||||
errorCheck("public key", err)
|
||||
|
||||
accountInfo, err := client.GetAccountInfo(ctx, pubKey)
|
||||
errorCheck("get account info", err)
|
||||
t, err := tokenregistry.GetTokenRegistryEntry(cmd.Context(), client, pubKey)
|
||||
if err != nil {
|
||||
if err == rpc.ErrNotFound {
|
||||
fmt.Printf("No token registry entry found for given mint %q", pubKey.String())
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unable to retrieve token registry entry for mint %q: %w", pubKey.String(), err)
|
||||
}
|
||||
|
||||
var tm *tokenregistry.TokenMeta
|
||||
err = bin.NewDecoder(accountInfo.Value.Data).Decode(&tm)
|
||||
errorCheck("decode", err)
|
||||
|
||||
err = text.NewEncoder(os.Stdout).Encode(tm, nil)
|
||||
err = text.NewEncoder(os.Stdout).Encode(t, nil)
|
||||
errorCheck("textEncoding", err)
|
||||
|
||||
//fmt.Println("raw data", hex.EncodeToString(accountInfo.Value.Data))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
splGetCmd.AddCommand(getTokenMetaCmd)
|
||||
tokenRegistryCmd.AddCommand(tokenRegistryGetCmd)
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright 2020 dfuse Platform Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/ryanuber/columnize"
|
||||
|
||||
"github.com/dfuse-io/solana-go/programs/tokenregistry"
|
||||
_ "github.com/dfuse-io/solana-go/programs/tokenregistry"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var tokenRegistryListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "Retrieve token register entries",
|
||||
Args: cobra.ExactArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client := getClient()
|
||||
|
||||
entries, err := tokenregistry.GetEntries(cmd.Context(), client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to retrieve entries: %w", err)
|
||||
}
|
||||
|
||||
out := []string{"Is Initialized | Mint Address | Registration Authority | Logo | Name | Symbol"}
|
||||
|
||||
for _, e := range entries {
|
||||
initalized := "false"
|
||||
if e.IsInitialized {
|
||||
initalized = "true"
|
||||
}
|
||||
|
||||
line := []string{
|
||||
initalized,
|
||||
e.MintAddress.String(),
|
||||
e.RegistrationAuthority.String(),
|
||||
e.Logo.String(),
|
||||
e.Name.String(),
|
||||
e.Symbol.String(),
|
||||
}
|
||||
out = append(out, strings.Join(line, " | "))
|
||||
}
|
||||
|
||||
fmt.Println(columnize.Format(out, nil))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
tokenRegistryCmd.AddCommand(tokenRegistryListCmd)
|
||||
}
|
|
@ -21,9 +21,9 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var splTransferCmd = &cobra.Command{
|
||||
var tokenTransferCmd = &cobra.Command{
|
||||
Use: "transfer {from} {to} {amount}",
|
||||
Short: "Create and sign an SPL transfer transaction",
|
||||
Short: "Create and sign a token transfer transaction",
|
||||
Args: cobra.ExactArgs(3),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
client := getClient()
|
||||
|
@ -43,5 +43,5 @@ var splTransferCmd = &cobra.Command{
|
|||
}
|
||||
|
||||
func init() {
|
||||
splCmd.AddCommand(splTransferCmd)
|
||||
tokenCmd.AddCommand(tokenTransferCmd)
|
||||
}
|
1
go.mod
1
go.mod
|
@ -11,6 +11,7 @@ require (
|
|||
github.com/gorilla/rpc v1.2.0
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible
|
||||
github.com/magiconair/properties v1.8.1
|
||||
github.com/mr-tron/base58 v1.2.0
|
||||
github.com/onsi/gomega v1.10.1 // indirect
|
||||
github.com/pkg/errors v0.9.1
|
||||
|
|
|
@ -54,10 +54,53 @@ func DecodeInstruction(accounts []*solana.AccountMeta, compiledInstruction *sola
|
|||
return inst, nil
|
||||
}
|
||||
|
||||
func NewCreateAccountInstruction(lamports uint64, space uint64, owner, from, to solana.PublicKey) *Instruction {
|
||||
return &Instruction{
|
||||
BaseVariant: bin.BaseVariant{
|
||||
TypeID: 0,
|
||||
Impl: &CreateAccount{
|
||||
Lamports: bin.Uint64(lamports),
|
||||
Space: bin.Uint64(space),
|
||||
Owner: owner,
|
||||
Accounts: &CreateAccountAccounts{
|
||||
From: &solana.AccountMeta{PublicKey: from, IsSigner: true, IsWritable: true},
|
||||
New: &solana.AccountMeta{PublicKey: to, IsSigner: true, IsWritable: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type Instruction struct {
|
||||
bin.BaseVariant
|
||||
}
|
||||
|
||||
func (i *Instruction) Accounts() (out []*solana.AccountMeta) {
|
||||
switch i.TypeID {
|
||||
case 0:
|
||||
accounts := i.Impl.(*CreateAccount).Accounts
|
||||
out = []*solana.AccountMeta{accounts.From, accounts.New}
|
||||
case 1:
|
||||
// no account here
|
||||
case 2:
|
||||
accounts := i.Impl.(*Transfer).Accounts
|
||||
out = []*solana.AccountMeta{accounts.From, accounts.To}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (i *Instruction) ProgramID() solana.PublicKey {
|
||||
return PROGRAM_ID
|
||||
}
|
||||
|
||||
func (i *Instruction) Data() ([]byte, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
if err := bin.NewEncoder(buf).Encode(i); err != nil {
|
||||
return nil, fmt.Errorf("unable to encode instruction: %w", err)
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (i *Instruction) TextEncode(encoder *text.Encoder, option *text.Option) error {
|
||||
return encoder.Encode(i.Impl, option)
|
||||
}
|
||||
|
@ -100,32 +143,6 @@ func (i *CreateAccount) SetAccounts(accounts []*solana.AccountMeta, accountIndex
|
|||
return nil
|
||||
}
|
||||
|
||||
func NewCreateAccount(lamports bin.Uint64, space bin.Uint64, owner solana.PublicKey, programIdIndex uint8, fromAccountIdx uint8, toAccountIdx uint8) (*solana.CompiledInstruction, error) {
|
||||
instruction := &Instruction{
|
||||
BaseVariant: bin.BaseVariant{
|
||||
TypeID: 0,
|
||||
Impl: &CreateAccount{
|
||||
Lamports: lamports,
|
||||
Space: space,
|
||||
Owner: owner,
|
||||
},
|
||||
},
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
if err := bin.NewEncoder(buf).Encode(instruction); err != nil {
|
||||
return nil, fmt.Errorf("new create account: encode: %w", err)
|
||||
}
|
||||
|
||||
data := buf.Bytes()
|
||||
return &solana.CompiledInstruction{
|
||||
ProgramIDIndex: programIdIndex,
|
||||
AccountCount: 2,
|
||||
Accounts: []uint8{fromAccountIdx, toAccountIdx},
|
||||
DataLength: bin.Varuint16(len(data)),
|
||||
Data: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type Assign struct {
|
||||
// prefixed with byte 0x01
|
||||
Owner solana.PublicKey
|
||||
|
|
|
@ -27,6 +27,38 @@ import (
|
|||
|
||||
//go:generate rice embed-go
|
||||
|
||||
func FetchMints(ctx context.Context, rpcCli *rpc.Client) (out []*Mint, err error) {
|
||||
resp, err := rpcCli.GetProgramAccounts(
|
||||
ctx,
|
||||
TOKEN_PROGRAM_ID,
|
||||
&rpc.GetProgramAccountsOpts{
|
||||
Filters: []rpc.RPCFilter{
|
||||
{
|
||||
DataSize: MINT_SIZE,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp == nil {
|
||||
return nil, fmt.Errorf("resp empty... program account not found")
|
||||
}
|
||||
|
||||
for _, keyedAcct := range resp {
|
||||
acct := keyedAcct.Account
|
||||
|
||||
m, err := DecodeMint(acct.Data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decode mint %q: %w", acct.Owner.String(), err)
|
||||
}
|
||||
out = append(out, m)
|
||||
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func KnownMints(network string) ([]*MintMeta, error) {
|
||||
box := rice.MustFindBox("mints-data").MustBytes(network + "-tokens.json")
|
||||
if box == nil {
|
||||
|
|
|
@ -37,6 +37,8 @@ type Multisig struct {
|
|||
Signers [11]solana.PublicKey
|
||||
}
|
||||
|
||||
const MINT_SIZE = 82
|
||||
|
||||
type Mint struct {
|
||||
MintAuthorityOption uint32
|
||||
MintAuthority solana.PublicKey
|
||||
|
@ -50,7 +52,7 @@ type Mint struct {
|
|||
func DecodeMint(in []byte) (*Mint, error) {
|
||||
var m *Mint
|
||||
decoder := bin.NewDecoder(in)
|
||||
err := decoder.Decode(m)
|
||||
err := decoder.Decode(&m)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unpack: %w", err)
|
||||
}
|
||||
|
|
|
@ -39,10 +39,49 @@ func DecodeInstruction(accounts []*solana.AccountMeta, compiledInstruction *sola
|
|||
return &inst, nil
|
||||
}
|
||||
|
||||
func NewRegisterTokenInstruction(logo Logo, name Name, symbol Symbol, tokenMetaKey, ownerKey, tokenKey solana.PublicKey) *Instruction {
|
||||
return &Instruction{
|
||||
BaseVariant: bin.BaseVariant{
|
||||
TypeID: 0,
|
||||
Impl: &RegisterToken{
|
||||
Logo: logo,
|
||||
Name: name,
|
||||
Symbol: symbol,
|
||||
Accounts: &RegisterTokenAccounts{
|
||||
TokenMeta: &solana.AccountMeta{tokenMetaKey, false, true},
|
||||
Owner: &solana.AccountMeta{ownerKey, true, false},
|
||||
Token: &solana.AccountMeta{tokenKey, false, false},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type Instruction struct {
|
||||
bin.BaseVariant
|
||||
}
|
||||
|
||||
func (i *Instruction) Accounts() (out []*solana.AccountMeta) {
|
||||
switch i.TypeID {
|
||||
case 0:
|
||||
accounts := i.Impl.(*RegisterToken).Accounts
|
||||
out = []*solana.AccountMeta{accounts.TokenMeta, accounts.Owner, accounts.Token}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (i *Instruction) ProgramID() solana.PublicKey {
|
||||
return ProgramID()
|
||||
}
|
||||
|
||||
func (i *Instruction) Data() ([]byte, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
if err := bin.NewEncoder(buf).Encode(i); err != nil {
|
||||
return nil, fmt.Errorf("unable to encode instruction: %w", err)
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
var InstructionDefVariant = bin.NewVariantDefinition(bin.Uint32TypeIDEncoding, []bin.VariantType{
|
||||
{"register_token", (*RegisterToken)(nil)},
|
||||
})
|
||||
|
@ -64,9 +103,9 @@ func (i *Instruction) MarshalBinary(encoder *bin.Encoder) error {
|
|||
}
|
||||
|
||||
type RegisterTokenAccounts struct {
|
||||
MintMeta *solana.AccountMeta `text:"linear,notype"`
|
||||
Owner *solana.AccountMeta `text:"linear,notype"`
|
||||
Token *solana.AccountMeta `text:"linear,notype"`
|
||||
TokenMeta *solana.AccountMeta `text:"linear,notype"`
|
||||
Owner *solana.AccountMeta `text:"linear,notype"`
|
||||
Token *solana.AccountMeta `text:"linear,notype"`
|
||||
}
|
||||
|
||||
type RegisterToken struct {
|
||||
|
@ -76,46 +115,14 @@ type RegisterToken struct {
|
|||
Accounts *RegisterTokenAccounts `bin:"-"`
|
||||
}
|
||||
|
||||
func NewRegisterToken(logo Logo, name Name, symbol Symbol, programIdIndex uint8, mintMetaIdx uint8, ownerIdx uint8, tokenIdx uint8) (*solana.CompiledInstruction, error) {
|
||||
instruction := &Instruction{
|
||||
BaseVariant: bin.BaseVariant{
|
||||
TypeID: 0,
|
||||
Impl: &RegisterToken{
|
||||
Logo: logo,
|
||||
Name: name,
|
||||
Symbol: symbol,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if err := bin.NewEncoder(buf).Encode(instruction); err != nil {
|
||||
return nil, fmt.Errorf("new register token: encode: %w", err)
|
||||
}
|
||||
data := buf.Bytes()
|
||||
|
||||
/// 0. `[writable]` The register data's account to initialize
|
||||
/// 1. `[signer]` The registry's owner
|
||||
/// 2. `[]` The mint address to link with this registration
|
||||
|
||||
return &solana.CompiledInstruction{
|
||||
ProgramIDIndex: programIdIndex,
|
||||
AccountCount: 3,
|
||||
Accounts: []uint8{mintMetaIdx, ownerIdx, tokenIdx},
|
||||
DataLength: bin.Varuint16(len(data)),
|
||||
Data: data,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func (i *RegisterToken) SetAccounts(accounts []*solana.AccountMeta, instructionActIdx []uint8) error {
|
||||
if len(instructionActIdx) < 9 {
|
||||
return fmt.Errorf("insuficient account")
|
||||
}
|
||||
i.Accounts = &RegisterTokenAccounts{
|
||||
MintMeta: accounts[instructionActIdx[0]],
|
||||
Owner: accounts[instructionActIdx[1]],
|
||||
Token: accounts[instructionActIdx[2]],
|
||||
TokenMeta: accounts[instructionActIdx[0]],
|
||||
Owner: accounts[instructionActIdx[1]],
|
||||
Token: accounts[instructionActIdx[2]],
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
package tokenregistry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/dfuse-io/solana-go"
|
||||
"github.com/dfuse-io/solana-go/rpc"
|
||||
)
|
||||
|
||||
func GetTokenRegistryEntry(ctx context.Context, rpcCli *rpc.Client, mintAddress solana.PublicKey) (*TokenMeta, error) {
|
||||
resp, err := rpcCli.GetProgramAccounts(
|
||||
ctx,
|
||||
ProgramID(),
|
||||
&rpc.GetProgramAccountsOpts{
|
||||
Filters: []rpc.RPCFilter{
|
||||
{
|
||||
Memcmp: &rpc.RPCFilterMemcmp{
|
||||
Offset: 5,
|
||||
Bytes: mintAddress[:], // hackey to convert [32]byte to []byte
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp == nil {
|
||||
return nil, fmt.Errorf("resp empty... cannot find account")
|
||||
}
|
||||
|
||||
for _, keyedAcct := range resp {
|
||||
acct := keyedAcct.Account
|
||||
t, err := DecodeTokenMeta(acct.Data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decode token meta %q: %w", acct.Owner.String(), err)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
return nil, rpc.ErrNotFound
|
||||
}
|
||||
|
||||
func GetEntries(ctx context.Context, rpcCli *rpc.Client) (out []*TokenMeta, err error) {
|
||||
resp, err := rpcCli.GetProgramAccounts(
|
||||
ctx,
|
||||
ProgramID(),
|
||||
&rpc.GetProgramAccountsOpts{
|
||||
Filters: []rpc.RPCFilter{
|
||||
{
|
||||
DataSize: TOKEN_META_SIZE,
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp == nil {
|
||||
return nil, fmt.Errorf("resp empty... cannot find accounts")
|
||||
}
|
||||
|
||||
for _, keyedAcct := range resp {
|
||||
acct := keyedAcct.Account
|
||||
t, err := DecodeTokenMeta(acct.Data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decode token meta %q: %w", acct.Owner.String(), err)
|
||||
}
|
||||
out = append(out, t)
|
||||
}
|
||||
return out, nil
|
||||
}
|
|
@ -16,9 +16,13 @@ package tokenregistry
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
bin "github.com/dfuse-io/binary"
|
||||
|
||||
"github.com/dfuse-io/solana-go"
|
||||
)
|
||||
|
||||
const TOKEN_META_SIZE = 145
|
||||
|
||||
type TokenMeta struct {
|
||||
IsInitialized bool
|
||||
Reg [3]byte `text:"-"`
|
||||
|
@ -30,6 +34,16 @@ type TokenMeta struct {
|
|||
Symbol Symbol
|
||||
}
|
||||
|
||||
func DecodeTokenMeta(in []byte) (*TokenMeta, error) {
|
||||
var t *TokenMeta
|
||||
decoder := bin.NewDecoder(in)
|
||||
err := decoder.Decode(&t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unpack: %w", err)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
type Logo [32]byte
|
||||
|
||||
func LogoFromString(logo string) (Logo, error) {
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
package solana
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
bin "github.com/dfuse-io/binary"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type TransactionInstruction interface {
|
||||
Accounts() []*AccountMeta // returns the list of accounts the instructions requires
|
||||
ProgramID() PublicKey // the programID the instruction acts on
|
||||
Data() ([]byte, error) // the binary encoded instructions
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
payer *PublicKey
|
||||
}
|
||||
|
||||
func TransactionWithInstructions(instructions []TransactionInstruction, opt *Options) (*Transaction, error) {
|
||||
if len(instructions) == 0 {
|
||||
return nil, fmt.Errorf("requires at-least one instruction to create a transaction")
|
||||
}
|
||||
|
||||
if opt == nil {
|
||||
opt = &Options{}
|
||||
}
|
||||
|
||||
feePayer := opt.payer
|
||||
if feePayer == nil {
|
||||
for _, act := range instructions[0].Accounts() {
|
||||
if act.IsSigner {
|
||||
feePayer = &act.PublicKey
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if feePayer == nil {
|
||||
return nil, fmt.Errorf("cannot determine fee payer. You can ether pass the fee payer vai the 'TransactionWithInstructions' option parameter or it fallback to the first instruction's first signer")
|
||||
}
|
||||
|
||||
programIDs := map[string]bool{}
|
||||
accounts := []*AccountMeta{}
|
||||
for _, instruction := range instructions {
|
||||
for _, key := range instruction.Accounts() {
|
||||
accounts = append(accounts, key)
|
||||
}
|
||||
programIDs[instruction.ProgramID().String()] = true
|
||||
}
|
||||
|
||||
// Add programID to the account list
|
||||
for programId, _ := range programIDs {
|
||||
accounts = append(accounts, &AccountMeta{
|
||||
PublicKey: MustPublicKeyFromBase58(programId),
|
||||
IsSigner: false,
|
||||
IsWritable: false,
|
||||
})
|
||||
}
|
||||
|
||||
// Sort. Prioritizing first by signer, then by writable
|
||||
sort.Slice(accounts, func(i, j int) bool {
|
||||
return accounts[i].less(accounts[j])
|
||||
})
|
||||
|
||||
uniqAccountsMap := map[string]uint64{}
|
||||
uniqAccounts := []*AccountMeta{}
|
||||
for _, acc := range accounts {
|
||||
if index, found := uniqAccountsMap[acc.PublicKey.String()]; found {
|
||||
uniqAccounts[index].IsWritable = uniqAccounts[index].IsWritable || acc.IsWritable
|
||||
continue
|
||||
}
|
||||
uniqAccounts = append(uniqAccounts, acc)
|
||||
uniqAccountsMap[acc.PublicKey.String()] = uint64(len(uniqAccounts) - 1)
|
||||
}
|
||||
|
||||
zlog.Debug("unique account sorted", zap.Int("account_count", len(uniqAccounts)))
|
||||
// Move fee payer to the front
|
||||
feePayerIndex := -1
|
||||
for idx, acc := range uniqAccounts {
|
||||
if acc.PublicKey.Equals(*feePayer) {
|
||||
feePayerIndex = idx
|
||||
}
|
||||
}
|
||||
zlog.Debug("current fee payer index", zap.Int("fee_payer_index", feePayerIndex))
|
||||
|
||||
accountCount := len(uniqAccounts)
|
||||
if feePayerIndex < 0 {
|
||||
// fee payer is not part of accounts we want to add it
|
||||
accountCount++
|
||||
}
|
||||
finalAccounts := make([]*AccountMeta, accountCount)
|
||||
|
||||
itr := 1
|
||||
for idx, uniqAccount := range uniqAccounts {
|
||||
if idx == feePayerIndex {
|
||||
uniqAccount.IsSigner = true
|
||||
uniqAccount.IsWritable = true
|
||||
finalAccounts[0] = uniqAccount
|
||||
continue
|
||||
}
|
||||
finalAccounts[itr] = uniqAccount
|
||||
itr++
|
||||
}
|
||||
|
||||
message := Message{
|
||||
AccountKeys: nil,
|
||||
RecentBlockhash: PublicKey{},
|
||||
Instructions: nil,
|
||||
}
|
||||
accountKeyIndex := map[string]uint8{}
|
||||
for idx, acc := range finalAccounts {
|
||||
message.AccountKeys = append(message.AccountKeys, acc.PublicKey)
|
||||
accountKeyIndex[acc.PublicKey.String()] = uint8(idx)
|
||||
if acc.IsSigner {
|
||||
message.Header.NumRequiredSignatures++
|
||||
if !acc.IsWritable {
|
||||
message.Header.NumReadonlySignedAccounts++
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if !acc.IsWritable {
|
||||
message.Header.NumReadonlyUnsignedAccounts++
|
||||
}
|
||||
}
|
||||
|
||||
for trxIdx, instruction := range instructions {
|
||||
accounts = instruction.Accounts()
|
||||
accountIndex := make([]uint8, len(accounts))
|
||||
for idx, acc := range accounts {
|
||||
accountIndex[idx] = accountKeyIndex[acc.PublicKey.String()]
|
||||
}
|
||||
data, err := instruction.Data()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to encode instructions [%d]: %w", trxIdx, err)
|
||||
}
|
||||
message.Instructions = append(message.Instructions, CompiledInstruction{
|
||||
ProgramIDIndex: accountKeyIndex[instruction.ProgramID().String()],
|
||||
AccountCount: bin.Varuint16(uint16(len(accountIndex))),
|
||||
Accounts: accountIndex,
|
||||
DataLength: bin.Varuint16(uint16(len(data))),
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
return &Transaction{
|
||||
Message: message,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
type privateKeyGetter func(key PublicKey) *PrivateKey
|
||||
|
||||
func (t *Transaction) Sign(getter privateKeyGetter) (out []Signature, err error) {
|
||||
buf := new(bytes.Buffer)
|
||||
if err = bin.NewEncoder(buf).Encode(t.Message); err != nil {
|
||||
return nil, fmt.Errorf("unable to encode message for signing: %w", err)
|
||||
}
|
||||
messageCnt := buf.Bytes()
|
||||
|
||||
signerKeys := t.Message.signerKeys()
|
||||
|
||||
for _, key := range signerKeys {
|
||||
privateKey := getter(key)
|
||||
if privateKey == nil {
|
||||
return nil, fmt.Errorf("signer key %q not found. Ensure all the signer keys are in the vault", key.String())
|
||||
}
|
||||
|
||||
s, err := privateKey.Sign(messageCnt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to signed with key %q: %w", key.String(), err)
|
||||
}
|
||||
|
||||
t.Signatures = append(t.Signatures, s)
|
||||
}
|
||||
return t.Signatures, nil
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package solana
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/magiconair/properties/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type testTransactionInstructions struct {
|
||||
accounts []*AccountMeta
|
||||
data []byte
|
||||
programID PublicKey
|
||||
}
|
||||
|
||||
func (t *testTransactionInstructions) Accounts() []*AccountMeta {
|
||||
return t.accounts
|
||||
}
|
||||
|
||||
func (t *testTransactionInstructions) ProgramID() PublicKey {
|
||||
return t.programID
|
||||
}
|
||||
|
||||
func (t *testTransactionInstructions) Data() ([]byte, error) {
|
||||
return t.data, nil
|
||||
}
|
||||
|
||||
func TestTransactionWithInstructions(t *testing.T) {
|
||||
instructions := []TransactionInstruction{
|
||||
&testTransactionInstructions{
|
||||
accounts: []*AccountMeta{
|
||||
{PublicKey: MustPublicKeyFromBase58("A9QnpgfhCkmiBSjgBuWk76Wo3HxzxvDopUq9x6UUMmjn"), IsSigner: true, IsWritable: false},
|
||||
{PublicKey: MustPublicKeyFromBase58("9hFtYBYmBJCVguRYs9pBTWKYAFoKfjYR7zBPpEkVsmD"), IsSigner: true, IsWritable: true},
|
||||
},
|
||||
data: []byte{0xaa, 0xbb},
|
||||
programID: MustPublicKeyFromBase58("11111111111111111111111111111111"),
|
||||
},
|
||||
&testTransactionInstructions{
|
||||
accounts: []*AccountMeta{
|
||||
{PublicKey: MustPublicKeyFromBase58("SysvarC1ock11111111111111111111111111111111"), IsSigner: false, IsWritable: false},
|
||||
{PublicKey: MustPublicKeyFromBase58("SysvarS1otHashes111111111111111111111111111"), IsSigner: false, IsWritable: true},
|
||||
{PublicKey: MustPublicKeyFromBase58("9hFtYBYmBJCVguRYs9pBTWKYAFoKfjYR7zBPpEkVsmD"), IsSigner: false, IsWritable: true},
|
||||
{PublicKey: MustPublicKeyFromBase58("6FzXPEhCJoBx7Zw3SN9qhekHemd6E2b8kVguitmVAngW"), IsSigner: true, IsWritable: false},
|
||||
},
|
||||
data: []byte{0xcc, 0xdd},
|
||||
programID: MustPublicKeyFromBase58("Vote111111111111111111111111111111111111111"),
|
||||
},
|
||||
}
|
||||
|
||||
trx, err := TransactionWithInstructions(instructions, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, trx.Message.Header, MessageHeader{
|
||||
NumRequiredSignatures: 3,
|
||||
NumReadonlySignedAccounts: 1,
|
||||
NumReadonlyUnsignedAccounts: 3,
|
||||
})
|
||||
|
||||
assert.Equal(t, trx.Message.AccountKeys, []PublicKey{
|
||||
MustPublicKeyFromBase58("A9QnpgfhCkmiBSjgBuWk76Wo3HxzxvDopUq9x6UUMmjn"),
|
||||
MustPublicKeyFromBase58("9hFtYBYmBJCVguRYs9pBTWKYAFoKfjYR7zBPpEkVsmD"),
|
||||
MustPublicKeyFromBase58("6FzXPEhCJoBx7Zw3SN9qhekHemd6E2b8kVguitmVAngW"),
|
||||
MustPublicKeyFromBase58("SysvarS1otHashes111111111111111111111111111"),
|
||||
MustPublicKeyFromBase58("SysvarC1ock11111111111111111111111111111111"),
|
||||
MustPublicKeyFromBase58("11111111111111111111111111111111"),
|
||||
MustPublicKeyFromBase58("Vote111111111111111111111111111111111111111"),
|
||||
})
|
||||
|
||||
assert.Equal(t, trx.Message.Instructions, []CompiledInstruction{
|
||||
{
|
||||
ProgramIDIndex: 5,
|
||||
AccountCount: 2,
|
||||
Accounts: []uint8{0, 01},
|
||||
DataLength: 2,
|
||||
Data: []byte{0xaa, 0xbb},
|
||||
},
|
||||
{
|
||||
ProgramIDIndex: 6,
|
||||
AccountCount: 4,
|
||||
Accounts: []uint8{4, 3, 1, 2},
|
||||
DataLength: 2,
|
||||
Data: []byte{0xcc, 0xdd},
|
||||
},
|
||||
})
|
||||
}
|
21
types.go
21
types.go
|
@ -57,7 +57,7 @@ func (t *Transaction) IsWritable(account PublicKey) bool {
|
|||
}
|
||||
h := t.Message.Header
|
||||
return (index < int(h.NumRequiredSignatures-h.NumReadonlySignedAccounts)) ||
|
||||
((index >= int(h.NumRequiredSignatures)) && (index < len(t.Message.AccountKeys)-int(h.NumReadonlyunsignedAccounts)))
|
||||
((index >= int(h.NumRequiredSignatures)) && (index < len(t.Message.AccountKeys)-int(h.NumReadonlyUnsignedAccounts)))
|
||||
}
|
||||
|
||||
func (t *Transaction) ResolveProgramIDIndex(programIDIndex uint8) (PublicKey, error) {
|
||||
|
@ -85,10 +85,14 @@ type Message struct {
|
|||
Instructions []CompiledInstruction `json:"instructions"`
|
||||
}
|
||||
|
||||
func (m *Message) signerKeys() []PublicKey {
|
||||
return m.AccountKeys[0:m.Header.NumRequiredSignatures]
|
||||
}
|
||||
|
||||
type MessageHeader struct {
|
||||
NumRequiredSignatures uint8 `json:"numRequiredSignatures"`
|
||||
NumReadonlySignedAccounts uint8 `json:"numReadonlySignedAccounts"`
|
||||
NumReadonlyunsignedAccounts uint8 `json:"numReadonlyUnsignedAccounts"`
|
||||
NumReadonlyUnsignedAccounts uint8 `json:"numReadonlyUnsignedAccounts"`
|
||||
}
|
||||
|
||||
type CompiledInstruction struct {
|
||||
|
@ -108,16 +112,3 @@ func TransactionFromData(in []byte) (*Transaction, error) {
|
|||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type AccountMeta struct {
|
||||
PublicKey PublicKey
|
||||
IsSigner bool
|
||||
IsWritable bool
|
||||
}
|
||||
|
||||
//func (a *AccountMeta) String() string {
|
||||
// if a == nil {
|
||||
// return ""
|
||||
// }
|
||||
// return fmt.Sprintf("%s Signer: %t Writable: %t", a.PublicKey.String(), a.IsSigner, a.IsWritable)
|
||||
//}
|
||||
|
|
Loading…
Reference in New Issue