Skip to content

Commit abce4af

Browse files
committed
Support versioned transactions
1 parent fcdb240 commit abce4af

File tree

5 files changed

+228
-19
lines changed

5 files changed

+228
-19
lines changed

pkg/jupiter/client.go

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ func (q *Quote) GetEstimatedSwapAmount() uint64 {
5151
return q.estimatedSwapAmount
5252
}
5353

54+
func (q *Quote) String() string {
55+
return q.jsonString
56+
}
57+
5458
// GetQuote gets an optimal route for performing a swap
5559
func (c *Client) GetQuote(
5660
ctx context.Context,
@@ -117,6 +121,7 @@ type SwapInstructions struct {
117121
SetupInstructions []solana.Instruction
118122
SwapInstruction solana.Instruction
119123
CleanupInstruction *solana.Instruction
124+
AddressLookupTables []string
120125
}
121126

122127
// GetSwapInstructions gets the instructions to construct a transaction to sign
@@ -130,10 +135,6 @@ func (c *Client) GetSwapInstructions(
130135
tracer := metrics.TraceMethodCall(ctx, metricsStructName, "GetSwapInstructions")
131136
defer tracer.End()
132137

133-
if !quote.useLegacyInstructions {
134-
return nil, errors.New("only legacy transactions are supported")
135-
}
136-
137138
// todo: struct this
138139
reqBody := fmt.Sprintf(
139140
`{"quoteResponse": %s, "userPublicKey": "%s", "destinationTokenAccount": "%s", "prioritizationFeeLamports": "auto", "asLegacyTransaction": %v}`,
@@ -206,6 +207,8 @@ func (c *Client) GetSwapInstructions(
206207
}
207208
}
208209

210+
res.AddressLookupTables = jsonBody.AddressLookupTableAddresses
211+
209212
return &res, nil
210213
}
211214

@@ -258,9 +261,10 @@ type jsonInstruction struct {
258261
}
259262

260263
type jsonSwapInstructions struct {
261-
TokenLedgerInstruction *jsonInstruction `json:"tokenLedgerInstruction"`
262-
ComputeBudgetInstructions []*jsonInstruction `json:"computeBudgetInstructions"`
263-
SetupInstructions []*jsonInstruction `json:"setupInstructions"`
264-
SwapInstruction *jsonInstruction `json:"swapInstruction"`
265-
CleanupInstruction *jsonInstruction `json:"cleanupInstruction"`
264+
TokenLedgerInstruction *jsonInstruction `json:"tokenLedgerInstruction"`
265+
ComputeBudgetInstructions []*jsonInstruction `json:"computeBudgetInstructions"`
266+
SetupInstructions []*jsonInstruction `json:"setupInstructions"`
267+
SwapInstruction *jsonInstruction `json:"swapInstruction"`
268+
CleanupInstruction *jsonInstruction `json:"cleanupInstruction"`
269+
AddressLookupTableAddresses []string `json:"addressLookupTableAddresses"`
266270
}

pkg/solana/address_lookup_table.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package solana
2+
3+
import (
4+
"bytes"
5+
"crypto/ed25519"
6+
)
7+
8+
type AddressLookupTable struct {
9+
PublicKey ed25519.PublicKey
10+
Addresses []ed25519.PublicKey
11+
}
12+
13+
type SortableAddressLookupTables []AddressLookupTable
14+
15+
func (s SortableAddressLookupTables) Len() int {
16+
return len(s)
17+
}
18+
19+
func (s SortableAddressLookupTables) Less(i int, j int) bool {
20+
return bytes.Compare(s[i].PublicKey, s[j].PublicKey) < 0
21+
}
22+
23+
func (s SortableAddressLookupTables) Swap(i int, j int) {
24+
s[i], s[j] = s[j], s[i]
25+
}

pkg/solana/encoding.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,17 @@ func (t *Transaction) Unmarshal(b []byte) error {
4949
}
5050

5151
func (m Message) Marshal() []byte {
52+
switch m.version {
53+
case MessageVersionLegacy:
54+
return m.marshalLegacy()
55+
case MessageVersion0:
56+
return m.marshalV0()
57+
default:
58+
panic("unsupported message version")
59+
}
60+
}
61+
62+
func (m Message) marshalLegacy() []byte {
5263
b := bytes.NewBuffer(nil)
5364

5465
// Header
@@ -82,7 +93,60 @@ func (m Message) Marshal() []byte {
8293
return b.Bytes()
8394
}
8495

96+
func (m Message) marshalV0() []byte {
97+
b := bytes.NewBuffer(nil)
98+
99+
// Version Number
100+
_ = b.WriteByte(byte(m.version + 127))
101+
102+
// Header
103+
_ = b.WriteByte(m.Header.NumSignatures)
104+
_ = b.WriteByte(m.Header.NumReadonlySigned)
105+
_ = b.WriteByte(m.Header.NumReadOnly)
106+
107+
// Accounts
108+
_, _ = shortvec.EncodeLen(b, len(m.Accounts))
109+
for _, a := range m.Accounts {
110+
_, _ = b.Write(a)
111+
}
112+
113+
// Recent Blockhash
114+
_, _ = b.Write(m.RecentBlockhash[:])
115+
116+
// Instructions
117+
_, _ = shortvec.EncodeLen(b, len(m.Instructions))
118+
for _, i := range m.Instructions {
119+
_ = b.WriteByte(i.ProgramIndex)
120+
121+
// Accounts
122+
_, _ = shortvec.EncodeLen(b, len(i.Accounts))
123+
_, _ = b.Write(i.Accounts)
124+
125+
// Data
126+
_, _ = shortvec.EncodeLen(b, len(i.Data))
127+
_, _ = b.Write(i.Data)
128+
}
129+
130+
_, _ = shortvec.EncodeLen(b, len(m.AddressTableLookups))
131+
for _, addressTableLookup := range m.AddressTableLookups {
132+
_, _ = b.Write(addressTableLookup.PublicKey)
133+
134+
_, _ = shortvec.EncodeLen(b, len(addressTableLookup.WritableIndexes))
135+
_, _ = b.Write(addressTableLookup.WritableIndexes)
136+
137+
_, _ = shortvec.EncodeLen(b, len(addressTableLookup.ReadonlyIndexes))
138+
_, _ = b.Write(addressTableLookup.ReadonlyIndexes)
139+
}
140+
141+
return b.Bytes()
142+
}
143+
85144
func (m *Message) Unmarshal(b []byte) (err error) {
145+
// todo: double check this is correct
146+
if b[0] > 127 {
147+
return errors.New("versioned messages not supported")
148+
}
149+
86150
buf := bytes.NewBuffer(b)
87151

88152
// Header

pkg/solana/instruction.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@ func (s SortableAccountMeta) Less(i int, j int) bool {
5858
if s[i].isPayer != s[j].isPayer {
5959
return s[i].isPayer
6060
}
61-
if s[i].isProgram != s[j].isProgram {
61+
/*if s[i].isProgram != s[j].isProgram {
6262
return !s[i].isProgram
63-
}
63+
}*/
6464

6565
if s[i].IsSigner != s[j].IsSigner {
6666
return s[i].IsSigner

pkg/solana/transaction.go

Lines changed: 124 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,32 @@ const (
2020
type Signature [ed25519.SignatureSize]byte
2121
type Blockhash [sha256.Size]byte
2222

23+
type MessageVersion uint8
24+
25+
const (
26+
MessageVersionLegacy MessageVersion = iota
27+
MessageVersion0
28+
)
29+
2330
type Header struct {
2431
NumSignatures byte
2532
NumReadonlySigned byte
2633
NumReadOnly byte
2734
}
2835

36+
type MessageAddressTableLookup struct {
37+
PublicKey ed25519.PublicKey
38+
WritableIndexes []byte
39+
ReadonlyIndexes []byte
40+
}
41+
2942
type Message struct {
30-
Header Header
31-
Accounts []ed25519.PublicKey
32-
RecentBlockhash Blockhash
33-
Instructions []CompiledInstruction
43+
version MessageVersion
44+
Header Header
45+
Accounts []ed25519.PublicKey
46+
RecentBlockhash Blockhash
47+
Instructions []CompiledInstruction
48+
AddressTableLookups []MessageAddressTableLookup
3449
}
3550

3651
type Transaction struct {
@@ -39,6 +54,15 @@ type Transaction struct {
3954
}
4055

4156
func NewTransaction(payer ed25519.PublicKey, instructions ...Instruction) Transaction {
57+
return newTransaction(payer, nil, instructions)
58+
}
59+
60+
func NewVersionedTransaction(payer ed25519.PublicKey, addressLookupTables []AddressLookupTable, instructions []Instruction) Transaction {
61+
return newTransaction(payer, addressLookupTables, instructions)
62+
}
63+
64+
// todo: consolidate to new constructor
65+
func newTransaction(payer ed25519.PublicKey, addressLookupTables []AddressLookupTable, instructions []Instruction) Transaction {
4266
accounts := []AccountMeta{
4367
{
4468
PublicKey: payer,
@@ -65,8 +89,45 @@ func NewTransaction(payer ed25519.PublicKey, instructions ...Instruction) Transa
6589
accounts = filterUnique(accounts)
6690
sort.Sort(SortableAccountMeta(accounts))
6791

92+
// Sort address tables to guarantee consistent marshalling
93+
sortedAddressLookupTables := make([]AddressLookupTable, len(addressLookupTables))
94+
copy(sortedAddressLookupTables, addressLookupTables)
95+
sort.Sort(SortableAddressLookupTables(sortedAddressLookupTables))
96+
97+
writableAddressTableIndexes := make([][]byte, len(sortedAddressLookupTables))
98+
readonlyAddressTableIndexes := make([][]byte, len(sortedAddressLookupTables))
99+
68100
var m Message
69101
for _, account := range accounts {
102+
// If the account is eligible for dynamic loading, then pull its index
103+
// from the first address table where it's defined.
104+
var isDynamicallyLoaded bool
105+
if !account.isPayer && !account.IsSigner && !account.isProgram {
106+
for i, addressLookupTable := range sortedAddressLookupTables {
107+
for j, address := range addressLookupTable.Addresses {
108+
if bytes.Equal(address, account.PublicKey) {
109+
isDynamicallyLoaded = true
110+
111+
if account.IsWritable {
112+
writableAddressTableIndexes[i] = append(writableAddressTableIndexes[i], byte(j))
113+
} else {
114+
readonlyAddressTableIndexes[i] = append(readonlyAddressTableIndexes[i], byte(j))
115+
}
116+
117+
break
118+
}
119+
}
120+
121+
if isDynamicallyLoaded {
122+
break
123+
}
124+
}
125+
}
126+
if isDynamicallyLoaded {
127+
continue
128+
}
129+
130+
// Otherwise, the account is defined statically
70131
m.Accounts = append(m.Accounts, account.PublicKey)
71132

72133
if account.IsSigner {
@@ -80,21 +141,58 @@ func NewTransaction(payer ed25519.PublicKey, instructions ...Instruction) Transa
80141
}
81142
}
82143

144+
// Consolidate static and dynamically loaded accounts into an ordered list,
145+
// which is used for index references encoded in the message
146+
dynamicWritableAccounts := make([]ed25519.PublicKey, 0)
147+
dynamicReadonlyAccount := make([]ed25519.PublicKey, 0)
148+
for i, writableAddressTableIndexes := range writableAddressTableIndexes {
149+
for _, index := range writableAddressTableIndexes {
150+
writableAccount := sortedAddressLookupTables[i].Addresses[index]
151+
dynamicWritableAccounts = append(dynamicWritableAccounts, writableAccount)
152+
}
153+
}
154+
for i, readonlyAddressTableIndexes := range readonlyAddressTableIndexes {
155+
for _, index := range readonlyAddressTableIndexes {
156+
readonlyAccount := sortedAddressLookupTables[i].Addresses[index]
157+
dynamicReadonlyAccount = append(dynamicReadonlyAccount, readonlyAccount)
158+
}
159+
}
160+
var allAccounts []ed25519.PublicKey
161+
allAccounts = append(allAccounts, m.Accounts...)
162+
allAccounts = append(allAccounts, dynamicWritableAccounts...)
163+
allAccounts = append(allAccounts, dynamicReadonlyAccount...)
164+
83165
// Generate the compiled instruction, which uses indices instead
84166
// of raw account keys.
85167
for _, i := range instructions {
86168
c := CompiledInstruction{
87-
ProgramIndex: byte(indexOf(m.Accounts, i.Program)),
169+
ProgramIndex: byte(indexOf(allAccounts, i.Program)),
88170
Data: i.Data,
89171
}
90172

91173
for _, a := range i.Accounts {
92-
c.Accounts = append(c.Accounts, byte(indexOf(m.Accounts, a.PublicKey)))
174+
c.Accounts = append(c.Accounts, byte(indexOf(allAccounts, a.PublicKey)))
93175
}
94176

95177
m.Instructions = append(m.Instructions, c)
96178
}
97179

180+
// Generate the compiled message address table lookups
181+
for i, addressLookupTable := range sortedAddressLookupTables {
182+
if len(writableAddressTableIndexes[i]) == 0 && len(readonlyAddressTableIndexes[i]) == 0 {
183+
continue
184+
}
185+
186+
m.AddressTableLookups = append(m.AddressTableLookups, MessageAddressTableLookup{
187+
PublicKey: addressLookupTable.PublicKey,
188+
WritableIndexes: writableAddressTableIndexes[i],
189+
ReadonlyIndexes: readonlyAddressTableIndexes[i],
190+
})
191+
}
192+
if len(m.AddressTableLookups) > 0 {
193+
m.version = MessageVersion0
194+
}
195+
98196
for i := range m.Accounts {
99197
if len(m.Accounts[i]) == 0 {
100198
m.Accounts[i] = make([]byte, ed25519.PublicKeySize)
@@ -118,11 +216,12 @@ func (t *Transaction) String() string {
118216
sb.WriteString(fmt.Sprintf(" %d: %s\n", i, base58.Encode(s[:])))
119217
}
120218
sb.WriteString("Message:\n")
219+
sb.WriteString(fmt.Sprintf(" Version: %s\n", t.Message.version.String()))
121220
sb.WriteString(" Header:\n")
122221
sb.WriteString(fmt.Sprintf(" NumSignatures: %d\n", t.Message.Header.NumSignatures))
123222
sb.WriteString(fmt.Sprintf(" NumReadOnly: %d\n", t.Message.Header.NumReadOnly))
124223
sb.WriteString(fmt.Sprintf(" NumReadOnlySigned: %d\n", t.Message.Header.NumReadonlySigned))
125-
sb.WriteString(" Accounts:\n")
224+
sb.WriteString(" Static Accounts:\n")
126225
for i, a := range t.Message.Accounts {
127226
sb.WriteString(fmt.Sprintf(" %d: %s\n", i, base58.Encode(a)))
128227
}
@@ -133,7 +232,14 @@ func (t *Transaction) String() string {
133232
sb.WriteString(fmt.Sprintf(" Accounts: %v\n", t.Message.Instructions[i].Accounts))
134233
sb.WriteString(fmt.Sprintf(" Data: %v\n", t.Message.Instructions[i].Data))
135234
}
136-
235+
if len(t.Message.AddressTableLookups) > 0 {
236+
sb.WriteString(" Address Table Lookups:\n")
237+
for i := range t.Message.AddressTableLookups {
238+
sb.WriteString(fmt.Sprintf(" %s:\n", base58.Encode(t.Message.AddressTableLookups[i].PublicKey)))
239+
sb.WriteString(fmt.Sprintf(" Writable Indexes: %v\n", t.Message.AddressTableLookups[i].WritableIndexes))
240+
sb.WriteString(fmt.Sprintf(" Readonly Indexes: %v\n", t.Message.AddressTableLookups[i].ReadonlyIndexes))
241+
}
242+
}
137243
return sb.String()
138244
}
139245

@@ -198,3 +304,13 @@ func indexOf(slice []ed25519.PublicKey, item ed25519.PublicKey) int {
198304

199305
return -1
200306
}
307+
308+
func (v MessageVersion) String() string {
309+
switch v {
310+
case MessageVersionLegacy:
311+
return "legacy"
312+
case MessageVersion0:
313+
return "v0"
314+
}
315+
return "unknown"
316+
}

0 commit comments

Comments
 (0)