From 09b0ea70beb56cf68c4e6cae5a9cb65985201cbd Mon Sep 17 00:00:00 2001 From: Nate Date: Thu, 9 Jan 2025 11:54:16 -0800 Subject: [PATCH] feat(sdk): add support for signing v2 transactions --- sdk/encode.go | 21 +++++++------ sdk/go.mod | 19 +++++------ sdk/go.sum | 39 ++++++++++++----------- sdk/main.go | 2 ++ sdk/rhp.go | 34 ++++++++++---------- sdk/wallet.go | 87 +++++++++++++++++++++++++++++++++++++++++++++------ 6 files changed, 138 insertions(+), 64 deletions(-) diff --git a/sdk/encode.go b/sdk/encode.go index 48c107698..667f2efc1 100644 --- a/sdk/encode.go +++ b/sdk/encode.go @@ -4,28 +4,29 @@ import ( "bytes" "syscall/js" - "go.sia.tech/core/rhp/v4" + proto4 "go.sia.tech/core/rhp/v4" + "go.sia.tech/core/types" ) -func encodeRPCRequest(data js.Value, req rhp.Request) result { +func encodeRPCRequest(rpcID types.Specifier, data js.Value, req proto4.Object) result { if data.Type() != js.TypeUndefined { if err := unmarshalStruct(data, &req); err != nil { return resultErr(err) } } buf := bytes.NewBuffer(nil) - if err := rhp.WriteRequest(buf, req); err != nil { + if err := proto4.WriteRequest(buf, rpcID, req); err != nil { return resultErr(err) } return resultRPC(marshalUint8Array(buf.Bytes())) } -func decodeRPCRequest(rpcJsData js.Value, res rhp.Request) result { +func decodeRPCRequest(rpcJsData js.Value, res proto4.Object) result { rpcData, err := unmarshalUint8Array(rpcJsData) if err != nil { return resultErr(err) } - err = rhp.ReadRequest(bytes.NewReader(rpcData), res) + err = proto4.ReadRequest(bytes.NewReader(rpcData), res) if err != nil { return resultErr(err) } @@ -36,25 +37,25 @@ func decodeRPCRequest(rpcJsData js.Value, res rhp.Request) result { return resultData(d) } -func encodeRPCResponse(data js.Value, req rhp.Object) result { +func encodeRPCResponse(data js.Value, obj proto4.Object) result { if data.Type() != js.TypeUndefined { - if err := unmarshalStruct(data, &req); err != nil { + if err := unmarshalStruct(data, &obj); err != nil { return resultErr(err) } } buf := bytes.NewBuffer(nil) - if err := rhp.WriteResponse(buf, req); err != nil { + if err := proto4.WriteResponse(buf, obj); err != nil { return resultErr(err) } return resultRPC(marshalUint8Array(buf.Bytes())) } -func decodeRPCResponse(rpcJsData js.Value, res rhp.Object) result { +func decodeRPCResponse(rpcJsData js.Value, res proto4.Object) result { rpcData, err := unmarshalUint8Array(rpcJsData) if err != nil { return resultErr(err) } - err = rhp.ReadResponse(bytes.NewReader(rpcData), res) + err = proto4.ReadResponse(bytes.NewReader(rpcData), res) if err != nil { return resultErr(err) } diff --git a/sdk/go.mod b/sdk/go.mod index 79da1d255..7d645de22 100644 --- a/sdk/go.mod +++ b/sdk/go.mod @@ -1,17 +1,18 @@ module go.sia.tech/web/sdk -go 1.23.0 +go 1.23.1 + +toolchain go1.23.3 require ( - go.sia.tech/core v0.2.2-0.20240229154321-d97c1d5b2172 - go.sia.tech/coreutils v0.0.3 + go.sia.tech/core v0.9.1-0.20250106163018-8ec4de35d644 + go.sia.tech/coreutils v0.9.0 ) require ( - github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect - go.uber.org/multierr v1.10.0 // indirect - go.uber.org/zap v1.26.0 // indirect - golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 // indirect - golang.org/x/sys v0.5.0 // indirect - lukechampine.com/frand v1.4.2 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/sys v0.29.0 // indirect + lukechampine.com/frand v1.5.1 // indirect ) diff --git a/sdk/go.sum b/sdk/go.sum index 8feb98ce7..0d8e2f8b2 100644 --- a/sdk/go.sum +++ b/sdk/go.sum @@ -1,27 +1,28 @@ -github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= -github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -go.sia.tech/core v0.2.2-0.20240229154321-d97c1d5b2172 h1:uET7VyK5mz02bsicyNeoEut/RarvmQXAvXgUqZ1YJoE= -go.sia.tech/core v0.2.2-0.20240229154321-d97c1d5b2172/go.mod h1:3EoY+rR78w1/uGoXXVqcYdwSjSJKuEMI5bL7WROA27Q= -go.sia.tech/coreutils v0.0.3 h1:ZxuzovRpQMvfy/pCOV4om1cPF6sE15GyJyK36kIrF1Y= -go.sia.tech/coreutils v0.0.3/go.mod h1:UBFc77wXiE//eyilO5HLOncIEj7F69j0Nv2OkFujtP0= -go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= -go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= -go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= -go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= -go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= -golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 h1:NvGWuYG8dkDHFSKksI1P9faiVJ9rayE6l0+ouWVIDs8= -golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= +go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= +go.sia.tech/core v0.9.1-0.20250106163018-8ec4de35d644 h1:UXDMLhwPZzJJFGqG0pzACaOex9LZs432TcsuoIp6wpU= +go.sia.tech/core v0.9.1-0.20250106163018-8ec4de35d644/go.mod h1:BV9XKOoP8Ux2ctiyoDExvdWc21rxSJAuFQ0Yr5Baiqk= +go.sia.tech/coreutils v0.9.0 h1:5cnK0RtHOyErGhcmNkmCdEKeuj1tECwO9PYbErEbpDQ= +go.sia.tech/coreutils v0.9.0/go.mod h1:KFq1q5/YbPH6ZSWtXCxA1bRhBF5Zgcj8G3Wvu0jr/BA= +go.sia.tech/mux v1.3.0 h1:hgR34IEkqvfBKUJkAzGi31OADeW2y7D6Bmy/Jcbop9c= +go.sia.tech/mux v1.3.0/go.mod h1:I46++RD4beqA3cW9Xm9SwXbezwPqLvHhVs9HLpDtt58= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -lukechampine.com/frand v1.4.2 h1:RzFIpOvkMXuPMBb9maa4ND4wjBn71E1Jpf8BzJHMaVw= -lukechampine.com/frand v1.4.2/go.mod h1:4S/TM2ZgrKejMcKMbeLjISpJMO+/eZ1zu3vYX9dtj3s= +lukechampine.com/frand v1.5.1 h1:fg0eRtdmGFIxhP5zQJzM1lFDbD6CUfu/f+7WgAZd5/w= +lukechampine.com/frand v1.5.1/go.mod h1:4VstaWc2plN4Mjr10chUD46RAVGWhpkZ5Nja8+Azp0Q= diff --git a/sdk/main.go b/sdk/main.go index 5ebf343d3..3a36d6c70 100644 --- a/sdk/main.go +++ b/sdk/main.go @@ -35,6 +35,8 @@ func main() { "encodeTransaction": jsFunc(encodeTransaction), "transactionId": jsFunc(transactionID), "signTransactionV1": jsFunc(signTransactionV1), + "v2TransactionInputSigHash": jsFunc(v2TransactionInputSigHash), + "signHash": jsFunc(signHash), }, }) c := make(chan bool, 1) diff --git a/sdk/rhp.go b/sdk/rhp.go index 5c892766b..1200d2b4e 100644 --- a/sdk/rhp.go +++ b/sdk/rhp.go @@ -4,7 +4,7 @@ import ( "encoding/hex" "syscall/js" - "go.sia.tech/core/rhp/v4" + proto4 "go.sia.tech/core/rhp/v4" ) func generateAccount(this js.Value, args []js.Value) result { @@ -12,7 +12,7 @@ func generateAccount(this js.Value, args []js.Value) result { return resultErr(err) } - pk, a := rhp.GenerateAccount() + pk, a := proto4.GenerateAccount() privateKey := hex.EncodeToString(pk) account := hex.EncodeToString(a[:]) @@ -29,15 +29,15 @@ func encodeSettingsRequest(this js.Value, args []js.Value) result { if err := checkArgs(args); err != nil { return resultErr(err) } - var r rhp.RPCSettingsRequest - return encodeRPCRequest(js.Undefined(), &r) + var r proto4.RPCSettingsRequest + return encodeRPCRequest(proto4.RPCSettingsID, js.Undefined(), &r) } func decodeSettingsRequest(this js.Value, args []js.Value) result { if err := checkArgs(args, js.TypeObject); err != nil { return resultErr(err) } - var r rhp.RPCSettingsRequest + var r proto4.RPCSettingsRequest return decodeRPCRequest(args[0], &r) } @@ -45,7 +45,7 @@ func encodeSettingsResponse(this js.Value, args []js.Value) result { if err := checkArgs(args, js.TypeObject); err != nil { return resultErr(err) } - var r rhp.RPCSettingsResponse + var r proto4.RPCSettingsResponse return encodeRPCResponse(args[0], &r) } @@ -53,7 +53,7 @@ func decodeSettingsResponse(this js.Value, args []js.Value) result { if err := checkArgs(args, js.TypeObject); err != nil { return resultErr(err) } - var r rhp.RPCSettingsResponse + var r proto4.RPCSettingsResponse return decodeRPCResponse(args[0], &r) } @@ -64,15 +64,15 @@ func encodeReadSectorRequest(this js.Value, args []js.Value) result { return resultErr(err) } - var r rhp.RPCReadSectorRequest - return encodeRPCRequest(args[0], &r) + var r proto4.RPCReadSectorRequest + return encodeRPCRequest(proto4.RPCReadSectorID, args[0], &r) } func decodeReadSectorRequest(this js.Value, args []js.Value) result { if err := checkArgs(args, js.TypeObject); err != nil { return resultErr(err) } - var r rhp.RPCReadSectorRequest + var r proto4.RPCReadSectorRequest return decodeRPCRequest(args[0], &r) } @@ -80,7 +80,7 @@ func encodeReadSectorResponse(this js.Value, args []js.Value) result { if err := checkArgs(args, js.TypeObject); err != nil { return resultErr(err) } - var r rhp.RPCReadSectorResponse + var r proto4.RPCReadSectorResponse return encodeRPCResponse(args[0], &r) } @@ -88,7 +88,7 @@ func decodeReadSectorResponse(this js.Value, args []js.Value) result { if err := checkArgs(args, js.TypeObject); err != nil { return resultErr(err) } - var r rhp.RPCReadSectorResponse + var r proto4.RPCReadSectorResponse return decodeRPCResponse(args[0], &r) } @@ -98,15 +98,15 @@ func encodeWriteSectorRequest(this js.Value, args []js.Value) result { if err := checkArgs(args, js.TypeObject); err != nil { return resultErr(err) } - var r rhp.RPCWriteSectorRequest - return encodeRPCRequest(args[0], &r) + var r proto4.RPCWriteSectorRequest + return encodeRPCRequest(proto4.RPCWriteSectorID, args[0], &r) } func decodeWriteSectorRequest(this js.Value, args []js.Value) result { if err := checkArgs(args, js.TypeObject); err != nil { return resultErr(err) } - var r rhp.RPCWriteSectorRequest + var r proto4.RPCWriteSectorRequest return decodeRPCRequest(args[0], &r) } @@ -115,7 +115,7 @@ func encodeWriteSectorResponse(this js.Value, args []js.Value) result { return resultErr(err) } - var r rhp.RPCWriteSectorResponse + var r proto4.RPCWriteSectorResponse return encodeRPCResponse(args[0], &r) } @@ -123,6 +123,6 @@ func decodeWriteSectorResponse(this js.Value, args []js.Value) result { if err := checkArgs(args, js.TypeObject); err != nil { return resultErr(err) } - var r rhp.RPCWriteSectorResponse + var r proto4.RPCWriteSectorResponse return decodeRPCResponse(args[0], &r) } diff --git a/sdk/wallet.go b/sdk/wallet.go index 0f9ad4611..6ec1c4249 100644 --- a/sdk/wallet.go +++ b/sdk/wallet.go @@ -13,6 +13,7 @@ import ( "go.sia.tech/coreutils/wallet" ) +// encodeToBytes encodes a value to bytes. func encodeToBytes(v types.EncoderTo) []byte { var buf bytes.Buffer e := types.NewEncoder(&buf) @@ -21,7 +22,7 @@ func encodeToBytes(v types.EncoderTo) []byte { return buf.Bytes() } -// GenerateSeedPhrase returns a new seed phrase. +// generateSeedPhrase returns a new seed phrase. func generateSeedPhrase(this js.Value, args []js.Value) result { if err := checkArgs(args); err != nil { return resultErr(err) @@ -32,7 +33,7 @@ func generateSeedPhrase(this js.Value, args []js.Value) result { }) } -// GenerateKeyPair returns a new ed25519 key pair. +// generateKeyPair returns a new ed25519 key pair. func generateKeyPair(this js.Value, args []js.Value) result { if err := checkArgs(args); err != nil { return resultErr(err) @@ -45,7 +46,7 @@ func generateKeyPair(this js.Value, args []js.Value) result { }) } -// KeyPairFromSeedPhrase returns the key pair corresponding to the given seed phrase and index. +// keyPairFromSeedPhrase returns the key pair corresponding to the given seed phrase and index. func keyPairFromSeedPhrase(this js.Value, args []js.Value) result { if err := checkArgs(args, js.TypeString, js.TypeNumber); err != nil { return resultErr(err) @@ -62,7 +63,7 @@ func keyPairFromSeedPhrase(this js.Value, args []js.Value) result { }) } -// StandardUnlockConditions returns the unlock conditions for a standard v1 unlockhash. +// standardUnlockConditions returns the unlock conditions for a standard v1 unlockhash. func standardUnlockConditions(this js.Value, args []js.Value) result { if err := checkArgs(args, js.TypeString); err != nil { return resultErr(err) @@ -82,7 +83,7 @@ func standardUnlockConditions(this js.Value, args []js.Value) result { }) } -// StandardUnlockHash returns the v1 unlockhash corresponding to the given public key. +// standardUnlockHash returns the v1 unlockhash corresponding to the given public key. func standardUnlockHash(this js.Value, args []js.Value) result { if err := checkArgs(args, js.TypeString); err != nil { return resultErr(err) @@ -97,7 +98,7 @@ func standardUnlockHash(this js.Value, args []js.Value) result { }) } -// AddressFromUnlockConditions returns the address corresponding to the given unlock conditions. +// addressFromUnlockConditions returns the address corresponding to the given unlock conditions. func addressFromUnlockConditions(this js.Value, args []js.Value) result { if err := checkArgs(args, js.TypeObject); err != nil { return resultErr(err) @@ -113,7 +114,7 @@ func addressFromUnlockConditions(this js.Value, args []js.Value) result { }) } -// AddressFromSpendPolicy returns the address of a spend policy. +// addressFromSpendPolicy returns the address of a spend policy. func addressFromSpendPolicy(this js.Value, args []js.Value) result { if err := checkArgs(args, js.TypeString); err != nil { return resultErr(err) @@ -129,7 +130,7 @@ func addressFromSpendPolicy(this js.Value, args []js.Value) result { }) } -// EncodeTransaction returns the binary encoding of a transaction. +// encodeTransaction returns the binary encoding of a transaction. func encodeTransaction(this js.Value, args []js.Value) result { if err := checkArgs(args, js.TypeObject); err != nil { return resultErr(err) @@ -146,7 +147,7 @@ func encodeTransaction(this js.Value, args []js.Value) result { }) } -// SignTransaction returns the signature of a transaction. +// signTransactionV1 returns the signature of a transaction. func signTransactionV1(this js.Value, args []js.Value) result { if err := checkArgs(args, js.TypeObject, js.TypeObject, js.TypeObject, js.TypeNumber, js.TypeString); err != nil { return resultErr(err) @@ -209,3 +210,71 @@ func transactionID(this js.Value, args []js.Value) result { "id": txn.ID().String(), }) } + +// v2TransactionInputSigHash returns the input sighash of a v2 transaction. +func v2TransactionInputSigHash(_ js.Value, args []js.Value) result { + if err := checkArgs(args, js.TypeObject, js.TypeObject, js.TypeObject, js.TypeNumber, js.TypeString); err != nil { + return resultErr(err) + } + + var cs consensus.State + if err := unmarshalStruct(args[0], &cs); err != nil { + return resultErrStr(fmt.Sprintf("error decoding consensus state: %s", err)) + } + + var cn consensus.Network + if err := unmarshalStruct(args[1], &cn); err != nil { + return resultErrStr(fmt.Sprintf("error decoding consensus network: %s", err)) + } + + var txn types.V2Transaction + if err := unmarshalStruct(args[2], &txn); err != nil { + return resultErrStr(fmt.Sprintf("error decoding transaction: %s", err)) + } + cs.Network = &cn + + return result(map[string]any{ + "sighash": cs.InputSigHash(txn).String(), + }) +} + +// signHash returns the signature for a sig hash. +func signHash(_ js.Value, args []js.Value) result { + if err := checkArgs(args, js.TypeString, js.TypeString); err != nil { + return resultErr(err) + } + + buf, err := hex.DecodeString(args[0].String()) + if err != nil { + return resultErrStr(fmt.Sprintf("error decoding private key: %s", err)) + } else if len(buf) != ed25519.PrivateKeySize { + return resultErrStr(fmt.Sprintf("invalid private key length: %d", len(buf))) + } + privateKey := types.PrivateKey(buf) + + var sigHash types.Hash256 + if err := sigHash.UnmarshalText([]byte(args[1].String())); err != nil { + return resultErrStr(fmt.Sprintf("error decoding sig hash: %s", err)) + } + + sig := privateKey.SignHash(sigHash) + return result(map[string]any{ + "signature": sig.String(), + }) +} + +// v2TransactionID returns the ID of a transaction. +func v2TransactionID(_ js.Value, args []js.Value) result { + if err := checkArgs(args, js.TypeObject); err != nil { + return resultErr(err) + } + + var txn types.V2Transaction + if err := unmarshalStruct(args[0], &txn); err != nil { + return resultErrStr(fmt.Sprintf("error decoding transaction: %s", err)) + } + + return result(map[string]any{ + "id": txn.ID().String(), + }) +}