major refactor cli

This commit is contained in:
Julien Cassis 2020-11-26 17:05:05 -05:00
parent 5069c6cf4c
commit 8d95c0fd72
23 changed files with 746 additions and 233 deletions

4
.gitignore vendored
View File

@ -1,4 +1,6 @@
.idea
dist/
.DS_Store
.DS_Store
solana-vault.json

View File

@ -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
}

View File

@ -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))
})
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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")
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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)
}

View File

@ -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

View File

@ -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
}

View File

@ -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) {

180
transaction.go Normal file
View File

@ -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
}

85
transaction_test.go Normal file
View File

@ -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},
},
})
}

View File

@ -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)
//}