Skip to content

Commit 620b2a4

Browse files
committed
feat(token): add p-token (SIMD-0266) and missing SPL Token instructions
Add support for 7 missing token instructions to the SPL Token program: Instructions 21-24 (existing on-chain, missing from solana-go): - GetAccountDataSize (ID 21) - InitializeImmutableOwner (ID 22) - AmountToUiAmount (ID 23) - UiAmountToAmount (ID 24) P-token exclusive instructions (SIMD-0266 / Pinocchio): - WithdrawExcessLamports (ID 38) - UnwrapLamports (ID 45) - Batch (ID 255) Updated instructions.go with new constants, InstructionIDToName entries, extended InstructionImplDef for IDs 0-24, and added pTokenInstructionMap with custom DecodeInstruction dispatch for non-contiguous p-token IDs. Verified on Solana testnet (SIMD-0266 feature gate activated at slot 396236256, address ptokFjwyJtrwCa9Kgo9xoDS59V4QccBGEaRFnRPnSdP): Simulation tests (IDs 20-24): - InitializeMint2: 364 CUs - GetAccountDataSize: 179 CUs - AmountToUiAmount: 615 CUs - UiAmountToAmount: 589 CUs Send+confirm tests (p-token exclusive): - WithdrawExcessLamports (ID 38): 3C9iNnUPPaqRCA1RSD5tXDpD7ZW72VQYsKYm1j3voEYcH4UCfzKURan63Df2jdfX3gzPzEHZDu6eFfnTf2K4H98d - UnwrapLamports partial (ID 45): zPrV9K6wLqnES4RBDAQs15S8MgvC48QjsLZAmPoimvY6vmQkh8yi3XBJYqQBkpo28HpiNdhuPL3N65Dqf1ovhWn - UnwrapLamports all (ID 45): 5VVDC6PeWo5sAAUhbc3xRRiZDMo1t5Y6c1VDktBqLLhXZtri1HshL9y7TmEu9BBZEpiQCdAmULazfNjTfeHd5ujH - Batch with 2 transfers (ID 255): 2y1jC5rXbQvvym1bM5ukRcQfiovUQPi4fbcryixXpjTe5fc2UDGvvgurZqLNx7DEYd7NHHwEnEt6oPX4anV9kXnh All 68 tests pass (30 existing + 38 new), zero regressions. Made-with: Cursor
1 parent ac7125b commit 620b2a4

17 files changed

Lines changed: 2457 additions & 63 deletions

programs/token/AmountToUiAmount.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// Copyright 2021 github.com/gagliardetto
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package token
16+
17+
import (
18+
"errors"
19+
20+
ag_binary "github.com/gagliardetto/binary"
21+
ag_solanago "github.com/gagliardetto/solana-go"
22+
ag_format "github.com/gagliardetto/solana-go/text/format"
23+
ag_treeout "github.com/gagliardetto/treeout"
24+
)
25+
26+
// Convert an Amount of tokens to a UiAmount string, using the given mint.
27+
// In this version of the program, the mint can only specify the number of decimals.
28+
//
29+
// Return data can be fetched using sol_get_return_data and deserialized
30+
// with String::from_utf8.
31+
type AmountToUiAmount struct {
32+
// The amount of tokens to reformat.
33+
Amount *uint64
34+
35+
// [0] = [] mint
36+
// ··········· The mint to calculate for.
37+
ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"`
38+
}
39+
40+
func NewAmountToUiAmountInstructionBuilder() *AmountToUiAmount {
41+
nd := &AmountToUiAmount{
42+
AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 1),
43+
}
44+
return nd
45+
}
46+
47+
func (inst *AmountToUiAmount) SetAmount(amount uint64) *AmountToUiAmount {
48+
inst.Amount = &amount
49+
return inst
50+
}
51+
52+
func (inst *AmountToUiAmount) SetMintAccount(mint ag_solanago.PublicKey) *AmountToUiAmount {
53+
inst.AccountMetaSlice[0] = ag_solanago.Meta(mint)
54+
return inst
55+
}
56+
57+
func (inst *AmountToUiAmount) GetMintAccount() *ag_solanago.AccountMeta {
58+
return inst.AccountMetaSlice[0]
59+
}
60+
61+
func (inst AmountToUiAmount) Build() *Instruction {
62+
return &Instruction{BaseVariant: ag_binary.BaseVariant{
63+
Impl: inst,
64+
TypeID: ag_binary.TypeIDFromUint8(Instruction_AmountToUiAmount),
65+
}}
66+
}
67+
68+
func (inst AmountToUiAmount) ValidateAndBuild() (*Instruction, error) {
69+
if err := inst.Validate(); err != nil {
70+
return nil, err
71+
}
72+
return inst.Build(), nil
73+
}
74+
75+
func (inst *AmountToUiAmount) Validate() error {
76+
if inst.Amount == nil {
77+
return errors.New("Amount parameter is not set")
78+
}
79+
if inst.AccountMetaSlice[0] == nil {
80+
return errors.New("accounts.Mint is not set")
81+
}
82+
return nil
83+
}
84+
85+
func (inst *AmountToUiAmount) EncodeToTree(parent ag_treeout.Branches) {
86+
parent.Child(ag_format.Program(ProgramName, ProgramID)).
87+
ParentFunc(func(programBranch ag_treeout.Branches) {
88+
programBranch.Child(ag_format.Instruction("AmountToUiAmount")).
89+
ParentFunc(func(instructionBranch ag_treeout.Branches) {
90+
instructionBranch.Child("Params").ParentFunc(func(paramsBranch ag_treeout.Branches) {
91+
paramsBranch.Child(ag_format.Param("Amount", *inst.Amount))
92+
})
93+
instructionBranch.Child("Accounts").ParentFunc(func(accountsBranch ag_treeout.Branches) {
94+
accountsBranch.Child(ag_format.Meta("mint", inst.AccountMetaSlice[0]))
95+
})
96+
})
97+
})
98+
}
99+
100+
func (obj AmountToUiAmount) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) {
101+
err = encoder.Encode(obj.Amount)
102+
if err != nil {
103+
return err
104+
}
105+
return nil
106+
}
107+
108+
func (obj *AmountToUiAmount) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) {
109+
err = decoder.Decode(&obj.Amount)
110+
if err != nil {
111+
return err
112+
}
113+
return nil
114+
}
115+
116+
func NewAmountToUiAmountInstruction(
117+
amount uint64,
118+
mint ag_solanago.PublicKey,
119+
) *AmountToUiAmount {
120+
return NewAmountToUiAmountInstructionBuilder().
121+
SetAmount(amount).
122+
SetMintAccount(mint)
123+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright 2021 github.com/gagliardetto
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package token
16+
17+
import (
18+
"bytes"
19+
ag_gofuzz "github.com/gagliardetto/gofuzz"
20+
ag_require "github.com/stretchr/testify/require"
21+
"strconv"
22+
"testing"
23+
)
24+
25+
func TestEncodeDecode_AmountToUiAmount(t *testing.T) {
26+
fu := ag_gofuzz.New().NilChance(0)
27+
for i := 0; i < 1; i++ {
28+
t.Run("AmountToUiAmount"+strconv.Itoa(i), func(t *testing.T) {
29+
{
30+
params := new(AmountToUiAmount)
31+
fu.Fuzz(params)
32+
params.AccountMetaSlice = nil
33+
buf := new(bytes.Buffer)
34+
err := encodeT(*params, buf)
35+
ag_require.NoError(t, err)
36+
got := new(AmountToUiAmount)
37+
err = decodeT(got, buf.Bytes())
38+
got.AccountMetaSlice = nil
39+
ag_require.NoError(t, err)
40+
ag_require.Equal(t, params, got)
41+
}
42+
})
43+
}
44+
}

programs/token/Batch.go

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// Copyright 2021 github.com/gagliardetto
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package token
16+
17+
import (
18+
"bytes"
19+
"errors"
20+
"fmt"
21+
22+
ag_binary "github.com/gagliardetto/binary"
23+
ag_solanago "github.com/gagliardetto/solana-go"
24+
ag_format "github.com/gagliardetto/solana-go/text/format"
25+
ag_treeout "github.com/gagliardetto/treeout"
26+
)
27+
28+
// Batch allows executing multiple token instructions in a single CPI call,
29+
// reducing the overhead of multiple cross-program invocations.
30+
//
31+
// Each sub-instruction in the batch is prefixed with a 2-byte header:
32+
// - byte 0: number of accounts for this sub-instruction
33+
// - byte 1: length of instruction data for this sub-instruction
34+
//
35+
// This instruction is only available in the p-token (Pinocchio) implementation.
36+
type Batch struct {
37+
Instructions []*Instruction
38+
39+
ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"`
40+
}
41+
42+
func NewBatchInstructionBuilder() *Batch {
43+
return &Batch{
44+
AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 0),
45+
}
46+
}
47+
48+
func (inst *Batch) AddInstruction(ix *Instruction) *Batch {
49+
inst.Instructions = append(inst.Instructions, ix)
50+
return inst
51+
}
52+
53+
func (inst Batch) Build() *Instruction {
54+
accounts := make(ag_solanago.AccountMetaSlice, 0)
55+
for _, ix := range inst.Instructions {
56+
accounts = append(accounts, ix.Accounts()...)
57+
}
58+
inst.AccountMetaSlice = accounts
59+
60+
return &Instruction{BaseVariant: ag_binary.BaseVariant{
61+
Impl: inst,
62+
TypeID: ag_binary.TypeIDFromUint8(Instruction_Batch),
63+
}}
64+
}
65+
66+
func (inst Batch) ValidateAndBuild() (*Instruction, error) {
67+
if err := inst.Validate(); err != nil {
68+
return nil, err
69+
}
70+
return inst.Build(), nil
71+
}
72+
73+
func (inst *Batch) Validate() error {
74+
if len(inst.Instructions) == 0 {
75+
return errors.New("batch must contain at least one instruction")
76+
}
77+
return nil
78+
}
79+
80+
func (inst *Batch) EncodeToTree(parent ag_treeout.Branches) {
81+
parent.Child(ag_format.Program(ProgramName, ProgramID)).
82+
ParentFunc(func(programBranch ag_treeout.Branches) {
83+
programBranch.Child(ag_format.Instruction("Batch")).
84+
ParentFunc(func(instructionBranch ag_treeout.Branches) {
85+
instructionBranch.Child("Params").ParentFunc(func(paramsBranch ag_treeout.Branches) {
86+
paramsBranch.Child(ag_format.Param("InstructionCount", len(inst.Instructions)))
87+
})
88+
instructionBranch.Child("Accounts").ParentFunc(func(accountsBranch ag_treeout.Branches) {
89+
for i, acc := range inst.AccountMetaSlice {
90+
accountsBranch.Child(ag_format.Meta(fmt.Sprintf("[%v]", i), acc))
91+
}
92+
})
93+
})
94+
})
95+
}
96+
97+
func (obj Batch) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) {
98+
for _, ix := range obj.Instructions {
99+
accountCount := uint8(len(ix.Accounts()))
100+
101+
data, err := ix.Data()
102+
if err != nil {
103+
return fmt.Errorf("unable to encode batch sub-instruction: %w", err)
104+
}
105+
// data includes the discriminator byte from the outer Instruction encoding,
106+
// but for batch sub-instructions we need the raw inner data (discriminator + params).
107+
// The ix.Data() already produces [discriminator | params], which is what we need.
108+
dataLen := uint8(len(data))
109+
110+
if err = encoder.WriteUint8(accountCount); err != nil {
111+
return err
112+
}
113+
if err = encoder.WriteUint8(dataLen); err != nil {
114+
return err
115+
}
116+
if _, err = encoder.Write(data); err != nil {
117+
return err
118+
}
119+
}
120+
return nil
121+
}
122+
123+
func (obj *Batch) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) {
124+
for decoder.HasRemaining() {
125+
accountCount, err := decoder.ReadUint8()
126+
if err != nil {
127+
return err
128+
}
129+
dataLen, err := decoder.ReadUint8()
130+
if err != nil {
131+
return err
132+
}
133+
_ = accountCount
134+
135+
data, err := decoder.ReadNBytes(int(dataLen))
136+
if err != nil {
137+
return err
138+
}
139+
ix := new(Instruction)
140+
if err = ag_binary.NewBinDecoder(data).Decode(ix); err != nil {
141+
return fmt.Errorf("unable to decode batch sub-instruction: %w", err)
142+
}
143+
obj.Instructions = append(obj.Instructions, ix)
144+
}
145+
return nil
146+
}
147+
148+
// BuildBatchData constructs the complete instruction data for a batch,
149+
// including the batch discriminator (255) and all sub-instruction data.
150+
func BuildBatchData(instructions []*Instruction) ([]byte, error) {
151+
buf := new(bytes.Buffer)
152+
buf.WriteByte(Instruction_Batch)
153+
for _, ix := range instructions {
154+
accountCount := uint8(len(ix.Accounts()))
155+
data, err := ix.Data()
156+
if err != nil {
157+
return nil, fmt.Errorf("unable to encode batch sub-instruction: %w", err)
158+
}
159+
dataLen := uint8(len(data))
160+
buf.WriteByte(accountCount)
161+
buf.WriteByte(dataLen)
162+
buf.Write(data)
163+
}
164+
return buf.Bytes(), nil
165+
}
166+
167+
func NewBatchInstruction(instructions ...*Instruction) *Batch {
168+
b := NewBatchInstructionBuilder()
169+
for _, ix := range instructions {
170+
b.AddInstruction(ix)
171+
}
172+
return b
173+
}

programs/token/Batch_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright 2021 github.com/gagliardetto
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package token
16+
17+
import (
18+
ag_require "github.com/stretchr/testify/require"
19+
"testing"
20+
)
21+
22+
func TestEncodeDecode_Batch(t *testing.T) {
23+
t.Run("Batch_InstructionIDToName", func(t *testing.T) {
24+
ag_require.Equal(t, "Batch", InstructionIDToName(Instruction_Batch))
25+
ag_require.Equal(t, "WithdrawExcessLamports", InstructionIDToName(Instruction_WithdrawExcessLamports))
26+
ag_require.Equal(t, "UnwrapLamports", InstructionIDToName(Instruction_UnwrapLamports))
27+
})
28+
29+
t.Run("Batch_InstructionIDs", func(t *testing.T) {
30+
ag_require.Equal(t, uint8(21), Instruction_GetAccountDataSize)
31+
ag_require.Equal(t, uint8(22), Instruction_InitializeImmutableOwner)
32+
ag_require.Equal(t, uint8(23), Instruction_AmountToUiAmount)
33+
ag_require.Equal(t, uint8(24), Instruction_UiAmountToAmount)
34+
ag_require.Equal(t, uint8(38), Instruction_WithdrawExcessLamports)
35+
ag_require.Equal(t, uint8(45), Instruction_UnwrapLamports)
36+
ag_require.Equal(t, uint8(255), Instruction_Batch)
37+
})
38+
}

0 commit comments

Comments
 (0)