Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CreateIdempotent instruction #158

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -644,22 +644,25 @@ func FindProgramAddress(seed [][]byte, programID PublicKey) (PublicKey, uint8, e
func FindAssociatedTokenAddress(
wallet PublicKey,
mint PublicKey,
tokenProgramID PublicKey,
) (PublicKey, uint8, error) {
return findAssociatedTokenAddressAndBumpSeed(
wallet,
mint,
SPLAssociatedTokenAccountProgramID,
tokenProgramID,
)
}

func findAssociatedTokenAddressAndBumpSeed(
walletAddress PublicKey,
splTokenMintAddress PublicKey,
programID PublicKey,
tokenProgramID PublicKey,
) (PublicKey, uint8, error) {
return FindProgramAddress([][]byte{
walletAddress[:],
TokenProgramID[:],
tokenProgramID[:],
splTokenMintAddress[:],
},
programID,
Expand Down
3 changes: 3 additions & 0 deletions program_ids.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ var (
// A Token program on the Solana blockchain.
// This program defines a common implementation for Fungible and Non Fungible tokens.
TokenProgramID = MustPublicKeyFromBase58("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA")
// Superset of Token Program
// https://spl.solana.com/token-2022
Token2022ProgramID = MustPublicKeyFromBase58("TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb")

// A Uniswap-like exchange for the Token program on the Solana blockchain,
// implementing multiple automated market maker (AMM) curves.
Expand Down
6 changes: 4 additions & 2 deletions programs/associated-token-account/Create.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ func (inst Create) Build() *Instruction {
associatedTokenAddress, _, _ := solana.FindAssociatedTokenAddress(
inst.Wallet,
inst.Mint,
TokenProgramID,
)

keys := []*solana.AccountMeta{
Expand Down Expand Up @@ -108,7 +109,7 @@ func (inst Create) Build() *Instruction {
IsWritable: false,
},
{
PublicKey: solana.TokenProgramID,
PublicKey: TokenProgramID,
IsSigner: false,
IsWritable: false,
},
Expand All @@ -123,7 +124,7 @@ func (inst Create) Build() *Instruction {

return &Instruction{BaseVariant: bin.BaseVariant{
Impl: inst,
TypeID: bin.NoTypeIDDefaultID,
TypeID: bin.TypeIDFromUint8(Instruction_Create),
}}
}

Expand All @@ -150,6 +151,7 @@ func (inst *Create) Validate() error {
_, _, err := solana.FindAssociatedTokenAddress(
inst.Wallet,
inst.Mint,
TokenProgramID,
)
if err != nil {
return fmt.Errorf("error while FindAssociatedTokenAddress: %w", err)
Expand Down
204 changes: 204 additions & 0 deletions programs/associated-token-account/CreateIdempotent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
// Copyright 2021 github.com/gagliardetto
//
// 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 associatedtokenaccount

import (
"errors"
"fmt"

bin "github.com/gagliardetto/binary"
solana "github.com/gagliardetto/solana-go"
format "github.com/gagliardetto/solana-go/text/format"
treeout "github.com/gagliardetto/treeout"
)

type CreateIdempotent struct {
Payer solana.PublicKey `bin:"-" borsh_skip:"true"`
Wallet solana.PublicKey `bin:"-" borsh_skip:"true"`
Mint solana.PublicKey `bin:"-" borsh_skip:"true"`

// [0] = [WRITE, SIGNER] Payer
// ··········· Funding account
//
// [1] = [WRITE] AssociatedTokenAccount
// ··········· Associated token account address to be created
//
// [2] = [] Wallet
// ··········· Wallet address for the new associated token account
//
// [3] = [] TokenMint
// ··········· The token mint for the new associated token account
//
// [4] = [] SystemProgram
// ··········· System program ID
//
// [5] = [] TokenProgram
// ··········· SPL token program ID
//
// [6] = [] SysVarRent
// ··········· SysVarRentPubkey
solana.AccountMetaSlice `bin:"-" borsh_skip:"true"`
}

// NewCreateIdempotentInstructionBuilder creates a new `CreateIdempotent` instruction builder.
func NewCreateIdempotentInstructionBuilder() *CreateIdempotent {
nd := &CreateIdempotent{}
return nd
}

func (inst *CreateIdempotent) SetPayer(payer solana.PublicKey) *CreateIdempotent {
inst.Payer = payer
return inst
}

func (inst *CreateIdempotent) SetWallet(wallet solana.PublicKey) *CreateIdempotent {
inst.Wallet = wallet
return inst
}

func (inst *CreateIdempotent) SetMint(mint solana.PublicKey) *CreateIdempotent {
inst.Mint = mint
return inst
}

func (inst CreateIdempotent) Build() *Instruction {

// Find the associatedTokenAddress;
associatedTokenAddress, _, _ := solana.FindAssociatedTokenAddress(
inst.Wallet,
inst.Mint,
TokenProgramID,
)

keys := []*solana.AccountMeta{
{
PublicKey: inst.Payer,
IsSigner: true,
IsWritable: true,
},
{
PublicKey: associatedTokenAddress,
IsSigner: false,
IsWritable: true,
},
{
PublicKey: inst.Wallet,
IsSigner: false,
IsWritable: false,
},
{
PublicKey: inst.Mint,
IsSigner: false,
IsWritable: false,
},
{
PublicKey: solana.SystemProgramID,
IsSigner: false,
IsWritable: false,
},
{
PublicKey: TokenProgramID,
IsSigner: false,
IsWritable: false,
},
{
PublicKey: solana.SysVarRentPubkey,
IsSigner: false,
IsWritable: false,
},
}

inst.AccountMetaSlice = keys

return &Instruction{BaseVariant: bin.BaseVariant{
Impl: inst,
TypeID: bin.TypeIDFromUint8(Instruction_CreateIdempotent),
}}
}

// ValidateAndBuild validates the instruction accounts.
// If there is a validation error, return the error.
// Otherwise, build and return the instruction.
func (inst CreateIdempotent) ValidateAndBuild() (*Instruction, error) {
if err := inst.Validate(); err != nil {
return nil, err
}
return inst.Build(), nil
}

func (inst *CreateIdempotent) Validate() error {
if inst.Payer.IsZero() {
return errors.New("Payer not set")
}
if inst.Wallet.IsZero() {
return errors.New("Wallet not set")
}
if inst.Mint.IsZero() {
return errors.New("Mint not set")
}
_, _, err := solana.FindAssociatedTokenAddress(
inst.Wallet,
inst.Mint,
TokenProgramID,
)
if err != nil {
return fmt.Errorf("error while FindAssociatedTokenAddress: %w", err)
}
return nil
}

func (inst *CreateIdempotent) EncodeToTree(parent treeout.Branches) {
parent.Child(format.Program(ProgramName, ProgramID)).
//
ParentFunc(func(programBranch treeout.Branches) {
programBranch.Child(format.Instruction("CreateIdempotent")).
//
ParentFunc(func(instructionBranch treeout.Branches) {

// Parameters of the instruction:
instructionBranch.Child("Params[len=0]").ParentFunc(func(paramsBranch treeout.Branches) {})

// Accounts of the instruction:
instructionBranch.Child("Accounts[len=7").ParentFunc(func(accountsBranch treeout.Branches) {
accountsBranch.Child(format.Meta(" payer", inst.AccountMetaSlice.Get(0)))
accountsBranch.Child(format.Meta("associatedTokenAddress", inst.AccountMetaSlice.Get(1)))
accountsBranch.Child(format.Meta(" wallet", inst.AccountMetaSlice.Get(2)))
accountsBranch.Child(format.Meta(" tokenMint", inst.AccountMetaSlice.Get(3)))
accountsBranch.Child(format.Meta(" systemProgram", inst.AccountMetaSlice.Get(4)))
accountsBranch.Child(format.Meta(" tokenProgram", inst.AccountMetaSlice.Get(5)))
accountsBranch.Child(format.Meta(" sysVarRent", inst.AccountMetaSlice.Get(6)))
})
})
})
}

func (inst CreateIdempotent) MarshalWithEncoder(encoder *bin.Encoder) error {
return encoder.WriteBytes([]byte{}, false)
}

func (inst *CreateIdempotent) UnmarshalWithDecoder(decoder *bin.Decoder) error {
return nil
}

func NewCreateIdempotentInstruction(
payer solana.PublicKey,
walletAddress solana.PublicKey,
splTokenMintAddress solana.PublicKey,
) *CreateIdempotent {
return NewCreateIdempotentInstructionBuilder().
SetPayer(payer).
SetWallet(walletAddress).
SetMint(splTokenMintAddress)
}
44 changes: 42 additions & 2 deletions programs/associated-token-account/instructions.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package associatedtokenaccount

import (
"bytes"
"fmt"

spew "github.com/davecgh/go-spew/spew"
Expand All @@ -25,18 +26,46 @@ import (
)

var ProgramID solana.PublicKey = solana.SPLAssociatedTokenAccountProgramID
var TokenProgramID solana.PublicKey = solana.TokenProgramID

func SetProgramID(pubkey solana.PublicKey) {
ProgramID = pubkey
solana.RegisterInstructionDecoder(ProgramID, registryDecodeInstruction)
}

func SetTokenProgramID(pubkey solana.PublicKey) {
TokenProgramID = pubkey
}

const ProgramName = "AssociatedTokenAccount"

func init() {
solana.RegisterInstructionDecoder(ProgramID, registryDecodeInstruction)
}

const (
// Creates an associated token account for the given wallet address and token mint
// Returns an error if the account exists.
Instruction_Create uint8 = iota

// Creates an associated token account for the given wallet address and token mint,
// if it doesn't already exist. Returns an error if the account exists,
// but with a different owner.
Instruction_CreateIdempotent
)

// InstructionIDToName returns the name of the instruction given its ID.
func InstructionIDToName(id uint8) string {
switch id {
case Instruction_Create:
return "Create"
case Instruction_CreateIdempotent:
return "CreateIdempotent"
default:
return ""
}
}

type Instruction struct {
bin.BaseVariant
}
Expand All @@ -50,11 +79,14 @@ func (inst *Instruction) EncodeToTree(parent treeout.Branches) {
}

var InstructionImplDef = bin.NewVariantDefinition(
bin.NoTypeIDEncoding, // NOTE: the associated-token-account program has no ID encoding.
bin.Uint8TypeIDEncoding,
[]bin.VariantType{
{
"Create", (*Create)(nil),
},
{
"CreateIdempotent", (*CreateIdempotent)(nil),
},
},
)

Expand All @@ -67,7 +99,11 @@ func (inst *Instruction) Accounts() (out []*solana.AccountMeta) {
}

func (inst *Instruction) Data() ([]byte, error) {
return []byte{}, nil
buf := new(bytes.Buffer)
if err := bin.NewBinEncoder(buf).Encode(inst); err != nil {
return nil, fmt.Errorf("unable to encode instruction: %w", err)
}
return buf.Bytes(), nil
}

func (inst *Instruction) TextEncode(encoder *text.Encoder, option *text.Option) error {
Expand All @@ -79,6 +115,10 @@ func (inst *Instruction) UnmarshalWithDecoder(decoder *bin.Decoder) error {
}

func (inst Instruction) MarshalWithEncoder(encoder *bin.Encoder) error {
err := encoder.WriteUint8(inst.TypeID.Uint8())
if err != nil {
return fmt.Errorf("unable to write variant type: %w", err)
}
return encoder.Encode(inst.Impl)
}

Expand Down
Loading