diff --git a/programs/token/AmountToUiAmount.go b/programs/token/AmountToUiAmount.go new file mode 100644 index 000000000..797f74ece --- /dev/null +++ b/programs/token/AmountToUiAmount.go @@ -0,0 +1,123 @@ +// 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 token + +import ( + "errors" + + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + ag_format "github.com/gagliardetto/solana-go/text/format" + ag_treeout "github.com/gagliardetto/treeout" +) + +// Convert an Amount of tokens to a UiAmount string, using the given mint. +// In this version of the program, the mint can only specify the number of decimals. +// +// Return data can be fetched using sol_get_return_data and deserialized +// with String::from_utf8. +type AmountToUiAmount struct { + // The amount of tokens to reformat. + Amount *uint64 + + // [0] = [] mint + // ··········· The mint to calculate for. + ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewAmountToUiAmountInstructionBuilder() *AmountToUiAmount { + nd := &AmountToUiAmount{ + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 1), + } + return nd +} + +func (inst *AmountToUiAmount) SetAmount(amount uint64) *AmountToUiAmount { + inst.Amount = &amount + return inst +} + +func (inst *AmountToUiAmount) SetMintAccount(mint ag_solanago.PublicKey) *AmountToUiAmount { + inst.AccountMetaSlice[0] = ag_solanago.Meta(mint) + return inst +} + +func (inst *AmountToUiAmount) GetMintAccount() *ag_solanago.AccountMeta { + return inst.AccountMetaSlice[0] +} + +func (inst AmountToUiAmount) Build() *Instruction { + return &Instruction{BaseVariant: ag_binary.BaseVariant{ + Impl: inst, + TypeID: ag_binary.TypeIDFromUint8(Instruction_AmountToUiAmount), + }} +} + +func (inst AmountToUiAmount) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *AmountToUiAmount) Validate() error { + if inst.Amount == nil { + return errors.New("Amount parameter is not set") + } + if inst.AccountMetaSlice[0] == nil { + return errors.New("accounts.Mint is not set") + } + return nil +} + +func (inst *AmountToUiAmount) EncodeToTree(parent ag_treeout.Branches) { + parent.Child(ag_format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch ag_treeout.Branches) { + programBranch.Child(ag_format.Instruction("AmountToUiAmount")). + ParentFunc(func(instructionBranch ag_treeout.Branches) { + instructionBranch.Child("Params").ParentFunc(func(paramsBranch ag_treeout.Branches) { + paramsBranch.Child(ag_format.Param("Amount", *inst.Amount)) + }) + instructionBranch.Child("Accounts").ParentFunc(func(accountsBranch ag_treeout.Branches) { + accountsBranch.Child(ag_format.Meta("mint", inst.AccountMetaSlice[0])) + }) + }) + }) +} + +func (obj AmountToUiAmount) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { + err = encoder.Encode(obj.Amount) + if err != nil { + return err + } + return nil +} + +func (obj *AmountToUiAmount) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) { + err = decoder.Decode(&obj.Amount) + if err != nil { + return err + } + return nil +} + +func NewAmountToUiAmountInstruction( + amount uint64, + mint ag_solanago.PublicKey, +) *AmountToUiAmount { + return NewAmountToUiAmountInstructionBuilder(). + SetAmount(amount). + SetMintAccount(mint) +} diff --git a/programs/token/AmountToUiAmount_test.go b/programs/token/AmountToUiAmount_test.go new file mode 100644 index 000000000..f4dcf7fd2 --- /dev/null +++ b/programs/token/AmountToUiAmount_test.go @@ -0,0 +1,44 @@ +// 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 token + +import ( + "bytes" + ag_gofuzz "github.com/gagliardetto/gofuzz" + ag_require "github.com/stretchr/testify/require" + "strconv" + "testing" +) + +func TestEncodeDecode_AmountToUiAmount(t *testing.T) { + fu := ag_gofuzz.New().NilChance(0) + for i := 0; i < 1; i++ { + t.Run("AmountToUiAmount"+strconv.Itoa(i), func(t *testing.T) { + { + params := new(AmountToUiAmount) + fu.Fuzz(params) + params.AccountMetaSlice = nil + buf := new(bytes.Buffer) + err := encodeT(*params, buf) + ag_require.NoError(t, err) + got := new(AmountToUiAmount) + err = decodeT(got, buf.Bytes()) + got.AccountMetaSlice = nil + ag_require.NoError(t, err) + ag_require.Equal(t, params, got) + } + }) + } +} diff --git a/programs/token/Batch.go b/programs/token/Batch.go new file mode 100644 index 000000000..23ab057f4 --- /dev/null +++ b/programs/token/Batch.go @@ -0,0 +1,173 @@ +// 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 token + +import ( + "bytes" + "errors" + "fmt" + + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + ag_format "github.com/gagliardetto/solana-go/text/format" + ag_treeout "github.com/gagliardetto/treeout" +) + +// Batch allows executing multiple token instructions in a single CPI call, +// reducing the overhead of multiple cross-program invocations. +// +// Each sub-instruction in the batch is prefixed with a 2-byte header: +// - byte 0: number of accounts for this sub-instruction +// - byte 1: length of instruction data for this sub-instruction +// +// This instruction is only available in the p-token (Pinocchio) implementation. +type Batch struct { + Instructions []*Instruction + + ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewBatchInstructionBuilder() *Batch { + return &Batch{ + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 0), + } +} + +func (inst *Batch) AddInstruction(ix *Instruction) *Batch { + inst.Instructions = append(inst.Instructions, ix) + return inst +} + +func (inst Batch) Build() *Instruction { + accounts := make(ag_solanago.AccountMetaSlice, 0) + for _, ix := range inst.Instructions { + accounts = append(accounts, ix.Accounts()...) + } + inst.AccountMetaSlice = accounts + + return &Instruction{BaseVariant: ag_binary.BaseVariant{ + Impl: inst, + TypeID: ag_binary.TypeIDFromUint8(Instruction_Batch), + }} +} + +func (inst Batch) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *Batch) Validate() error { + if len(inst.Instructions) == 0 { + return errors.New("batch must contain at least one instruction") + } + return nil +} + +func (inst *Batch) EncodeToTree(parent ag_treeout.Branches) { + parent.Child(ag_format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch ag_treeout.Branches) { + programBranch.Child(ag_format.Instruction("Batch")). + ParentFunc(func(instructionBranch ag_treeout.Branches) { + instructionBranch.Child("Params").ParentFunc(func(paramsBranch ag_treeout.Branches) { + paramsBranch.Child(ag_format.Param("InstructionCount", len(inst.Instructions))) + }) + instructionBranch.Child("Accounts").ParentFunc(func(accountsBranch ag_treeout.Branches) { + for i, acc := range inst.AccountMetaSlice { + accountsBranch.Child(ag_format.Meta(fmt.Sprintf("[%v]", i), acc)) + } + }) + }) + }) +} + +func (obj Batch) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { + for _, ix := range obj.Instructions { + accountCount := uint8(len(ix.Accounts())) + + data, err := ix.Data() + if err != nil { + return fmt.Errorf("unable to encode batch sub-instruction: %w", err) + } + // data includes the discriminator byte from the outer Instruction encoding, + // but for batch sub-instructions we need the raw inner data (discriminator + params). + // The ix.Data() already produces [discriminator | params], which is what we need. + dataLen := uint8(len(data)) + + if err = encoder.WriteUint8(accountCount); err != nil { + return err + } + if err = encoder.WriteUint8(dataLen); err != nil { + return err + } + if _, err = encoder.Write(data); err != nil { + return err + } + } + return nil +} + +func (obj *Batch) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) { + for decoder.HasRemaining() { + accountCount, err := decoder.ReadUint8() + if err != nil { + return err + } + dataLen, err := decoder.ReadUint8() + if err != nil { + return err + } + _ = accountCount + + data, err := decoder.ReadNBytes(int(dataLen)) + if err != nil { + return err + } + ix := new(Instruction) + if err = ag_binary.NewBinDecoder(data).Decode(ix); err != nil { + return fmt.Errorf("unable to decode batch sub-instruction: %w", err) + } + obj.Instructions = append(obj.Instructions, ix) + } + return nil +} + +// BuildBatchData constructs the complete instruction data for a batch, +// including the batch discriminator (255) and all sub-instruction data. +func BuildBatchData(instructions []*Instruction) ([]byte, error) { + buf := new(bytes.Buffer) + buf.WriteByte(Instruction_Batch) + for _, ix := range instructions { + accountCount := uint8(len(ix.Accounts())) + data, err := ix.Data() + if err != nil { + return nil, fmt.Errorf("unable to encode batch sub-instruction: %w", err) + } + dataLen := uint8(len(data)) + buf.WriteByte(accountCount) + buf.WriteByte(dataLen) + buf.Write(data) + } + return buf.Bytes(), nil +} + +func NewBatchInstruction(instructions ...*Instruction) *Batch { + b := NewBatchInstructionBuilder() + for _, ix := range instructions { + b.AddInstruction(ix) + } + return b +} diff --git a/programs/token/Batch_test.go b/programs/token/Batch_test.go new file mode 100644 index 000000000..275dc5e03 --- /dev/null +++ b/programs/token/Batch_test.go @@ -0,0 +1,38 @@ +// 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 token + +import ( + ag_require "github.com/stretchr/testify/require" + "testing" +) + +func TestEncodeDecode_Batch(t *testing.T) { + t.Run("Batch_InstructionIDToName", func(t *testing.T) { + ag_require.Equal(t, "Batch", InstructionIDToName(Instruction_Batch)) + ag_require.Equal(t, "WithdrawExcessLamports", InstructionIDToName(Instruction_WithdrawExcessLamports)) + ag_require.Equal(t, "UnwrapLamports", InstructionIDToName(Instruction_UnwrapLamports)) + }) + + t.Run("Batch_InstructionIDs", func(t *testing.T) { + ag_require.Equal(t, uint8(21), Instruction_GetAccountDataSize) + ag_require.Equal(t, uint8(22), Instruction_InitializeImmutableOwner) + ag_require.Equal(t, uint8(23), Instruction_AmountToUiAmount) + ag_require.Equal(t, uint8(24), Instruction_UiAmountToAmount) + ag_require.Equal(t, uint8(38), Instruction_WithdrawExcessLamports) + ag_require.Equal(t, uint8(45), Instruction_UnwrapLamports) + ag_require.Equal(t, uint8(255), Instruction_Batch) + }) +} diff --git a/programs/token/GetAccountDataSize.go b/programs/token/GetAccountDataSize.go new file mode 100644 index 000000000..2d8853372 --- /dev/null +++ b/programs/token/GetAccountDataSize.go @@ -0,0 +1,98 @@ +// 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 token + +import ( + "errors" + + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + ag_format "github.com/gagliardetto/solana-go/text/format" + ag_treeout "github.com/gagliardetto/treeout" +) + +// Gets the required size of an account for the given mint as a little-endian u64. +// Return data can be fetched using sol_get_return_data and deserializing +// the return data as a little-endian u64. +type GetAccountDataSize struct { + // [0] = [] mint + // ··········· The mint to calculate for. + ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewGetAccountDataSizeInstructionBuilder() *GetAccountDataSize { + nd := &GetAccountDataSize{ + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 1), + } + return nd +} + +func (inst *GetAccountDataSize) SetMintAccount(mint ag_solanago.PublicKey) *GetAccountDataSize { + inst.AccountMetaSlice[0] = ag_solanago.Meta(mint) + return inst +} + +func (inst *GetAccountDataSize) GetMintAccount() *ag_solanago.AccountMeta { + return inst.AccountMetaSlice[0] +} + +func (inst GetAccountDataSize) Build() *Instruction { + return &Instruction{BaseVariant: ag_binary.BaseVariant{ + Impl: inst, + TypeID: ag_binary.TypeIDFromUint8(Instruction_GetAccountDataSize), + }} +} + +func (inst GetAccountDataSize) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *GetAccountDataSize) Validate() error { + if inst.AccountMetaSlice[0] == nil { + return errors.New("accounts.Mint is not set") + } + return nil +} + +func (inst *GetAccountDataSize) EncodeToTree(parent ag_treeout.Branches) { + parent.Child(ag_format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch ag_treeout.Branches) { + programBranch.Child(ag_format.Instruction("GetAccountDataSize")). + ParentFunc(func(instructionBranch ag_treeout.Branches) { + instructionBranch.Child("Params").ParentFunc(func(paramsBranch ag_treeout.Branches) {}) + instructionBranch.Child("Accounts").ParentFunc(func(accountsBranch ag_treeout.Branches) { + accountsBranch.Child(ag_format.Meta("mint", inst.AccountMetaSlice[0])) + }) + }) + }) +} + +func (obj GetAccountDataSize) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { + return nil +} + +func (obj *GetAccountDataSize) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) { + return nil +} + +func NewGetAccountDataSizeInstruction( + mint ag_solanago.PublicKey, +) *GetAccountDataSize { + return NewGetAccountDataSizeInstructionBuilder(). + SetMintAccount(mint) +} diff --git a/programs/token/GetAccountDataSize_test.go b/programs/token/GetAccountDataSize_test.go new file mode 100644 index 000000000..b0d5391b4 --- /dev/null +++ b/programs/token/GetAccountDataSize_test.go @@ -0,0 +1,44 @@ +// 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 token + +import ( + "bytes" + ag_gofuzz "github.com/gagliardetto/gofuzz" + ag_require "github.com/stretchr/testify/require" + "strconv" + "testing" +) + +func TestEncodeDecode_GetAccountDataSize(t *testing.T) { + fu := ag_gofuzz.New().NilChance(0) + for i := 0; i < 1; i++ { + t.Run("GetAccountDataSize"+strconv.Itoa(i), func(t *testing.T) { + { + params := new(GetAccountDataSize) + fu.Fuzz(params) + params.AccountMetaSlice = nil + buf := new(bytes.Buffer) + err := encodeT(*params, buf) + ag_require.NoError(t, err) + got := new(GetAccountDataSize) + err = decodeT(got, buf.Bytes()) + got.AccountMetaSlice = nil + ag_require.NoError(t, err) + ag_require.Equal(t, params, got) + } + }) + } +} diff --git a/programs/token/InitializeImmutableOwner.go b/programs/token/InitializeImmutableOwner.go new file mode 100644 index 000000000..d56bfa3d0 --- /dev/null +++ b/programs/token/InitializeImmutableOwner.go @@ -0,0 +1,101 @@ +// 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 token + +import ( + "errors" + + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + ag_format "github.com/gagliardetto/solana-go/text/format" + ag_treeout "github.com/gagliardetto/treeout" +) + +// Initialize the Immutable Owner extension for the given token account. +// Fails if the account has already been initialized, so must be called +// before InitializeAccount. +// +// No-ops in this version of the program, but is included for compatibility +// with the Associated Token Account program. +type InitializeImmutableOwner struct { + // [0] = [WRITE] account + // ··········· The account to initialize. + ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewInitializeImmutableOwnerInstructionBuilder() *InitializeImmutableOwner { + nd := &InitializeImmutableOwner{ + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 1), + } + return nd +} + +func (inst *InitializeImmutableOwner) SetAccount(account ag_solanago.PublicKey) *InitializeImmutableOwner { + inst.AccountMetaSlice[0] = ag_solanago.Meta(account).WRITE() + return inst +} + +func (inst *InitializeImmutableOwner) GetAccount() *ag_solanago.AccountMeta { + return inst.AccountMetaSlice[0] +} + +func (inst InitializeImmutableOwner) Build() *Instruction { + return &Instruction{BaseVariant: ag_binary.BaseVariant{ + Impl: inst, + TypeID: ag_binary.TypeIDFromUint8(Instruction_InitializeImmutableOwner), + }} +} + +func (inst InitializeImmutableOwner) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *InitializeImmutableOwner) Validate() error { + if inst.AccountMetaSlice[0] == nil { + return errors.New("accounts.Account is not set") + } + return nil +} + +func (inst *InitializeImmutableOwner) EncodeToTree(parent ag_treeout.Branches) { + parent.Child(ag_format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch ag_treeout.Branches) { + programBranch.Child(ag_format.Instruction("InitializeImmutableOwner")). + ParentFunc(func(instructionBranch ag_treeout.Branches) { + instructionBranch.Child("Params").ParentFunc(func(paramsBranch ag_treeout.Branches) {}) + instructionBranch.Child("Accounts").ParentFunc(func(accountsBranch ag_treeout.Branches) { + accountsBranch.Child(ag_format.Meta("account", inst.AccountMetaSlice[0])) + }) + }) + }) +} + +func (obj InitializeImmutableOwner) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { + return nil +} + +func (obj *InitializeImmutableOwner) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) { + return nil +} + +func NewInitializeImmutableOwnerInstruction( + account ag_solanago.PublicKey, +) *InitializeImmutableOwner { + return NewInitializeImmutableOwnerInstructionBuilder(). + SetAccount(account) +} diff --git a/programs/token/InitializeImmutableOwner_test.go b/programs/token/InitializeImmutableOwner_test.go new file mode 100644 index 000000000..c8394294c --- /dev/null +++ b/programs/token/InitializeImmutableOwner_test.go @@ -0,0 +1,44 @@ +// 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 token + +import ( + "bytes" + ag_gofuzz "github.com/gagliardetto/gofuzz" + ag_require "github.com/stretchr/testify/require" + "strconv" + "testing" +) + +func TestEncodeDecode_InitializeImmutableOwner(t *testing.T) { + fu := ag_gofuzz.New().NilChance(0) + for i := 0; i < 1; i++ { + t.Run("InitializeImmutableOwner"+strconv.Itoa(i), func(t *testing.T) { + { + params := new(InitializeImmutableOwner) + fu.Fuzz(params) + params.AccountMetaSlice = nil + buf := new(bytes.Buffer) + err := encodeT(*params, buf) + ag_require.NoError(t, err) + got := new(InitializeImmutableOwner) + err = decodeT(got, buf.Bytes()) + got.AccountMetaSlice = nil + ag_require.NoError(t, err) + ag_require.Equal(t, params, got) + } + }) + } +} diff --git a/programs/token/UiAmountToAmount.go b/programs/token/UiAmountToAmount.go new file mode 100644 index 000000000..bbf185623 --- /dev/null +++ b/programs/token/UiAmountToAmount.go @@ -0,0 +1,125 @@ +// 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 token + +import ( + "errors" + + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + ag_format "github.com/gagliardetto/solana-go/text/format" + ag_treeout "github.com/gagliardetto/treeout" +) + +// Convert a UiAmount of tokens to a little-endian u64 raw Amount, using the given mint. +// In this version of the program, the mint can only specify the number of decimals. +// +// Return data can be fetched using sol_get_return_data and deserializing +// the return data as a little-endian u64. +type UiAmountToAmount struct { + // The ui_amount of tokens to reformat. + UiAmount *string + + // [0] = [] mint + // ··········· The mint to calculate for. + ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func NewUiAmountToAmountInstructionBuilder() *UiAmountToAmount { + nd := &UiAmountToAmount{ + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 1), + } + return nd +} + +func (inst *UiAmountToAmount) SetUiAmount(uiAmount string) *UiAmountToAmount { + inst.UiAmount = &uiAmount + return inst +} + +func (inst *UiAmountToAmount) SetMintAccount(mint ag_solanago.PublicKey) *UiAmountToAmount { + inst.AccountMetaSlice[0] = ag_solanago.Meta(mint) + return inst +} + +func (inst *UiAmountToAmount) GetMintAccount() *ag_solanago.AccountMeta { + return inst.AccountMetaSlice[0] +} + +func (inst UiAmountToAmount) Build() *Instruction { + return &Instruction{BaseVariant: ag_binary.BaseVariant{ + Impl: inst, + TypeID: ag_binary.TypeIDFromUint8(Instruction_UiAmountToAmount), + }} +} + +func (inst UiAmountToAmount) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *UiAmountToAmount) Validate() error { + if inst.UiAmount == nil { + return errors.New("UiAmount parameter is not set") + } + if inst.AccountMetaSlice[0] == nil { + return errors.New("accounts.Mint is not set") + } + return nil +} + +func (inst *UiAmountToAmount) EncodeToTree(parent ag_treeout.Branches) { + parent.Child(ag_format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch ag_treeout.Branches) { + programBranch.Child(ag_format.Instruction("UiAmountToAmount")). + ParentFunc(func(instructionBranch ag_treeout.Branches) { + instructionBranch.Child("Params").ParentFunc(func(paramsBranch ag_treeout.Branches) { + paramsBranch.Child(ag_format.Param("UiAmount", *inst.UiAmount)) + }) + instructionBranch.Child("Accounts").ParentFunc(func(accountsBranch ag_treeout.Branches) { + accountsBranch.Child(ag_format.Meta("mint", inst.AccountMetaSlice[0])) + }) + }) + }) +} + +func (obj UiAmountToAmount) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { + _, err = encoder.Write([]byte(*obj.UiAmount)) + if err != nil { + return err + } + return nil +} + +func (obj *UiAmountToAmount) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) { + data, err := decoder.ReadNBytes(decoder.Remaining()) + if err != nil { + return err + } + s := string(data) + obj.UiAmount = &s + return nil +} + +func NewUiAmountToAmountInstruction( + uiAmount string, + mint ag_solanago.PublicKey, +) *UiAmountToAmount { + return NewUiAmountToAmountInstructionBuilder(). + SetUiAmount(uiAmount). + SetMintAccount(mint) +} diff --git a/programs/token/UiAmountToAmount_test.go b/programs/token/UiAmountToAmount_test.go new file mode 100644 index 000000000..bf1a0d6a6 --- /dev/null +++ b/programs/token/UiAmountToAmount_test.go @@ -0,0 +1,35 @@ +// 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 token + +import ( + "bytes" + ag_require "github.com/stretchr/testify/require" + "testing" +) + +func TestEncodeDecode_UiAmountToAmount(t *testing.T) { + t.Run("UiAmountToAmount", func(t *testing.T) { + uiAmount := "123.456" + params := &UiAmountToAmount{UiAmount: &uiAmount} + buf := new(bytes.Buffer) + err := encodeT(*params, buf) + ag_require.NoError(t, err) + got := new(UiAmountToAmount) + err = decodeT(got, buf.Bytes()) + ag_require.NoError(t, err) + ag_require.Equal(t, params.UiAmount, got.UiAmount) + }) +} diff --git a/programs/token/UnwrapLamports.go b/programs/token/UnwrapLamports.go new file mode 100644 index 000000000..84079eb25 --- /dev/null +++ b/programs/token/UnwrapLamports.go @@ -0,0 +1,226 @@ +// 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 token + +import ( + "errors" + "fmt" + + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + ag_format "github.com/gagliardetto/solana-go/text/format" + ag_treeout "github.com/gagliardetto/treeout" +) + +// Unwrap lamports from a native SOL token account, transferring them directly +// to a destination account without requiring a temporary associated token account. +// +// If Amount is nil, all lamports (the full token balance) are unwrapped. +// If Amount is set, only the specified amount is unwrapped. +// +// This instruction is only available in the p-token (Pinocchio) implementation. +type UnwrapLamports struct { + // The amount of lamports to unwrap (optional; nil means unwrap all). + Amount *uint64 + + // [0] = [WRITE] source + // ··········· The native SOL token account to unwrap from. + // + // [1] = [WRITE] destination + // ··········· The destination account for the lamports. + // + // [2] = [] authority + // ··········· The source account's owner/delegate. + // + // [3...] = [SIGNER] signers + // ··········· M signer accounts. + Accounts ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` + Signers ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func (obj *UnwrapLamports) SetAccounts(accounts []*ag_solanago.AccountMeta) error { + obj.Accounts, obj.Signers = ag_solanago.AccountMetaSlice(accounts).SplitFrom(3) + return nil +} + +func (slice UnwrapLamports) GetAccounts() (accounts []*ag_solanago.AccountMeta) { + accounts = append(accounts, slice.Accounts...) + accounts = append(accounts, slice.Signers...) + return +} + +func NewUnwrapLamportsInstructionBuilder() *UnwrapLamports { + nd := &UnwrapLamports{ + Accounts: make(ag_solanago.AccountMetaSlice, 3), + Signers: make(ag_solanago.AccountMetaSlice, 0), + } + return nd +} + +func (inst *UnwrapLamports) SetAmount(amount uint64) *UnwrapLamports { + inst.Amount = &amount + return inst +} + +func (inst *UnwrapLamports) SetSourceAccount(source ag_solanago.PublicKey) *UnwrapLamports { + inst.Accounts[0] = ag_solanago.Meta(source).WRITE() + return inst +} + +func (inst *UnwrapLamports) GetSourceAccount() *ag_solanago.AccountMeta { + return inst.Accounts[0] +} + +func (inst *UnwrapLamports) SetDestinationAccount(destination ag_solanago.PublicKey) *UnwrapLamports { + inst.Accounts[1] = ag_solanago.Meta(destination).WRITE() + return inst +} + +func (inst *UnwrapLamports) GetDestinationAccount() *ag_solanago.AccountMeta { + return inst.Accounts[1] +} + +func (inst *UnwrapLamports) SetAuthorityAccount(authority ag_solanago.PublicKey, multisigSigners ...ag_solanago.PublicKey) *UnwrapLamports { + inst.Accounts[2] = ag_solanago.Meta(authority) + if len(multisigSigners) == 0 { + inst.Accounts[2].SIGNER() + } + for _, signer := range multisigSigners { + inst.Signers = append(inst.Signers, ag_solanago.Meta(signer).SIGNER()) + } + return inst +} + +func (inst *UnwrapLamports) GetAuthorityAccount() *ag_solanago.AccountMeta { + return inst.Accounts[2] +} + +func (inst UnwrapLamports) Build() *Instruction { + return &Instruction{BaseVariant: ag_binary.BaseVariant{ + Impl: inst, + TypeID: ag_binary.TypeIDFromUint8(Instruction_UnwrapLamports), + }} +} + +func (inst UnwrapLamports) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *UnwrapLamports) Validate() error { + if inst.Accounts[0] == nil { + return errors.New("accounts.Source is not set") + } + if inst.Accounts[1] == nil { + return errors.New("accounts.Destination is not set") + } + if inst.Accounts[2] == nil { + return errors.New("accounts.Authority is not set") + } + if !inst.Accounts[2].IsSigner && len(inst.Signers) == 0 { + return fmt.Errorf("accounts.Signers is not set") + } + if len(inst.Signers) > MAX_SIGNERS { + return fmt.Errorf("too many signers; got %v, but max is 11", len(inst.Signers)) + } + return nil +} + +func (inst *UnwrapLamports) EncodeToTree(parent ag_treeout.Branches) { + parent.Child(ag_format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch ag_treeout.Branches) { + programBranch.Child(ag_format.Instruction("UnwrapLamports")). + ParentFunc(func(instructionBranch ag_treeout.Branches) { + instructionBranch.Child("Params").ParentFunc(func(paramsBranch ag_treeout.Branches) { + if inst.Amount != nil { + paramsBranch.Child(ag_format.Param("Amount", *inst.Amount)) + } else { + paramsBranch.Child(ag_format.Param("Amount", "all")) + } + }) + instructionBranch.Child("Accounts").ParentFunc(func(accountsBranch ag_treeout.Branches) { + accountsBranch.Child(ag_format.Meta(" source", inst.Accounts[0])) + accountsBranch.Child(ag_format.Meta("destination", inst.Accounts[1])) + accountsBranch.Child(ag_format.Meta(" authority", inst.Accounts[2])) + + signersBranch := accountsBranch.Child(fmt.Sprintf("signers[len=%v]", len(inst.Signers))) + for i, v := range inst.Signers { + if len(inst.Signers) > 9 && i < 10 { + signersBranch.Child(ag_format.Meta(fmt.Sprintf(" [%v]", i), v)) + } else { + signersBranch.Child(ag_format.Meta(fmt.Sprintf("[%v]", i), v)) + } + } + }) + }) + }) +} + +// On-chain format: u8(has_amount) + optional u64(amount) +// has_amount=0 means unwrap all, has_amount=1 means unwrap specified amount. +func (obj UnwrapLamports) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { + if obj.Amount == nil { + return encoder.WriteUint8(0) + } + if err = encoder.WriteUint8(1); err != nil { + return err + } + return encoder.WriteUint64(*obj.Amount, ag_binary.LE) +} + +func (obj *UnwrapLamports) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) { + hasAmount, err := decoder.ReadUint8() + if err != nil { + return err + } + if hasAmount == 0 { + obj.Amount = nil + return nil + } + amount, err := decoder.ReadUint64(ag_binary.LE) + if err != nil { + return err + } + obj.Amount = &amount + return nil +} + +func NewUnwrapLamportsInstruction( + source ag_solanago.PublicKey, + destination ag_solanago.PublicKey, + authority ag_solanago.PublicKey, + multisigSigners []ag_solanago.PublicKey, +) *UnwrapLamports { + return NewUnwrapLamportsInstructionBuilder(). + SetSourceAccount(source). + SetDestinationAccount(destination). + SetAuthorityAccount(authority, multisigSigners...) +} + +func NewUnwrapLamportsWithAmountInstruction( + amount uint64, + source ag_solanago.PublicKey, + destination ag_solanago.PublicKey, + authority ag_solanago.PublicKey, + multisigSigners []ag_solanago.PublicKey, +) *UnwrapLamports { + return NewUnwrapLamportsInstructionBuilder(). + SetAmount(amount). + SetSourceAccount(source). + SetDestinationAccount(destination). + SetAuthorityAccount(authority, multisigSigners...) +} diff --git a/programs/token/UnwrapLamports_test.go b/programs/token/UnwrapLamports_test.go new file mode 100644 index 000000000..d5cdb10b9 --- /dev/null +++ b/programs/token/UnwrapLamports_test.go @@ -0,0 +1,47 @@ +// 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 token + +import ( + "bytes" + ag_require "github.com/stretchr/testify/require" + "testing" +) + +func TestEncodeDecode_UnwrapLamports(t *testing.T) { + t.Run("UnwrapLamports_NoAmount", func(t *testing.T) { + params := &UnwrapLamports{Amount: nil} + buf := new(bytes.Buffer) + err := encodeT(*params, buf) + ag_require.NoError(t, err) + got := new(UnwrapLamports) + err = decodeT(got, buf.Bytes()) + ag_require.NoError(t, err) + ag_require.Nil(t, got.Amount) + }) + + t.Run("UnwrapLamports_WithAmount", func(t *testing.T) { + amount := uint64(1000000) + params := &UnwrapLamports{Amount: &amount} + buf := new(bytes.Buffer) + err := encodeT(*params, buf) + ag_require.NoError(t, err) + got := new(UnwrapLamports) + err = decodeT(got, buf.Bytes()) + ag_require.NoError(t, err) + ag_require.NotNil(t, got.Amount) + ag_require.Equal(t, amount, *got.Amount) + }) +} diff --git a/programs/token/WithdrawExcessLamports.go b/programs/token/WithdrawExcessLamports.go new file mode 100644 index 000000000..3e6f4b023 --- /dev/null +++ b/programs/token/WithdrawExcessLamports.go @@ -0,0 +1,174 @@ +// 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 token + +import ( + "errors" + "fmt" + + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + ag_format "github.com/gagliardetto/solana-go/text/format" + ag_treeout "github.com/gagliardetto/treeout" +) + +// Withdraw excess lamports from a token account, mint account, or multisig account. +// The excess lamports are the amount above the rent-exempt minimum balance. +// +// This instruction is only available in the p-token (Pinocchio) implementation. +type WithdrawExcessLamports struct { + // [0] = [WRITE] source + // ··········· The source account (token account, mint, or multisig). + // + // [1] = [WRITE] destination + // ··········· The destination account for the excess lamports. + // + // [2] = [] authority + // ··········· The source account's owner/authority. + // + // [3...] = [SIGNER] signers + // ··········· M signer accounts. + Accounts ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` + Signers ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +func (obj *WithdrawExcessLamports) SetAccounts(accounts []*ag_solanago.AccountMeta) error { + obj.Accounts, obj.Signers = ag_solanago.AccountMetaSlice(accounts).SplitFrom(3) + return nil +} + +func (slice WithdrawExcessLamports) GetAccounts() (accounts []*ag_solanago.AccountMeta) { + accounts = append(accounts, slice.Accounts...) + accounts = append(accounts, slice.Signers...) + return +} + +func NewWithdrawExcessLamportsInstructionBuilder() *WithdrawExcessLamports { + nd := &WithdrawExcessLamports{ + Accounts: make(ag_solanago.AccountMetaSlice, 3), + Signers: make(ag_solanago.AccountMetaSlice, 0), + } + return nd +} + +func (inst *WithdrawExcessLamports) SetSourceAccount(source ag_solanago.PublicKey) *WithdrawExcessLamports { + inst.Accounts[0] = ag_solanago.Meta(source).WRITE() + return inst +} + +func (inst *WithdrawExcessLamports) GetSourceAccount() *ag_solanago.AccountMeta { + return inst.Accounts[0] +} + +func (inst *WithdrawExcessLamports) SetDestinationAccount(destination ag_solanago.PublicKey) *WithdrawExcessLamports { + inst.Accounts[1] = ag_solanago.Meta(destination).WRITE() + return inst +} + +func (inst *WithdrawExcessLamports) GetDestinationAccount() *ag_solanago.AccountMeta { + return inst.Accounts[1] +} + +func (inst *WithdrawExcessLamports) SetAuthorityAccount(authority ag_solanago.PublicKey, multisigSigners ...ag_solanago.PublicKey) *WithdrawExcessLamports { + inst.Accounts[2] = ag_solanago.Meta(authority) + if len(multisigSigners) == 0 { + inst.Accounts[2].SIGNER() + } + for _, signer := range multisigSigners { + inst.Signers = append(inst.Signers, ag_solanago.Meta(signer).SIGNER()) + } + return inst +} + +func (inst *WithdrawExcessLamports) GetAuthorityAccount() *ag_solanago.AccountMeta { + return inst.Accounts[2] +} + +func (inst WithdrawExcessLamports) Build() *Instruction { + return &Instruction{BaseVariant: ag_binary.BaseVariant{ + Impl: inst, + TypeID: ag_binary.TypeIDFromUint8(Instruction_WithdrawExcessLamports), + }} +} + +func (inst WithdrawExcessLamports) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *WithdrawExcessLamports) Validate() error { + if inst.Accounts[0] == nil { + return errors.New("accounts.Source is not set") + } + if inst.Accounts[1] == nil { + return errors.New("accounts.Destination is not set") + } + if inst.Accounts[2] == nil { + return errors.New("accounts.Authority is not set") + } + if !inst.Accounts[2].IsSigner && len(inst.Signers) == 0 { + return fmt.Errorf("accounts.Signers is not set") + } + if len(inst.Signers) > MAX_SIGNERS { + return fmt.Errorf("too many signers; got %v, but max is 11", len(inst.Signers)) + } + return nil +} + +func (inst *WithdrawExcessLamports) EncodeToTree(parent ag_treeout.Branches) { + parent.Child(ag_format.Program(ProgramName, ProgramID)). + ParentFunc(func(programBranch ag_treeout.Branches) { + programBranch.Child(ag_format.Instruction("WithdrawExcessLamports")). + ParentFunc(func(instructionBranch ag_treeout.Branches) { + instructionBranch.Child("Params").ParentFunc(func(paramsBranch ag_treeout.Branches) {}) + instructionBranch.Child("Accounts").ParentFunc(func(accountsBranch ag_treeout.Branches) { + accountsBranch.Child(ag_format.Meta(" source", inst.Accounts[0])) + accountsBranch.Child(ag_format.Meta("destination", inst.Accounts[1])) + accountsBranch.Child(ag_format.Meta(" authority", inst.Accounts[2])) + + signersBranch := accountsBranch.Child(fmt.Sprintf("signers[len=%v]", len(inst.Signers))) + for i, v := range inst.Signers { + if len(inst.Signers) > 9 && i < 10 { + signersBranch.Child(ag_format.Meta(fmt.Sprintf(" [%v]", i), v)) + } else { + signersBranch.Child(ag_format.Meta(fmt.Sprintf("[%v]", i), v)) + } + } + }) + }) + }) +} + +func (obj WithdrawExcessLamports) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { + return nil +} + +func (obj *WithdrawExcessLamports) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) { + return nil +} + +func NewWithdrawExcessLamportsInstruction( + source ag_solanago.PublicKey, + destination ag_solanago.PublicKey, + authority ag_solanago.PublicKey, + multisigSigners []ag_solanago.PublicKey, +) *WithdrawExcessLamports { + return NewWithdrawExcessLamportsInstructionBuilder(). + SetSourceAccount(source). + SetDestinationAccount(destination). + SetAuthorityAccount(authority, multisigSigners...) +} diff --git a/programs/token/WithdrawExcessLamports_test.go b/programs/token/WithdrawExcessLamports_test.go new file mode 100644 index 000000000..0ce7a2666 --- /dev/null +++ b/programs/token/WithdrawExcessLamports_test.go @@ -0,0 +1,46 @@ +// 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 token + +import ( + "bytes" + ag_gofuzz "github.com/gagliardetto/gofuzz" + ag_require "github.com/stretchr/testify/require" + "strconv" + "testing" +) + +func TestEncodeDecode_WithdrawExcessLamports(t *testing.T) { + fu := ag_gofuzz.New().NilChance(0) + for i := 0; i < 1; i++ { + t.Run("WithdrawExcessLamports"+strconv.Itoa(i), func(t *testing.T) { + { + params := new(WithdrawExcessLamports) + fu.Fuzz(params) + params.Accounts = nil + params.Signers = nil + buf := new(bytes.Buffer) + err := encodeT(*params, buf) + ag_require.NoError(t, err) + got := new(WithdrawExcessLamports) + err = decodeT(got, buf.Bytes()) + got.Accounts = nil + got.Signers = nil + ag_require.NoError(t, err) + ag_require.Equal(t, params, got) + } + }) + } +} diff --git a/programs/token/instructions.go b/programs/token/instructions.go index cda804b1d..3a284e2d9 100644 --- a/programs/token/instructions.go +++ b/programs/token/instructions.go @@ -173,6 +173,34 @@ const ( // Like InitializeMint, but does not require the Rent sysvar to be provided. Instruction_InitializeMint2 + + // Gets the required size of an account for the given mint as a little-endian u64. + Instruction_GetAccountDataSize + + // Initialize the Immutable Owner extension for the given token account. + // No-ops in this version of the program, but is included for compatibility + // with the Associated Token Account program. + Instruction_InitializeImmutableOwner + + // Convert an Amount of tokens to a UiAmount string, using the given mint. + Instruction_AmountToUiAmount + + // Convert a UiAmount of tokens to a little-endian u64 raw Amount, using the given mint. + Instruction_UiAmountToAmount +) + +const ( + // Withdraw excess lamports from a token account, mint, or multisig. + // Only available in the p-token (Pinocchio) implementation. + Instruction_WithdrawExcessLamports uint8 = 38 + + // Unwrap lamports from a native SOL token account directly to a destination. + // Only available in the p-token (Pinocchio) implementation. + Instruction_UnwrapLamports uint8 = 45 + + // Execute multiple token instructions in a single call. + // Only available in the p-token (Pinocchio) implementation. + Instruction_Batch uint8 = 255 ) // InstructionIDToName returns the name of the instruction given its ID. @@ -220,6 +248,20 @@ func InstructionIDToName(id uint8) string { return "InitializeMultisig2" case Instruction_InitializeMint2: return "InitializeMint2" + case Instruction_GetAccountDataSize: + return "GetAccountDataSize" + case Instruction_InitializeImmutableOwner: + return "InitializeImmutableOwner" + case Instruction_AmountToUiAmount: + return "AmountToUiAmount" + case Instruction_UiAmountToAmount: + return "UiAmountToAmount" + case Instruction_WithdrawExcessLamports: + return "WithdrawExcessLamports" + case Instruction_UnwrapLamports: + return "UnwrapLamports" + case Instruction_Batch: + return "Batch" default: return "" } @@ -237,75 +279,52 @@ func (inst *Instruction) EncodeToTree(parent ag_treeout.Branches) { } } +// instructionImplDefs contains the variant types for instruction IDs 0-24. +// IDs 0-20 are the original SPL Token instructions, IDs 21-24 are additional +// instructions added for Token-2022 compatibility. +var instructionImplDefs = []ag_binary.VariantType{ + {"InitializeMint", (*InitializeMint)(nil)}, // 0 + {"InitializeAccount", (*InitializeAccount)(nil)}, // 1 + {"InitializeMultisig", (*InitializeMultisig)(nil)}, // 2 + {"Transfer", (*Transfer)(nil)}, // 3 + {"Approve", (*Approve)(nil)}, // 4 + {"Revoke", (*Revoke)(nil)}, // 5 + {"SetAuthority", (*SetAuthority)(nil)}, // 6 + {"MintTo", (*MintTo)(nil)}, // 7 + {"Burn", (*Burn)(nil)}, // 8 + {"CloseAccount", (*CloseAccount)(nil)}, // 9 + {"FreezeAccount", (*FreezeAccount)(nil)}, // 10 + {"ThawAccount", (*ThawAccount)(nil)}, // 11 + {"TransferChecked", (*TransferChecked)(nil)}, // 12 + {"ApproveChecked", (*ApproveChecked)(nil)}, // 13 + {"MintToChecked", (*MintToChecked)(nil)}, // 14 + {"BurnChecked", (*BurnChecked)(nil)}, // 15 + {"InitializeAccount2", (*InitializeAccount2)(nil)}, // 16 + {"SyncNative", (*SyncNative)(nil)}, // 17 + {"InitializeAccount3", (*InitializeAccount3)(nil)}, // 18 + {"InitializeMultisig2", (*InitializeMultisig2)(nil)}, // 19 + {"InitializeMint2", (*InitializeMint2)(nil)}, // 20 + {"GetAccountDataSize", (*GetAccountDataSize)(nil)}, // 21 + {"InitializeImmutableOwner", (*InitializeImmutableOwner)(nil)}, // 22 + {"AmountToUiAmount", (*AmountToUiAmount)(nil)}, // 23 + {"UiAmountToAmount", (*UiAmountToAmount)(nil)}, // 24 +} + +// InstructionImplDef is the variant definition for instruction IDs 0-24. +// For p-token instructions with non-contiguous IDs (38, 45, 255), +// use DecodeInstruction which handles them via custom dispatch. var InstructionImplDef = ag_binary.NewVariantDefinition( ag_binary.Uint8TypeIDEncoding, - []ag_binary.VariantType{ - { - "InitializeMint", (*InitializeMint)(nil), - }, - { - "InitializeAccount", (*InitializeAccount)(nil), - }, - { - "InitializeMultisig", (*InitializeMultisig)(nil), - }, - { - "Transfer", (*Transfer)(nil), - }, - { - "Approve", (*Approve)(nil), - }, - { - "Revoke", (*Revoke)(nil), - }, - { - "SetAuthority", (*SetAuthority)(nil), - }, - { - "MintTo", (*MintTo)(nil), - }, - { - "Burn", (*Burn)(nil), - }, - { - "CloseAccount", (*CloseAccount)(nil), - }, - { - "FreezeAccount", (*FreezeAccount)(nil), - }, - { - "ThawAccount", (*ThawAccount)(nil), - }, - { - "TransferChecked", (*TransferChecked)(nil), - }, - { - "ApproveChecked", (*ApproveChecked)(nil), - }, - { - "MintToChecked", (*MintToChecked)(nil), - }, - { - "BurnChecked", (*BurnChecked)(nil), - }, - { - "InitializeAccount2", (*InitializeAccount2)(nil), - }, - { - "SyncNative", (*SyncNative)(nil), - }, - { - "InitializeAccount3", (*InitializeAccount3)(nil), - }, - { - "InitializeMultisig2", (*InitializeMultisig2)(nil), - }, - { - "InitializeMint2", (*InitializeMint2)(nil), - }, - }, + instructionImplDefs, ) +// pTokenInstructionMap maps non-contiguous p-token instruction IDs to their types. +var pTokenInstructionMap = map[uint8]ag_binary.VariantType{ + Instruction_WithdrawExcessLamports: {"WithdrawExcessLamports", (*WithdrawExcessLamports)(nil)}, + Instruction_UnwrapLamports: {"UnwrapLamports", (*UnwrapLamports)(nil)}, + Instruction_Batch: {"Batch", (*Batch)(nil)}, +} + func (inst *Instruction) ProgramID() ag_solanago.PublicKey { return ProgramID } @@ -347,6 +366,16 @@ func registryDecodeInstruction(accounts []*ag_solanago.AccountMeta, data []byte) } func DecodeInstruction(accounts []*ag_solanago.AccountMeta, data []byte) (*Instruction, error) { + if len(data) < 1 { + return nil, fmt.Errorf("instruction data is empty") + } + + discriminator := data[0] + + if vt, ok := pTokenInstructionMap[discriminator]; ok { + return decodePTokenInstruction(accounts, data, discriminator, vt) + } + inst := new(Instruction) if err := ag_binary.NewBinDecoder(data).Decode(inst); err != nil { return nil, fmt.Errorf("unable to decode instruction: %w", err) @@ -359,3 +388,41 @@ func DecodeInstruction(accounts []*ag_solanago.AccountMeta, data []byte) (*Instr } return inst, nil } + +func decodePTokenInstruction(accounts []*ag_solanago.AccountMeta, data []byte, discriminator uint8, vt ag_binary.VariantType) (*Instruction, error) { + inst := new(Instruction) + inst.TypeID = ag_binary.TypeIDFromUint8(discriminator) + + switch impl := vt.Type.(type) { + case *WithdrawExcessLamports: + _ = impl + obj := new(WithdrawExcessLamports) + if err := ag_binary.NewBinDecoder(data[1:]).Decode(obj); err != nil { + return nil, fmt.Errorf("unable to decode WithdrawExcessLamports: %w", err) + } + inst.Impl = obj + case *UnwrapLamports: + _ = impl + obj := new(UnwrapLamports) + if err := ag_binary.NewBinDecoder(data[1:]).Decode(obj); err != nil { + return nil, fmt.Errorf("unable to decode UnwrapLamports: %w", err) + } + inst.Impl = obj + case *Batch: + _ = impl + obj := new(Batch) + if err := ag_binary.NewBinDecoder(data[1:]).Decode(obj); err != nil { + return nil, fmt.Errorf("unable to decode Batch: %w", err) + } + inst.Impl = obj + default: + return nil, fmt.Errorf("unknown p-token instruction type for discriminator %d", discriminator) + } + + if v, ok := inst.Impl.(ag_solanago.AccountsSettable); ok { + if err := v.SetAccounts(accounts); err != nil { + return nil, fmt.Errorf("unable to set accounts for instruction: %w", err) + } + } + return inst, nil +} diff --git a/programs/token/ptoken_integration_test.go b/programs/token/ptoken_integration_test.go new file mode 100644 index 000000000..c5b76d58c --- /dev/null +++ b/programs/token/ptoken_integration_test.go @@ -0,0 +1,614 @@ +package token + +import ( + "bytes" + "encoding/binary" + "testing" + + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + ag_require "github.com/stretchr/testify/require" +) + +func TestInstructionIDs_MatchOnChain(t *testing.T) { + ag_require.Equal(t, uint8(0), Instruction_InitializeMint) + ag_require.Equal(t, uint8(1), Instruction_InitializeAccount) + ag_require.Equal(t, uint8(2), Instruction_InitializeMultisig) + ag_require.Equal(t, uint8(3), Instruction_Transfer) + ag_require.Equal(t, uint8(4), Instruction_Approve) + ag_require.Equal(t, uint8(5), Instruction_Revoke) + ag_require.Equal(t, uint8(6), Instruction_SetAuthority) + ag_require.Equal(t, uint8(7), Instruction_MintTo) + ag_require.Equal(t, uint8(8), Instruction_Burn) + ag_require.Equal(t, uint8(9), Instruction_CloseAccount) + ag_require.Equal(t, uint8(10), Instruction_FreezeAccount) + ag_require.Equal(t, uint8(11), Instruction_ThawAccount) + ag_require.Equal(t, uint8(12), Instruction_TransferChecked) + ag_require.Equal(t, uint8(13), Instruction_ApproveChecked) + ag_require.Equal(t, uint8(14), Instruction_MintToChecked) + ag_require.Equal(t, uint8(15), Instruction_BurnChecked) + ag_require.Equal(t, uint8(16), Instruction_InitializeAccount2) + ag_require.Equal(t, uint8(17), Instruction_SyncNative) + ag_require.Equal(t, uint8(18), Instruction_InitializeAccount3) + ag_require.Equal(t, uint8(19), Instruction_InitializeMultisig2) + ag_require.Equal(t, uint8(20), Instruction_InitializeMint2) + ag_require.Equal(t, uint8(21), Instruction_GetAccountDataSize) + ag_require.Equal(t, uint8(22), Instruction_InitializeImmutableOwner) + ag_require.Equal(t, uint8(23), Instruction_AmountToUiAmount) + ag_require.Equal(t, uint8(24), Instruction_UiAmountToAmount) + ag_require.Equal(t, uint8(38), Instruction_WithdrawExcessLamports) + ag_require.Equal(t, uint8(45), Instruction_UnwrapLamports) + ag_require.Equal(t, uint8(255), Instruction_Batch) +} + +func TestGetAccountDataSize_ByteFormat(t *testing.T) { + mint := ag_solanago.NewWallet().PublicKey() + ix := NewGetAccountDataSizeInstruction(mint) + built := ix.Build() + + data, err := built.Data() + ag_require.NoError(t, err) + ag_require.Equal(t, []byte{21}, data) + + accounts := built.Accounts() + ag_require.Len(t, accounts, 1) + ag_require.Equal(t, mint, accounts[0].PublicKey) + ag_require.False(t, accounts[0].IsWritable) + ag_require.False(t, accounts[0].IsSigner) +} + +func TestInitializeImmutableOwner_ByteFormat(t *testing.T) { + account := ag_solanago.NewWallet().PublicKey() + ix := NewInitializeImmutableOwnerInstruction(account) + built := ix.Build() + + data, err := built.Data() + ag_require.NoError(t, err) + ag_require.Equal(t, []byte{22}, data) + + accounts := built.Accounts() + ag_require.Len(t, accounts, 1) + ag_require.Equal(t, account, accounts[0].PublicKey) + ag_require.True(t, accounts[0].IsWritable) +} + +func TestAmountToUiAmount_ByteFormat(t *testing.T) { + mint := ag_solanago.NewWallet().PublicKey() + amount := uint64(1_000_000_000) + ix := NewAmountToUiAmountInstruction(amount, mint) + built := ix.Build() + + data, err := built.Data() + ag_require.NoError(t, err) + + ag_require.Equal(t, byte(23), data[0]) + ag_require.Len(t, data, 9) + gotAmount := binary.LittleEndian.Uint64(data[1:9]) + ag_require.Equal(t, amount, gotAmount) +} + +func TestUiAmountToAmount_ByteFormat(t *testing.T) { + mint := ag_solanago.NewWallet().PublicKey() + uiAmount := "1.5" + ix := NewUiAmountToAmountInstruction(uiAmount, mint) + built := ix.Build() + + data, err := built.Data() + ag_require.NoError(t, err) + + ag_require.Equal(t, byte(24), data[0]) + ag_require.Equal(t, "1.5", string(data[1:])) +} + +func TestWithdrawExcessLamports_ByteFormat(t *testing.T) { + source := ag_solanago.NewWallet().PublicKey() + dest := ag_solanago.NewWallet().PublicKey() + authority := ag_solanago.NewWallet().PublicKey() + + ix := NewWithdrawExcessLamportsInstruction(source, dest, authority, nil) + built := ix.Build() + + data, err := built.Data() + ag_require.NoError(t, err) + ag_require.Equal(t, []byte{38}, data) + + accounts := built.Accounts() + ag_require.Len(t, accounts, 3) + ag_require.True(t, accounts[0].IsWritable) + ag_require.True(t, accounts[1].IsWritable) + ag_require.True(t, accounts[2].IsSigner) +} + +func TestWithdrawExcessLamports_Multisig(t *testing.T) { + source := ag_solanago.NewWallet().PublicKey() + dest := ag_solanago.NewWallet().PublicKey() + authority := ag_solanago.NewWallet().PublicKey() + signer1 := ag_solanago.NewWallet().PublicKey() + signer2 := ag_solanago.NewWallet().PublicKey() + + ix := NewWithdrawExcessLamportsInstruction(source, dest, authority, []ag_solanago.PublicKey{signer1, signer2}) + built := ix.Build() + + accounts := built.Accounts() + ag_require.Len(t, accounts, 5) + ag_require.False(t, accounts[2].IsSigner) + ag_require.True(t, accounts[3].IsSigner) + ag_require.True(t, accounts[4].IsSigner) +} + +func TestUnwrapLamports_ByteFormat_NoAmount(t *testing.T) { + source := ag_solanago.NewWallet().PublicKey() + dest := ag_solanago.NewWallet().PublicKey() + authority := ag_solanago.NewWallet().PublicKey() + + ix := NewUnwrapLamportsInstruction(source, dest, authority, nil) + built := ix.Build() + + data, err := built.Data() + ag_require.NoError(t, err) + ag_require.Equal(t, []byte{45, 0}, data) +} + +func TestUnwrapLamports_ByteFormat_WithAmount(t *testing.T) { + source := ag_solanago.NewWallet().PublicKey() + dest := ag_solanago.NewWallet().PublicKey() + authority := ag_solanago.NewWallet().PublicKey() + amount := uint64(500_000) + + ix := NewUnwrapLamportsWithAmountInstruction(amount, source, dest, authority, nil) + built := ix.Build() + + data, err := built.Data() + ag_require.NoError(t, err) + + ag_require.Equal(t, byte(45), data[0]) + ag_require.Equal(t, byte(1), data[1]) + ag_require.Len(t, data, 10) + gotAmount := binary.LittleEndian.Uint64(data[2:10]) + ag_require.Equal(t, amount, gotAmount) +} + +func TestDecodeInstruction_PTokenInstructions(t *testing.T) { + t.Run("DecodeWithdrawExcessLamports", func(t *testing.T) { + source := ag_solanago.NewWallet().PublicKey() + dest := ag_solanago.NewWallet().PublicKey() + authority := ag_solanago.NewWallet().PublicKey() + + accounts := []*ag_solanago.AccountMeta{ + ag_solanago.Meta(source).WRITE(), + ag_solanago.Meta(dest).WRITE(), + ag_solanago.Meta(authority).SIGNER(), + } + + data := []byte{38} + inst, err := DecodeInstruction(accounts, data) + ag_require.NoError(t, err) + ag_require.Equal(t, uint8(38), inst.TypeID.Uint8()) + + _, ok := inst.Impl.(*WithdrawExcessLamports) + ag_require.True(t, ok) + }) + + t.Run("DecodeUnwrapLamports_NoAmount", func(t *testing.T) { + accounts := []*ag_solanago.AccountMeta{ + ag_solanago.Meta(ag_solanago.NewWallet().PublicKey()).WRITE(), + ag_solanago.Meta(ag_solanago.NewWallet().PublicKey()).WRITE(), + ag_solanago.Meta(ag_solanago.NewWallet().PublicKey()).SIGNER(), + } + + data := []byte{45, 0} + inst, err := DecodeInstruction(accounts, data) + ag_require.NoError(t, err) + ag_require.Equal(t, uint8(45), inst.TypeID.Uint8()) + + unwrap, ok := inst.Impl.(*UnwrapLamports) + ag_require.True(t, ok) + ag_require.Nil(t, unwrap.Amount) + }) + + t.Run("DecodeUnwrapLamports_WithAmount", func(t *testing.T) { + accounts := []*ag_solanago.AccountMeta{ + ag_solanago.Meta(ag_solanago.NewWallet().PublicKey()).WRITE(), + ag_solanago.Meta(ag_solanago.NewWallet().PublicKey()).WRITE(), + ag_solanago.Meta(ag_solanago.NewWallet().PublicKey()).SIGNER(), + } + + amount := uint64(999) + buf := new(bytes.Buffer) + buf.WriteByte(45) + buf.WriteByte(1) + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, amount) + buf.Write(b) + + inst, err := DecodeInstruction(accounts, buf.Bytes()) + ag_require.NoError(t, err) + + unwrap, ok := inst.Impl.(*UnwrapLamports) + ag_require.True(t, ok) + ag_require.NotNil(t, unwrap.Amount) + ag_require.Equal(t, amount, *unwrap.Amount) + }) +} + +func TestDecodeInstruction_LegacyInstructions(t *testing.T) { + t.Run("DecodeGetAccountDataSize", func(t *testing.T) { + mint := ag_solanago.NewWallet().PublicKey() + accounts := []*ag_solanago.AccountMeta{ + ag_solanago.Meta(mint), + } + data := []byte{21} + inst, err := DecodeInstruction(accounts, data) + ag_require.NoError(t, err) + ag_require.Equal(t, uint8(21), inst.TypeID.Uint8()) + _, ok := inst.Impl.(*GetAccountDataSize) + ag_require.True(t, ok) + }) + + t.Run("DecodeInitializeImmutableOwner", func(t *testing.T) { + account := ag_solanago.NewWallet().PublicKey() + accounts := []*ag_solanago.AccountMeta{ + ag_solanago.Meta(account).WRITE(), + } + data := []byte{22} + inst, err := DecodeInstruction(accounts, data) + ag_require.NoError(t, err) + ag_require.Equal(t, uint8(22), inst.TypeID.Uint8()) + _, ok := inst.Impl.(*InitializeImmutableOwner) + ag_require.True(t, ok) + }) + + t.Run("DecodeAmountToUiAmount", func(t *testing.T) { + mint := ag_solanago.NewWallet().PublicKey() + accounts := []*ag_solanago.AccountMeta{ + ag_solanago.Meta(mint), + } + amount := uint64(1_000_000) + buf := new(bytes.Buffer) + buf.WriteByte(23) + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, amount) + buf.Write(b) + + inst, err := DecodeInstruction(accounts, buf.Bytes()) + ag_require.NoError(t, err) + ag_require.Equal(t, uint8(23), inst.TypeID.Uint8()) + a2u, ok := inst.Impl.(*AmountToUiAmount) + ag_require.True(t, ok) + ag_require.Equal(t, amount, *a2u.Amount) + }) + + t.Run("DecodeUiAmountToAmount", func(t *testing.T) { + mint := ag_solanago.NewWallet().PublicKey() + accounts := []*ag_solanago.AccountMeta{ + ag_solanago.Meta(mint), + } + buf := new(bytes.Buffer) + buf.WriteByte(24) + buf.WriteString("1.5") + + inst, err := DecodeInstruction(accounts, buf.Bytes()) + ag_require.NoError(t, err) + ag_require.Equal(t, uint8(24), inst.TypeID.Uint8()) + u2a, ok := inst.Impl.(*UiAmountToAmount) + ag_require.True(t, ok) + ag_require.Equal(t, "1.5", *u2a.UiAmount) + }) +} + +func TestBuildAndEncode_RoundTrip(t *testing.T) { + t.Run("Transfer_RoundTrip", func(t *testing.T) { + src := ag_solanago.NewWallet().PublicKey() + dst := ag_solanago.NewWallet().PublicKey() + owner := ag_solanago.NewWallet().PublicKey() + + ix := NewTransferInstruction(100, src, dst, owner, nil) + built := ix.Build() + + data, err := built.Data() + ag_require.NoError(t, err) + + decoded, err := DecodeInstruction(built.Accounts(), data) + ag_require.NoError(t, err) + + transfer, ok := decoded.Impl.(*Transfer) + ag_require.True(t, ok) + ag_require.Equal(t, uint64(100), *transfer.Amount) + }) + + t.Run("AmountToUiAmount_RoundTrip", func(t *testing.T) { + mint := ag_solanago.NewWallet().PublicKey() + ix := NewAmountToUiAmountInstruction(999, mint) + built := ix.Build() + + data, err := built.Data() + ag_require.NoError(t, err) + + decoded, err := DecodeInstruction(built.Accounts(), data) + ag_require.NoError(t, err) + + a2u, ok := decoded.Impl.(*AmountToUiAmount) + ag_require.True(t, ok) + ag_require.Equal(t, uint64(999), *a2u.Amount) + }) + + t.Run("WithdrawExcessLamports_RoundTrip", func(t *testing.T) { + source := ag_solanago.NewWallet().PublicKey() + dest := ag_solanago.NewWallet().PublicKey() + auth := ag_solanago.NewWallet().PublicKey() + + ix := NewWithdrawExcessLamportsInstruction(source, dest, auth, nil) + built := ix.Build() + + data, err := built.Data() + ag_require.NoError(t, err) + + decoded, err := DecodeInstruction(built.Accounts(), data) + ag_require.NoError(t, err) + + _, ok := decoded.Impl.(*WithdrawExcessLamports) + ag_require.True(t, ok) + ag_require.Equal(t, uint8(38), decoded.TypeID.Uint8()) + }) + + t.Run("UnwrapLamports_RoundTrip_WithAmount", func(t *testing.T) { + source := ag_solanago.NewWallet().PublicKey() + dest := ag_solanago.NewWallet().PublicKey() + auth := ag_solanago.NewWallet().PublicKey() + + ix := NewUnwrapLamportsWithAmountInstruction(12345, source, dest, auth, nil) + built := ix.Build() + + data, err := built.Data() + ag_require.NoError(t, err) + + decoded, err := DecodeInstruction(built.Accounts(), data) + ag_require.NoError(t, err) + + unwrap, ok := decoded.Impl.(*UnwrapLamports) + ag_require.True(t, ok) + ag_require.NotNil(t, unwrap.Amount) + ag_require.Equal(t, uint64(12345), *unwrap.Amount) + }) +} + +func TestInstructionIDToName_AllNew(t *testing.T) { + tests := []struct { + id uint8 + name string + }{ + {21, "GetAccountDataSize"}, + {22, "InitializeImmutableOwner"}, + {23, "AmountToUiAmount"}, + {24, "UiAmountToAmount"}, + {38, "WithdrawExcessLamports"}, + {45, "UnwrapLamports"}, + {255, "Batch"}, + } + for _, tt := range tests { + ag_require.Equal(t, tt.name, InstructionIDToName(tt.id)) + } +} + +func TestValidation(t *testing.T) { + t.Run("GetAccountDataSize_MissingMint", func(t *testing.T) { + ix := NewGetAccountDataSizeInstructionBuilder() + err := ix.Validate() + ag_require.Error(t, err) + }) + + t.Run("AmountToUiAmount_MissingAmount", func(t *testing.T) { + ix := NewAmountToUiAmountInstructionBuilder() + ix.SetMintAccount(ag_solanago.NewWallet().PublicKey()) + err := ix.Validate() + ag_require.Error(t, err) + }) + + t.Run("WithdrawExcessLamports_MissingAccounts", func(t *testing.T) { + ix := NewWithdrawExcessLamportsInstructionBuilder() + err := ix.Validate() + ag_require.Error(t, err) + }) + + t.Run("UnwrapLamports_MissingAccounts", func(t *testing.T) { + ix := NewUnwrapLamportsInstructionBuilder() + err := ix.Validate() + ag_require.Error(t, err) + }) + + t.Run("Batch_Empty", func(t *testing.T) { + ix := NewBatchInstructionBuilder() + err := ix.Validate() + ag_require.Error(t, err) + }) + + t.Run("WithdrawExcessLamports_TooManySigners", func(t *testing.T) { + ix := NewWithdrawExcessLamportsInstructionBuilder(). + SetSourceAccount(ag_solanago.NewWallet().PublicKey()). + SetDestinationAccount(ag_solanago.NewWallet().PublicKey()) + + signers := make([]ag_solanago.PublicKey, 12) + for i := range signers { + signers[i] = ag_solanago.NewWallet().PublicKey() + } + ix.SetAuthorityAccount(ag_solanago.NewWallet().PublicKey(), signers...) + err := ix.Validate() + ag_require.Error(t, err) + ag_require.Contains(t, err.Error(), "too many signers") + }) +} + +func TestMarshalWithEncoder_PTokenInstructions(t *testing.T) { + t.Run("WithdrawExcessLamports_MarshalEncode", func(t *testing.T) { + obj := WithdrawExcessLamports{ + Accounts: make(ag_solanago.AccountMetaSlice, 3), + Signers: make(ag_solanago.AccountMetaSlice, 0), + } + buf := new(bytes.Buffer) + enc := ag_binary.NewBinEncoder(buf) + err := obj.MarshalWithEncoder(enc) + ag_require.NoError(t, err) + ag_require.Equal(t, 0, buf.Len()) + }) + + t.Run("UnwrapLamports_MarshalEncode_Nil", func(t *testing.T) { + obj := UnwrapLamports{ + Amount: nil, + Accounts: make(ag_solanago.AccountMetaSlice, 3), + Signers: make(ag_solanago.AccountMetaSlice, 0), + } + buf := new(bytes.Buffer) + enc := ag_binary.NewBinEncoder(buf) + err := obj.MarshalWithEncoder(enc) + ag_require.NoError(t, err) + ag_require.Equal(t, []byte{0}, buf.Bytes()) + }) + + t.Run("UnwrapLamports_MarshalEncode_WithAmount", func(t *testing.T) { + amount := uint64(42) + obj := UnwrapLamports{ + Amount: &amount, + Accounts: make(ag_solanago.AccountMetaSlice, 3), + Signers: make(ag_solanago.AccountMetaSlice, 0), + } + buf := new(bytes.Buffer) + enc := ag_binary.NewBinEncoder(buf) + err := obj.MarshalWithEncoder(enc) + ag_require.NoError(t, err) + + expected := make([]byte, 9) + expected[0] = 1 + binary.LittleEndian.PutUint64(expected[1:], 42) + ag_require.Equal(t, expected, buf.Bytes()) + }) +} + +func TestBatch_ByteFormat(t *testing.T) { + src := ag_solanago.NewWallet().PublicKey() + dst := ag_solanago.NewWallet().PublicKey() + owner := ag_solanago.NewWallet().PublicKey() + + transfer1 := NewTransferInstruction(100, src, dst, owner, nil).Build() + transfer2 := NewTransferInstruction(200, src, dst, owner, nil).Build() + + batch := NewBatchInstruction(transfer1, transfer2) + built := batch.Build() + + data, err := built.Data() + ag_require.NoError(t, err) + + // byte 0: discriminator 255 + ag_require.Equal(t, byte(255), data[0]) + + // Sub-instruction 1 header + // transfer has 3 accounts, data is [3 (discriminator) + 8 bytes (u64 amount)] = 9 bytes + ag_require.Equal(t, byte(3), data[1]) // num_accounts + ag_require.Equal(t, byte(9), data[2]) // data_len + ag_require.Equal(t, byte(3), data[3]) // Transfer discriminator (ID=3) + amount1 := binary.LittleEndian.Uint64(data[4:12]) + ag_require.Equal(t, uint64(100), amount1) + + // Sub-instruction 2 header + ag_require.Equal(t, byte(3), data[12]) // num_accounts + ag_require.Equal(t, byte(9), data[13]) // data_len + ag_require.Equal(t, byte(3), data[14]) // Transfer discriminator + amount2 := binary.LittleEndian.Uint64(data[15:23]) + ag_require.Equal(t, uint64(200), amount2) + + ag_require.Len(t, data, 23) +} + +func TestBatch_Accounts(t *testing.T) { + src := ag_solanago.NewWallet().PublicKey() + dst := ag_solanago.NewWallet().PublicKey() + owner := ag_solanago.NewWallet().PublicKey() + mint := ag_solanago.NewWallet().PublicKey() + + transfer := NewTransferInstruction(100, src, dst, owner, nil).Build() + getSize := NewGetAccountDataSizeInstruction(mint).Build() + + batch := NewBatchInstruction(transfer, getSize) + built := batch.Build() + + accounts := built.Accounts() + ag_require.Len(t, accounts, 4) // 3 from transfer + 1 from getSize +} + +func TestBatch_BuildBatchData(t *testing.T) { + src := ag_solanago.NewWallet().PublicKey() + dst := ag_solanago.NewWallet().PublicKey() + owner := ag_solanago.NewWallet().PublicKey() + + transfer := NewTransferInstruction(50, src, dst, owner, nil).Build() + + batchData, err := BuildBatchData([]*Instruction{transfer}) + ag_require.NoError(t, err) + + ag_require.Equal(t, byte(255), batchData[0]) + ag_require.Equal(t, byte(3), batchData[1]) // num_accounts + ag_require.Equal(t, byte(9), batchData[2]) // data_len + ag_require.Equal(t, byte(3), batchData[3]) // Transfer discriminator + amount := binary.LittleEndian.Uint64(batchData[4:12]) + ag_require.Equal(t, uint64(50), amount) +} + +func TestBatch_DecodeRoundTrip(t *testing.T) { + src := ag_solanago.NewWallet().PublicKey() + dst := ag_solanago.NewWallet().PublicKey() + owner := ag_solanago.NewWallet().PublicKey() + + transfer1 := NewTransferInstruction(100, src, dst, owner, nil).Build() + transfer2 := NewTransferInstruction(200, src, dst, owner, nil).Build() + + batch := NewBatchInstruction(transfer1, transfer2) + built := batch.Build() + + data, err := built.Data() + ag_require.NoError(t, err) + + accounts := built.Accounts() + decoded, err := DecodeInstruction(accounts, data) + ag_require.NoError(t, err) + ag_require.Equal(t, uint8(255), decoded.TypeID.Uint8()) + + decodedBatch, ok := decoded.Impl.(*Batch) + ag_require.True(t, ok) + ag_require.Len(t, decodedBatch.Instructions, 2) + + t1, ok := decodedBatch.Instructions[0].Impl.(*Transfer) + ag_require.True(t, ok) + ag_require.Equal(t, uint64(100), *t1.Amount) + + t2, ok := decodedBatch.Instructions[1].Impl.(*Transfer) + ag_require.True(t, ok) + ag_require.Equal(t, uint64(200), *t2.Amount) +} + +func TestBatch_MixedInstructions(t *testing.T) { + src := ag_solanago.NewWallet().PublicKey() + dst := ag_solanago.NewWallet().PublicKey() + owner := ag_solanago.NewWallet().PublicKey() + mint := ag_solanago.NewWallet().PublicKey() + + transfer := NewTransferInstruction(500, src, dst, owner, nil).Build() + getSize := NewGetAccountDataSizeInstruction(mint).Build() + + batch := NewBatchInstruction(transfer, getSize) + built := batch.Build() + + data, err := built.Data() + ag_require.NoError(t, err) + + ag_require.Equal(t, byte(255), data[0]) + + // Sub-instruction 1: Transfer (3 accounts, 9 bytes data) + ag_require.Equal(t, byte(3), data[1]) + ag_require.Equal(t, byte(9), data[2]) + ag_require.Equal(t, byte(3), data[3]) // Transfer ID + + // Sub-instruction 2: GetAccountDataSize (1 account, 1 byte data) + offset := 3 + 9 + ag_require.Equal(t, byte(1), data[offset]) // num_accounts + ag_require.Equal(t, byte(1), data[offset+1]) // data_len + ag_require.Equal(t, byte(21), data[offset+2]) // GetAccountDataSize ID +} diff --git a/programs/token/ptoken_testnet_test.go b/programs/token/ptoken_testnet_test.go new file mode 100644 index 000000000..7ecd53d86 --- /dev/null +++ b/programs/token/ptoken_testnet_test.go @@ -0,0 +1,400 @@ +package token + +import ( + "context" + "os" + "testing" + "time" + + ag_solanago "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/programs/system" + "github.com/gagliardetto/solana-go/rpc" + sendandconfirmtransaction "github.com/gagliardetto/solana-go/rpc/sendAndConfirmTransaction" + "github.com/gagliardetto/solana-go/rpc/ws" + ag_require "github.com/stretchr/testify/require" +) + +const testnetRPC = "https://api.testnet.solana.com" +const testnetWS = "wss://api.testnet.solana.com" + +func loadTestnetWallet(t *testing.T) ag_solanago.PrivateKey { + t.Helper() + home, err := os.UserHomeDir() + ag_require.NoError(t, err) + key, err := ag_solanago.PrivateKeyFromSolanaKeygenFile(home + "/.config/solana/id.json") + ag_require.NoError(t, err) + return key +} + +func signAndSend( + t *testing.T, + ctx context.Context, + rpcClient *rpc.Client, + wsClient *ws.Client, + payer ag_solanago.PrivateKey, + extraSigners []ag_solanago.PrivateKey, + instructions []ag_solanago.Instruction, +) ag_solanago.Signature { + t.Helper() + recent, err := rpcClient.GetLatestBlockhash(ctx, rpc.CommitmentFinalized) + ag_require.NoError(t, err) + + tx, err := ag_solanago.NewTransaction( + instructions, + recent.Value.Blockhash, + ag_solanago.TransactionPayer(payer.PublicKey()), + ) + ag_require.NoError(t, err) + + signerMap := map[ag_solanago.PublicKey]ag_solanago.PrivateKey{ + payer.PublicKey(): payer, + } + for _, s := range extraSigners { + signerMap[s.PublicKey()] = s + } + + _, err = tx.Sign(func(key ag_solanago.PublicKey) *ag_solanago.PrivateKey { + if pk, ok := signerMap[key]; ok { + return &pk + } + return nil + }) + ag_require.NoError(t, err) + + sig, err := sendandconfirmtransaction.SendAndConfirmTransactionWithTimeout( + ctx, rpcClient, wsClient, tx, 30*time.Second, + ) + ag_require.NoError(t, err) + return sig +} + +func TestTestnet_SimulateInitializeMint2(t *testing.T) { + if testing.Short() { + t.Skip("skipping testnet test in short mode") + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + client := rpc.New(testnetRPC) + payer := loadTestnetWallet(t) + mintKeypair := ag_solanago.NewWallet() + + rentExempt, err := client.GetMinimumBalanceForRentExemption(ctx, 82, rpc.CommitmentFinalized) + ag_require.NoError(t, err) + + recent, err := client.GetLatestBlockhash(ctx, rpc.CommitmentFinalized) + ag_require.NoError(t, err) + + tx, err := ag_solanago.NewTransaction( + []ag_solanago.Instruction{ + system.NewCreateAccountInstruction( + rentExempt, + 82, + ag_solanago.TokenProgramID, + payer.PublicKey(), + mintKeypair.PublicKey(), + ).Build(), + NewInitializeMint2Instruction( + 9, + payer.PublicKey(), + payer.PublicKey(), + mintKeypair.PublicKey(), + ).Build(), + }, + recent.Value.Blockhash, + ag_solanago.TransactionPayer(payer.PublicKey()), + ) + ag_require.NoError(t, err) + + _, err = tx.Sign(func(key ag_solanago.PublicKey) *ag_solanago.PrivateKey { + if key.Equals(payer.PublicKey()) { + return &payer + } + pk := mintKeypair.PrivateKey + return &pk + }) + ag_require.NoError(t, err) + + result, err := client.SimulateTransaction(ctx, tx) + ag_require.NoError(t, err) + ag_require.Nil(t, result.Value.Err, "simulation failed: %v", result.Value.Err) + t.Logf("InitializeMint2 simulation succeeded, consumed %d CUs", *result.Value.UnitsConsumed) +} + +func TestTestnet_SimulateGetAccountDataSize(t *testing.T) { + if testing.Short() { + t.Skip("skipping testnet test in short mode") + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + client := rpc.New(testnetRPC) + payer := loadTestnetWallet(t) + + nativeMint := ag_solanago.MustPublicKeyFromBase58("So11111111111111111111111111111111111111112") + + recent, err := client.GetLatestBlockhash(ctx, rpc.CommitmentFinalized) + ag_require.NoError(t, err) + + tx, err := ag_solanago.NewTransaction( + []ag_solanago.Instruction{ + NewGetAccountDataSizeInstruction(nativeMint).Build(), + }, + recent.Value.Blockhash, + ag_solanago.TransactionPayer(payer.PublicKey()), + ) + ag_require.NoError(t, err) + + _, err = tx.Sign(func(key ag_solanago.PublicKey) *ag_solanago.PrivateKey { + if key.Equals(payer.PublicKey()) { + return &payer + } + return nil + }) + ag_require.NoError(t, err) + + result, err := client.SimulateTransaction(ctx, tx) + ag_require.NoError(t, err) + ag_require.Nil(t, result.Value.Err, "simulation failed: %v", result.Value.Err) + t.Logf("GetAccountDataSize simulation succeeded, consumed %d CUs", *result.Value.UnitsConsumed) +} + +func TestTestnet_SimulateAmountToUiAmount(t *testing.T) { + if testing.Short() { + t.Skip("skipping testnet test in short mode") + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + client := rpc.New(testnetRPC) + payer := loadTestnetWallet(t) + + nativeMint := ag_solanago.MustPublicKeyFromBase58("So11111111111111111111111111111111111111112") + + recent, err := client.GetLatestBlockhash(ctx, rpc.CommitmentFinalized) + ag_require.NoError(t, err) + + tx, err := ag_solanago.NewTransaction( + []ag_solanago.Instruction{ + NewAmountToUiAmountInstruction(1_000_000_000, nativeMint).Build(), + }, + recent.Value.Blockhash, + ag_solanago.TransactionPayer(payer.PublicKey()), + ) + ag_require.NoError(t, err) + + _, err = tx.Sign(func(key ag_solanago.PublicKey) *ag_solanago.PrivateKey { + if key.Equals(payer.PublicKey()) { + return &payer + } + return nil + }) + ag_require.NoError(t, err) + + result, err := client.SimulateTransaction(ctx, tx) + ag_require.NoError(t, err) + ag_require.Nil(t, result.Value.Err, "simulation failed: %v", result.Value.Err) + t.Logf("AmountToUiAmount simulation succeeded, consumed %d CUs", *result.Value.UnitsConsumed) +} + +func TestTestnet_SimulateUiAmountToAmount(t *testing.T) { + if testing.Short() { + t.Skip("skipping testnet test in short mode") + } + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + client := rpc.New(testnetRPC) + payer := loadTestnetWallet(t) + + nativeMint := ag_solanago.MustPublicKeyFromBase58("So11111111111111111111111111111111111111112") + + recent, err := client.GetLatestBlockhash(ctx, rpc.CommitmentFinalized) + ag_require.NoError(t, err) + + tx, err := ag_solanago.NewTransaction( + []ag_solanago.Instruction{ + NewUiAmountToAmountInstruction("1.5", nativeMint).Build(), + }, + recent.Value.Blockhash, + ag_solanago.TransactionPayer(payer.PublicKey()), + ) + ag_require.NoError(t, err) + + _, err = tx.Sign(func(key ag_solanago.PublicKey) *ag_solanago.PrivateKey { + if key.Equals(payer.PublicKey()) { + return &payer + } + return nil + }) + ag_require.NoError(t, err) + + result, err := client.SimulateTransaction(ctx, tx) + ag_require.NoError(t, err) + ag_require.Nil(t, result.Value.Err, "simulation failed: %v", result.Value.Err) + t.Logf("UiAmountToAmount simulation succeeded, consumed %d CUs", *result.Value.UnitsConsumed) +} + +func TestTestnet_WithdrawExcessLamports(t *testing.T) { + if testing.Short() { + t.Skip("skipping testnet test in short mode") + } + + ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second) + defer cancel() + + rpcClient := rpc.New(testnetRPC) + wsClient, err := ws.Connect(ctx, testnetWS) + ag_require.NoError(t, err) + defer wsClient.Close() + + payer := loadTestnetWallet(t) + + mintKeypair := ag_solanago.NewWallet() + rentExempt, err := rpcClient.GetMinimumBalanceForRentExemption(ctx, 82, rpc.CommitmentFinalized) + ag_require.NoError(t, err) + + // Fund the mint with extra lamports above rent-exempt minimum. + extraLamports := uint64(100_000) + sig := signAndSend(t, ctx, rpcClient, wsClient, payer, []ag_solanago.PrivateKey{mintKeypair.PrivateKey}, []ag_solanago.Instruction{ + system.NewCreateAccountInstruction( + rentExempt+extraLamports, + 82, + ag_solanago.TokenProgramID, + payer.PublicKey(), + mintKeypair.PublicKey(), + ).Build(), + NewInitializeMint2Instruction( + 9, + payer.PublicKey(), + payer.PublicKey(), + mintKeypair.PublicKey(), + ).Build(), + }) + t.Logf("Created mint with excess lamports: %s", sig) + + // Now withdraw excess lamports using the p-token instruction (ID 38). + sig = signAndSend(t, ctx, rpcClient, wsClient, payer, nil, []ag_solanago.Instruction{ + NewWithdrawExcessLamportsInstruction( + mintKeypair.PublicKey(), + payer.PublicKey(), + payer.PublicKey(), + nil, + ).Build(), + }) + t.Logf("WithdrawExcessLamports succeeded: %s", sig) +} + +func TestTestnet_UnwrapLamports(t *testing.T) { + if testing.Short() { + t.Skip("skipping testnet test in short mode") + } + + ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) + defer cancel() + + rpcClient := rpc.New(testnetRPC) + wsClient, err := ws.Connect(ctx, testnetWS) + ag_require.NoError(t, err) + defer wsClient.Close() + + payer := loadTestnetWallet(t) + nativeMint := ag_solanago.SolMint + + tokenAccKeypair := ag_solanago.NewWallet() + rentExempt, err := rpcClient.GetMinimumBalanceForRentExemption(ctx, 165, rpc.CommitmentFinalized) + ag_require.NoError(t, err) + + wrapAmount := uint64(10_000_000) // 0.01 SOL + sig := signAndSend(t, ctx, rpcClient, wsClient, payer, []ag_solanago.PrivateKey{tokenAccKeypair.PrivateKey}, []ag_solanago.Instruction{ + system.NewCreateAccountInstruction( + rentExempt+wrapAmount, + 165, + ag_solanago.TokenProgramID, + payer.PublicKey(), + tokenAccKeypair.PublicKey(), + ).Build(), + NewInitializeAccount3Instruction( + payer.PublicKey(), + tokenAccKeypair.PublicKey(), + nativeMint, + ).Build(), + }) + t.Logf("Created wrapped SOL account and funded: %s", sig) + + // Unwrap partial using UnwrapLamports (ID 45) with a specific amount. + unwrapAmount := uint64(5_000_000) // 0.005 SOL + sig = signAndSend(t, ctx, rpcClient, wsClient, payer, nil, []ag_solanago.Instruction{ + NewUnwrapLamportsWithAmountInstruction( + unwrapAmount, + tokenAccKeypair.PublicKey(), + payer.PublicKey(), + payer.PublicKey(), + nil, + ).Build(), + }) + t.Logf("UnwrapLamports (partial) succeeded: %s", sig) + + // Unwrap remaining using UnwrapLamports with nil amount (unwrap all). + sig = signAndSend(t, ctx, rpcClient, wsClient, payer, nil, []ag_solanago.Instruction{ + NewUnwrapLamportsInstruction( + tokenAccKeypair.PublicKey(), + payer.PublicKey(), + payer.PublicKey(), + nil, + ).Build(), + }) + t.Logf("UnwrapLamports (all) succeeded: %s", sig) +} + +func TestTestnet_Batch(t *testing.T) { + if testing.Short() { + t.Skip("skipping testnet test in short mode") + } + + ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) + defer cancel() + + rpcClient := rpc.New(testnetRPC) + wsClient, err := ws.Connect(ctx, testnetWS) + ag_require.NoError(t, err) + defer wsClient.Close() + + payer := loadTestnetWallet(t) + + mintKeypair := ag_solanago.NewWallet() + tokenAcc1Keypair := ag_solanago.NewWallet() + tokenAcc2Keypair := ag_solanago.NewWallet() + + mintRent, err := rpcClient.GetMinimumBalanceForRentExemption(ctx, 82, rpc.CommitmentFinalized) + ag_require.NoError(t, err) + accRent, err := rpcClient.GetMinimumBalanceForRentExemption(ctx, 165, rpc.CommitmentFinalized) + ag_require.NoError(t, err) + + // Create mint + two token accounts + mint tokens to account 1. + sig := signAndSend(t, ctx, rpcClient, wsClient, payer, + []ag_solanago.PrivateKey{mintKeypair.PrivateKey, tokenAcc1Keypair.PrivateKey, tokenAcc2Keypair.PrivateKey}, + []ag_solanago.Instruction{ + system.NewCreateAccountInstruction(mintRent, 82, ag_solanago.TokenProgramID, payer.PublicKey(), mintKeypair.PublicKey()).Build(), + NewInitializeMint2Instruction(9, payer.PublicKey(), payer.PublicKey(), mintKeypair.PublicKey()).Build(), + system.NewCreateAccountInstruction(accRent, 165, ag_solanago.TokenProgramID, payer.PublicKey(), tokenAcc1Keypair.PublicKey()).Build(), + NewInitializeAccount3Instruction(payer.PublicKey(), tokenAcc1Keypair.PublicKey(), mintKeypair.PublicKey()).Build(), + system.NewCreateAccountInstruction(accRent, 165, ag_solanago.TokenProgramID, payer.PublicKey(), tokenAcc2Keypair.PublicKey()).Build(), + NewInitializeAccount3Instruction(payer.PublicKey(), tokenAcc2Keypair.PublicKey(), mintKeypair.PublicKey()).Build(), + NewMintToInstruction(1000, mintKeypair.PublicKey(), tokenAcc1Keypair.PublicKey(), payer.PublicKey(), nil).Build(), + }, + ) + t.Logf("Setup (mint + accounts + mintTo): %s", sig) + + // Batch: two transfers from account1 -> account2 in a single instruction. + transfer1 := NewTransferInstruction(100, tokenAcc1Keypair.PublicKey(), tokenAcc2Keypair.PublicKey(), payer.PublicKey(), nil).Build() + transfer2 := NewTransferInstruction(200, tokenAcc1Keypair.PublicKey(), tokenAcc2Keypair.PublicKey(), payer.PublicKey(), nil).Build() + + batchIx := NewBatchInstruction(transfer1, transfer2).Build() + sig = signAndSend(t, ctx, rpcClient, wsClient, payer, nil, []ag_solanago.Instruction{batchIx}) + t.Logf("Batch (2 transfers) succeeded: %s", sig) +}