diff --git a/programs/token/AmountToUiAmount.go b/programs/token/AmountToUiAmount.go new file mode 100644 index 00000000..797f74ec --- /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 00000000..90e92408 --- /dev/null +++ b/programs/token/AmountToUiAmount_test.go @@ -0,0 +1,78 @@ +// 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" + "fmt" + ag_gofuzz "github.com/gagliardetto/gofuzz" + ag_solanago "github.com/gagliardetto/solana-go" + 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) + } + }) + } +} + +func TestAmountToUiAmount_Validate(t *testing.T) { + mint := ag_solanago.NewWallet().PublicKey() + + t.Run("missing amount returns error", func(t *testing.T) { + ix := NewAmountToUiAmountInstructionBuilder().SetMintAccount(mint) + ag_require.Error(t, ix.Validate()) + }) + + t.Run("missing mint returns error", func(t *testing.T) { + ix := NewAmountToUiAmountInstructionBuilder().SetAmount(1000) + ag_require.Error(t, ix.Validate()) + }) + + t.Run("all fields set passes validation", func(t *testing.T) { + ag_require.NoError(t, NewAmountToUiAmountInstruction(1000, mint).Validate()) + }) +} + +func TestAmountToUiAmount_EncodeDecode_EdgeCases(t *testing.T) { + for _, amount := range []uint64{0, 1, 1_000_000_000, ^uint64(0)} { + amount := amount + t.Run(fmt.Sprintf("amount=%d", amount), func(t *testing.T) { + params := &AmountToUiAmount{Amount: &amount} + buf := new(bytes.Buffer) + ag_require.NoError(t, encodeT(*params, buf)) + got := new(AmountToUiAmount) + ag_require.NoError(t, decodeT(got, buf.Bytes())) + ag_require.Equal(t, amount, *got.Amount) + }) + } +} diff --git a/programs/token/Batch.go b/programs/token/Batch.go new file mode 100644 index 00000000..bdc35b8a --- /dev/null +++ b/programs/token/Batch.go @@ -0,0 +1,152 @@ +// 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" +) + +// 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 { + data, err := ix.Data() + if err != nil { + return fmt.Errorf("unable to encode batch sub-instruction: %w", err) + } + if err = encoder.WriteUint8(uint8(len(ix.Accounts()))); err != nil { + return err + } + if err = encoder.WriteUint8(uint8(len(data))); 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 is a convenience wrapper that returns the fully-encoded batch +// instruction bytes (discriminator + sub-instruction headers + data). +func BuildBatchData(instructions []*Instruction) ([]byte, error) { + return NewBatchInstruction(instructions...).Build().Data() +} + +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 00000000..275dc5e0 --- /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 00000000..2d885337 --- /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 00000000..65ddf78a --- /dev/null +++ b/programs/token/GetAccountDataSize_test.go @@ -0,0 +1,66 @@ +// 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_solanago "github.com/gagliardetto/solana-go" + 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) + } + }) + } +} + +func TestGetAccountDataSize_Validate(t *testing.T) { + t.Run("missing mint returns error", func(t *testing.T) { + ix := NewGetAccountDataSizeInstructionBuilder() + ag_require.Error(t, ix.Validate()) + }) + + t.Run("with mint passes validation", func(t *testing.T) { + mint := ag_solanago.NewWallet().PublicKey() + ag_require.NoError(t, NewGetAccountDataSizeInstruction(mint).Validate()) + }) +} + +func TestGetAccountDataSize_Build(t *testing.T) { + mint := ag_solanago.NewWallet().PublicKey() + ix := NewGetAccountDataSizeInstruction(mint).Build() + ag_require.Equal(t, uint8(Instruction_GetAccountDataSize), ix.TypeID.Uint8()) + accounts := ix.Accounts() + ag_require.Len(t, accounts, 1) + ag_require.Equal(t, mint, accounts[0].PublicKey) +} diff --git a/programs/token/InitializeImmutableOwner.go b/programs/token/InitializeImmutableOwner.go new file mode 100644 index 00000000..d56bfa3d --- /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 00000000..c8394294 --- /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 00000000..bbf18562 --- /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 00000000..bf1a0d6a --- /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 00000000..84079eb2 --- /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 00000000..d5cdb10b --- /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 00000000..3e6f4b02 --- /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 00000000..0ce7a266 --- /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 410bfe8b..068d108f 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,47 @@ func (inst *Instruction) EncodeToTree(parent ag_treeout.Branches) { } } +// InstructionImplDef is the variant definition for contiguous instruction IDs 0–24. +// P-token instructions with non-contiguous IDs (38, 45, 255) are handled separately +// by DecodeInstruction via pTokenInstructionMap. var InstructionImplDef = ag_binary.NewVariantDefinition( ag_binary.Uint8TypeIDEncoding, []ag_binary.VariantType{ - { - Name: "InitializeMint", Type: (*InitializeMint)(nil), - }, - { - Name: "InitializeAccount", Type: (*InitializeAccount)(nil), - }, - { - Name: "InitializeMultisig", Type: (*InitializeMultisig)(nil), - }, - { - Name: "Transfer", Type: (*Transfer)(nil), - }, - { - Name: "Approve", Type: (*Approve)(nil), - }, - { - Name: "Revoke", Type: (*Revoke)(nil), - }, - { - Name: "SetAuthority", Type: (*SetAuthority)(nil), - }, - { - Name: "MintTo", Type: (*MintTo)(nil), - }, - { - Name: "Burn", Type: (*Burn)(nil), - }, - { - Name: "CloseAccount", Type: (*CloseAccount)(nil), - }, - { - Name: "FreezeAccount", Type: (*FreezeAccount)(nil), - }, - { - Name: "ThawAccount", Type: (*ThawAccount)(nil), - }, - { - Name: "TransferChecked", Type: (*TransferChecked)(nil), - }, - { - Name: "ApproveChecked", Type: (*ApproveChecked)(nil), - }, - { - Name: "MintToChecked", Type: (*MintToChecked)(nil), - }, - { - Name: "BurnChecked", Type: (*BurnChecked)(nil), - }, - { - Name: "InitializeAccount2", Type: (*InitializeAccount2)(nil), - }, - { - Name: "SyncNative", Type: (*SyncNative)(nil), - }, - { - Name: "InitializeAccount3", Type: (*InitializeAccount3)(nil), - }, - { - Name: "InitializeMultisig2", Type: (*InitializeMultisig2)(nil), - }, - { - Name: "InitializeMint2", Type: (*InitializeMint2)(nil), - }, + {Name: "InitializeMint", Type: (*InitializeMint)(nil)}, + {Name: "InitializeAccount", Type: (*InitializeAccount)(nil)}, + {Name: "InitializeMultisig", Type: (*InitializeMultisig)(nil)}, + {Name: "Transfer", Type: (*Transfer)(nil)}, + {Name: "Approve", Type: (*Approve)(nil)}, + {Name: "Revoke", Type: (*Revoke)(nil)}, + {Name: "SetAuthority", Type: (*SetAuthority)(nil)}, + {Name: "MintTo", Type: (*MintTo)(nil)}, + {Name: "Burn", Type: (*Burn)(nil)}, + {Name: "CloseAccount", Type: (*CloseAccount)(nil)}, + {Name: "FreezeAccount", Type: (*FreezeAccount)(nil)}, + {Name: "ThawAccount", Type: (*ThawAccount)(nil)}, + {Name: "TransferChecked", Type: (*TransferChecked)(nil)}, + {Name: "ApproveChecked", Type: (*ApproveChecked)(nil)}, + {Name: "MintToChecked", Type: (*MintToChecked)(nil)}, + {Name: "BurnChecked", Type: (*BurnChecked)(nil)}, + {Name: "InitializeAccount2", Type: (*InitializeAccount2)(nil)}, + {Name: "SyncNative", Type: (*SyncNative)(nil)}, + {Name: "InitializeAccount3", Type: (*InitializeAccount3)(nil)}, + {Name: "InitializeMultisig2", Type: (*InitializeMultisig2)(nil)}, + {Name: "InitializeMint2", Type: (*InitializeMint2)(nil)}, + {Name: "GetAccountDataSize", Type: (*GetAccountDataSize)(nil)}, + {Name: "InitializeImmutableOwner", Type: (*InitializeImmutableOwner)(nil)}, + {Name: "AmountToUiAmount", Type: (*AmountToUiAmount)(nil)}, + {Name: "UiAmountToAmount", Type: (*UiAmountToAmount)(nil)}, }, ) +// pTokenInstructionMap maps non-contiguous p-token instruction IDs to their types. +var pTokenInstructionMap = map[uint8]ag_binary.VariantType{ + Instruction_WithdrawExcessLamports: {Name: "WithdrawExcessLamports", Type: (*WithdrawExcessLamports)(nil)}, + Instruction_UnwrapLamports: {Name: "UnwrapLamports", Type: (*UnwrapLamports)(nil)}, + Instruction_Batch: {Name: "Batch", Type: (*Batch)(nil)}, +} + func (inst *Instruction) ProgramID() ag_solanago.PublicKey { return ProgramID } @@ -347,6 +361,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 +383,29 @@ 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) + + var impl ag_solanago.AccountsSettable + switch vt.Type.(type) { + case *WithdrawExcessLamports: + impl = new(WithdrawExcessLamports) + case *UnwrapLamports: + impl = new(UnwrapLamports) + case *Batch: + impl = new(Batch) + default: + return nil, fmt.Errorf("unknown p-token instruction type for discriminator %d", discriminator) + } + + if err := ag_binary.NewBinDecoder(data[1:]).Decode(impl); err != nil { + return nil, fmt.Errorf("unable to decode %T: %w", impl, err) + } + if err := impl.SetAccounts(accounts); err != nil { + return nil, fmt.Errorf("unable to set accounts for instruction: %w", err) + } + inst.Impl = impl + return inst, nil +}