Skip to content

Commit

Permalink
Add anchor sighash functions
Browse files Browse the repository at this point in the history
  • Loading branch information
gagliardetto committed Oct 2, 2022
1 parent f85e20f commit 50750dd
Show file tree
Hide file tree
Showing 3 changed files with 336 additions and 9 deletions.
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ require github.com/tidwall/gjson v1.9.3

require (
github.com/AlekSi/pointer v1.1.0
github.com/davecgh/go-spew v1.1.1
github.com/dfuse-io/logging v0.0.0-20201110202154-26697de88c79
github.com/kr/pretty v0.2.1 // indirect
github.com/shopspring/decimal v1.3.1
Expand Down
200 changes: 199 additions & 1 deletion sighash.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,29 @@ package bin

import (
"crypto/sha256"
"strings"
"unicode"
)

// Sighash creates an anchor sighash for the provided namespace and element.
// An anchor sighash is the first 8 bytes of the sha256 of {namespace}:{name}
// NOTE: you must first convert the name to snake case using `ToSnakeForSighash`.
func Sighash(namespace string, name string) []byte {
data := namespace + ":" + name
sum := sha256.Sum256([]byte(data))
return sum[0:8]
}

func SighashInstruction(name string) []byte {
return Sighash(SIGHASH_GLOBAL_NAMESPACE, ToSnakeForSighash(name))
}

func SighashAccount(name string) []byte {
return Sighash(SIGHASH_ACCOUNT_NAMESPACE, ToSnakeForSighash(name))
}

func SighashTypeID(namespace string, name string) TypeID {
return TypeIDFromBytes(Sighash(namespace, name))
return TypeIDFromBytes(Sighash(namespace, ToSnakeForSighash(name)))
}

// Namespace for calculating state instruction sighash signatures.
Expand All @@ -47,3 +58,190 @@ const ACCOUNT_DISCRIMINATOR_SIZE = 8

// TODO:
// https://github.com/project-serum/anchor/blob/84a2b8200cc3c7cb51d7127918e6cbbd836f0e99/ts/src/error.ts#L48

func ToSnakeForSighash(s string) string {
return ToRustSnakeCase(s)
}

type reader struct {
runes []rune
index int
}

func splitStringByRune(s string) []rune {
var res []rune
iterateStringAsRunes(s, func(r rune) bool {
res = append(res, r)
return true
})
return res
}

func iterateStringAsRunes(s string, callback func(r rune) bool) {
for _, rn := range s {
//fmt.Printf("%d: %c\n", i, rn)
doContinue := callback(rn)
if !doContinue {
return
}
}
}

func newReader(s string) *reader {
return &reader{
runes: splitStringByRune(s),
index: -1,
}
}

func (r reader) This() (int, rune) {
return r.index, r.runes[r.index]
}

func (r reader) HasNext() bool {
return r.index < len(r.runes)-1
}

func (r reader) Peek() (int, rune) {
if r.HasNext() {
return r.index + 1, r.runes[r.index+1]
}
return -1, rune(0)
}

func (r *reader) Move() bool {
if r.HasNext() {
r.index++
return true
}
return false
}

// #[cfg(feature = "unicode")]
//
// fn get_iterator(s: &str) -> unicode_segmentation::UnicodeWords {
// use unicode_segmentation::UnicodeSegmentation;
// s.unicode_words()
// }
func splitByUnicode(s string) []string {
parts := strings.FieldsFunc(s, func(r rune) bool {
// TODO: see https://unicode.org/reports/tr29/#Word_Boundaries
return !(unicode.IsLetter(r) || unicode.IsDigit(r)) || unicode.Is(unicode.Extender, r)
})
return parts
}

// #[cfg(not(feature = "unicode"))]
func splitIntoWords(s string) []string {
parts := strings.FieldsFunc(s, func(r rune) bool {
return !(unicode.IsLetter(r) || unicode.IsDigit(r))
})
return parts
}

type _WordMode int

const (
/// There have been no lowercase or uppercase characters in the current
/// word.
_Boundary _WordMode = iota
/// The previous cased character in the current word is lowercase.
_Lowercase
/// The previous cased character in the current word is uppercase.
_Uppercase
)

// ToRustSnakeCase converts the given string to a snake_case string.
// Ported from https://github.com/withoutboats/heck/blob/c501fc95db91ce20eaef248a511caec7142208b4/src/lib.rs#L75
func ToRustSnakeCase(s string) string {

builder := new(strings.Builder)

first_word := true
words := splitIntoWords(s)
for _, word := range words {
char_indices := newReader(word)
init := 0
mode := _Boundary

for char_indices.Move() {
i, c := char_indices.This()

// Skip underscore characters
if c == '_' {
if init == i {
init += 1
}
continue
}

if next_i, next := char_indices.Peek(); next_i != -1 {

// The mode including the current character, assuming the
// current character does not result in a word boundary.
next_mode := func() _WordMode {
if unicode.IsLower(c) {
return _Lowercase
} else if unicode.IsUpper(c) {
return _Uppercase
} else {
return mode
}
}()

// Word boundary after if next is underscore or current is
// not uppercase and next is uppercase
if next == '_' || (next_mode == _Lowercase && unicode.IsUpper(next)) {
if !first_word {
// boundary(f)?;
builder.WriteRune('_')
}
{
// with_word(&word[init..next_i], f)?;
builder.WriteString(strings.ToLower(word[init:next_i]))
}

first_word = false
init = next_i
mode = _Boundary

// Otherwise if current and previous are uppercase and next
// is lowercase, word boundary before
} else if mode == _Uppercase && unicode.IsUpper(c) && unicode.IsLower(next) {
if !first_word {
// boundary(f)?;
builder.WriteRune('_')
} else {
first_word = false
}
{
// with_word(&word[init..i], f)?;
builder.WriteString(strings.ToLower(word[init:i]))
}
init = i
mode = _Boundary

// Otherwise no word boundary, just update the mode
} else {
mode = next_mode
}

} else {
// Collect trailing characters as a word
if !first_word {
// boundary(f)?;
builder.WriteRune('_')
} else {
first_word = false
}
{
// with_word(&word[init..], f)?;
builder.WriteString(strings.ToLower(word[init:]))
}
break
}
}
}

return builder.String()
}
144 changes: 137 additions & 7 deletions sighash_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,148 @@ package bin
import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestSighash(t *testing.T) {
value := "hello"
got := Sighash(SIGHASH_GLOBAL_NAMESPACE, value)
{
name := "hello"
got := Sighash(SIGHASH_GLOBAL_NAMESPACE, ToSnakeForSighash(name))
require.NotEmpty(t, got)

require.NotEmpty(t, got)
expected := []byte{149, 118, 59, 220, 196, 127, 161, 179}
require.Equal(t,
expected,
got,
)
}
{
name := "serumSwap"
got := Sighash(SIGHASH_GLOBAL_NAMESPACE, ToSnakeForSighash(name))
require.NotEmpty(t, got)

expected := []byte{149, 118, 59, 220, 196, 127, 161, 179}
require.Equal(t,
expected,
got,
expected := []byte{88, 183, 70, 249, 214, 118, 82, 210}
require.Equal(t,
expected,
got,
)
require.Equal(t,
expected,
SighashInstruction(name),
)
}
{
name := "aldrinV2Swap"
got := Sighash(SIGHASH_GLOBAL_NAMESPACE, ToSnakeForSighash(name))
require.NotEmpty(t, got)

expected := []byte{190, 166, 89, 139, 33, 152, 16, 10}
require.Equal(t,
expected,
got,
)
require.Equal(t,
expected,
SighashInstruction(name),
)
}
{
name := "raydiumSwapV2"
got := Sighash(SIGHASH_GLOBAL_NAMESPACE, ToSnakeForSighash(name))
require.NotEmpty(t, got)

expected := []byte{69, 227, 98, 93, 237, 202, 223, 140}
require.Equal(t,
expected,
got,
)
require.Equal(t,
expected,
SighashInstruction(name),
)
}
}

func TestToSnakeForSighash(t *testing.T) {
t.Run(
"typescript",
// "typescript package: https://www.npmjs.com/package/snake-case",
func(t *testing.T) {
// copied from https://github.com/blakeembrey/change-case/blob/040a079f007879cb0472ba4f7cc2e1d3185e90ba/packages/snake-case/src/index.spec.ts
// as used in anchor.
testCases := [][2]string{
{"", ""},
{"_id", "id"},
{"test", "test"},
{"test string", "test_string"},
{"Test String", "test_string"},
{"TestV2", "test_v2"},
{"version 1.2.10", "version_1_2_10"},
{"version 1.21.0", "version_1_21_0"},
{"doSomething2", "do_something2"},
}

for _, testCase := range testCases {
t.Run(
testCase[0],
func(t *testing.T) {
assert.Equal(t,
testCase[1],
ToSnakeForSighash(testCase[0]),
"from %q", testCase[0],
)
})
}
},
)
t.Run(
"rust",
// "rust package: https://docs.rs/heck",
func(t *testing.T) {
// copied from https://github.com/withoutboats/heck/blob/dbcfc7b8db8e532d1fad44518cf73e88d5212161/src/snake.rs#L60
// as used in anchor.
testCases := [][2]string{
{"CamelCase", "camel_case"},
{"This is Human case.", "this_is_human_case"},
{"MixedUP CamelCase, with some Spaces", "mixed_up_camel_case_with_some_spaces"},
{"mixed_up_ snake_case with some _spaces", "mixed_up_snake_case_with_some_spaces"},
{"kebab-case", "kebab_case"},
{"SHOUTY_SNAKE_CASE", "shouty_snake_case"},
{"snake_case", "snake_case"},
{"this-contains_ ALLKinds OfWord_Boundaries", "this_contains_all_kinds_of_word_boundaries"},

// #[cfg(feature = "unicode")]
{"XΣXΣ baffle", "xσxσ_baffle"},
{"XMLHttpRequest", "xml_http_request"},
{"FIELD_NAME11", "field_name11"},
{"99BOTTLES", "99bottles"},
{"FieldNamE11", "field_nam_e11"},

{"abc123def456", "abc123def456"},
{"abc123DEF456", "abc123_def456"},
{"abc123Def456", "abc123_def456"},
{"abc123DEf456", "abc123_d_ef456"},
{"ABC123def456", "abc123def456"},
{"ABC123DEF456", "abc123def456"},
{"ABC123Def456", "abc123_def456"},
{"ABC123DEf456", "abc123d_ef456"},
{"ABC123dEEf456FOO", "abc123d_e_ef456_foo"},
{"abcDEF", "abc_def"},
{"ABcDE", "a_bc_de"},
}

for _, testCase := range testCases {
t.Run(
testCase[0],
func(t *testing.T) {
assert.Equal(t,
testCase[1],
ToSnakeForSighash(testCase[0]),
"from %q", testCase[0],
)
})
}
})

}

0 comments on commit 50750dd

Please sign in to comment.