├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── api ├── api.go └── types.go ├── go.mod ├── go.sum ├── interfaces.go ├── transactor ├── transactor.go └── transactor_test.go ├── tx ├── transaction.go └── types.go ├── utils └── utils.go └── wallet ├── testdata └── arweave-test.json ├── wallet.go └── wallet_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | 9 | # Test binary, build with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | arweave.json 16 | main.go -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: "1.14" -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Patrick Guay 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Announcement] 2 | 3 | This Arweave SDK has been archived. If you are looking for an excellent library, please check out everFinance's [goar](https://github.com/everFinance/goar) 4 | 5 | 6 | # Arweave Go SDK 7 | 8 | 9 | [![License](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Dev43/arweave-go/blob/master/LICENSE.md) 10 | [![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://godoc.org/github.com/Dev43/arweave-go) 11 | [![Go Report Card](https://goreportcard.com/badge/github.com/Dev43/arweave-go)](https://goreportcard.com/report/github.com/Dev43/arweave-go) 12 | 13 | Golang Client to interact with the Arweave Blockchain. 14 | 15 | ## Usage 16 | 17 | ### Wallet 18 | 19 | In the current version, you can load the Arweave wallet file created from the Arweave server or the plugin. 20 | 21 | ```golang 22 | // create a new wallet instance 23 | w := wallet.NewWallet() 24 | // extract the key from the wallet instance 25 | err = w.LoadKeyFromFile("./arweave.json") 26 | if err != nil { 27 | //... 28 | } 29 | ``` 30 | 31 | You can directly load the key by using it's filepath or pass it as an array of bytes using `LoadKey([]byte)`. 32 | 33 | With the wallet struct, you can sign and verify a message: 34 | 35 | ```golang 36 | // sign the message "example" 37 | msg := []byte("example") 38 | sig, err := w.Sign(msg)) 39 | if err != nil { 40 | //... 41 | } 42 | err = w.Verify(msg, sig) 43 | if err != nil { 44 | // message signature is not valid... 45 | } 46 | // message signature is valid 47 | ``` 48 | 49 | ### API 50 | 51 | You can call all of the Arweave HTTP api endpoints using the api package. First you must give it the node url or IP address. 52 | 53 | ```golang 54 | ipAddress := "127.0.0.1" 55 | c, err := api.Dial(ipAddress) 56 | if err != nil { 57 | // problem connecting 58 | } 59 | ``` 60 | 61 | To call the endpoints, you will need to pass in a context. 62 | 63 | ```golang 64 | c.GetBalance(context.TODO(), "1seRanklLU_1VTGkEk7P0xAwMJfA7owA1JHW5KyZKlY") 65 | ``` 66 | 67 | ### Transactions 68 | 69 | To create a new transaction, you will need to interact with the `transactor` package. The transactor package has 3 main functions, creating, sending and waiting for a transaction. 70 | 71 | ```golang 72 | // create a new transactor client 73 | ar, err := transactor.NewTransactor("127.0.0.1") 74 | if err != nil { 75 | //... 76 | } 77 | 78 | // create a new wallet instance 79 | w := wallet.NewWallet() 80 | // extract the key from the wallet instance 81 | err = w.LoadKeyFromFile("./arweave.json") 82 | if err != nil { 83 | //... 84 | } 85 | // create a transaction 86 | txBuilder, err := ar.CreateTransaction(context.TODO(), w, "0", []byte(""), "1seRanklLU_1VTGkEk7P0xAwMJfA7owA1JHW5KyZKlY") 87 | if err != nil { 88 | //... 89 | } 90 | 91 | // sign the transaction 92 | txn, err := txBuilder.Sign(w) 93 | if err != nil { 94 | //... 95 | } 96 | 97 | // send the transaction 98 | resp, err := ar.SendTransaction(context.TODO(), txn) 99 | if err != nil { 100 | //... 101 | } 102 | 103 | // wait for the transaction to get mined 104 | finalTx, err := ar.WaitMined(context.TODO(), txn) 105 | if err != nil { 106 | //... 107 | } 108 | // get the hash of the transaction 109 | fmt.Println(finalTx.Hash()) 110 | ``` 111 | 112 | 113 | If you enjoy the library, please consider donating: 114 | 115 | - **Arweave Address**: `pfJXiTwwjQwSJF9VT1ZK6kauvobWuKKLUzjz29R1gbQ` 116 | - **Ethereum Address**: `0x3E42b8b399dca71b5c004921Fc6eFfa8dDc9409d` 117 | -------------------------------------------------------------------------------- /api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "net/http" 12 | 13 | "github.com/Dev43/arweave-go/tx" 14 | ) 15 | 16 | // Client struct 17 | type Client struct { 18 | client *http.Client 19 | url string 20 | } 21 | 22 | // Dial creates a new arweave client 23 | func Dial(url string) (*Client, error) { 24 | return &Client{client: new(http.Client), url: url}, nil 25 | } 26 | 27 | // GetData requests the data of a transaction 28 | func (c *Client) GetData(ctx context.Context, txID string) (string, error) { 29 | body, err := c.get(ctx, fmt.Sprintf("tx/%s/data", txID)) 30 | if err != nil { 31 | return "", err 32 | } 33 | return string(body), nil 34 | } 35 | 36 | func (c *Client) TxAnchor(ctx context.Context) (string, error) { 37 | body, err := c.get(ctx, "tx_anchor") 38 | if err != nil { 39 | return "", err 40 | } 41 | return string(body), nil 42 | } 43 | 44 | // LastTransaction requests the last transaction of an account 45 | func (c *Client) LastTransaction(ctx context.Context, address string) (string, error) { 46 | body, err := c.get(ctx, fmt.Sprintf("wallet/%s/last_tx", address)) 47 | if err != nil { 48 | return "", err 49 | } 50 | return string(body), nil 51 | } 52 | 53 | // GetTransaction requests the information of a transaction 54 | func (c *Client) GetTransaction(ctx context.Context, txID string) (*tx.Transaction, error) { 55 | body, err := c.get(ctx, fmt.Sprintf("tx/%s", txID)) 56 | if err != nil { 57 | return nil, err 58 | } 59 | // If it sends us a pending message, return a nil receipt and error 60 | if string(body) == "Pending" { 61 | return nil, nil 62 | } 63 | tx := tx.Transaction{} 64 | err = json.Unmarshal(body, &tx) 65 | if err != nil { 66 | return nil, err 67 | } 68 | return &tx, nil 69 | } 70 | 71 | // GetTransaction requests the information of a transaction 72 | func (c *Client) GetPendingTransactions(ctx context.Context) ([]string, error) { 73 | body, err := c.get(ctx, fmt.Sprintf("tx/pending")) 74 | if err != nil { 75 | return nil, err 76 | } 77 | tx := []string{} 78 | err = json.Unmarshal(body, &tx) 79 | if err != nil { 80 | return nil, err 81 | } 82 | return tx, nil 83 | } 84 | 85 | // GetTransactionField requests the specific field of a specific transaction 86 | func (c *Client) GetTransactionField(ctx context.Context, txID string, field string) (string, error) { 87 | _, ok := allowedFields[field] 88 | if !ok { 89 | return "", errors.New("field does not exist") 90 | } 91 | body, err := c.get(ctx, fmt.Sprintf("tx/%s/%s", txID, field)) 92 | if err != nil { 93 | return "", err 94 | } 95 | 96 | return string(body), nil 97 | } 98 | 99 | // GetBlockByID requests a block by its id 100 | func (c *Client) GetBlockByID(ctx context.Context, blockID string) (*Block, error) { 101 | body, err := c.get(ctx, fmt.Sprintf("block/hash/%s", blockID)) 102 | if err != nil { 103 | return nil, err 104 | } 105 | block := Block{} 106 | err = json.Unmarshal(body, &block) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | return &block, nil 112 | } 113 | 114 | // GetBlockByHeight requests a block by its height 115 | func (c *Client) GetBlockByHeight(ctx context.Context, height int64) (*Block, error) { 116 | body, err := c.get(ctx, fmt.Sprintf("block/height/%d", height)) 117 | if err != nil { 118 | return nil, err 119 | } 120 | block := Block{} 121 | err = json.Unmarshal(body, &block) 122 | if err != nil { 123 | return nil, err 124 | } 125 | 126 | return &block, nil 127 | } 128 | 129 | // GetCurrentBlock requests the latest block of the weave 130 | func (c *Client) GetCurrentBlock(ctx context.Context) (*Block, error) { 131 | body, err := c.get(ctx, "current_block") 132 | if err != nil { 133 | return nil, err 134 | } 135 | block := Block{} 136 | err = json.Unmarshal(body, &block) 137 | if err != nil { 138 | return nil, err 139 | } 140 | 141 | return &block, nil 142 | } 143 | 144 | // GetReward requests the current network reward 145 | func (c *Client) GetReward(ctx context.Context, data []byte) (string, error) { 146 | body, err := c.get(ctx, fmt.Sprintf("price/%d", len(data))) 147 | if err != nil { 148 | return "", err 149 | } 150 | return string(body), nil 151 | 152 | } 153 | 154 | // GetBalance requests the current balance of an address 155 | func (c *Client) GetBalance(ctx context.Context, address string) (string, error) { 156 | body, err := c.get(ctx, fmt.Sprintf("wallet/%s/balance", address)) 157 | if err != nil { 158 | return "", err 159 | } 160 | return string(body), nil 161 | 162 | } 163 | 164 | // GetPeers requests the list of peers of a node 165 | func (c *Client) GetPeers(ctx context.Context) ([]string, error) { 166 | body, err := c.get(ctx, "peers") 167 | if err != nil { 168 | return nil, err 169 | } 170 | peers := []string{} 171 | err = json.Unmarshal(body, &peers) 172 | if err != nil { 173 | return nil, err 174 | } 175 | 176 | return peers, nil 177 | 178 | } 179 | 180 | // GetInfo requests the information of a node 181 | func (c *Client) GetInfo(ctx context.Context) (*NetworkInfo, error) { 182 | body, err := c.get(ctx, "info") 183 | if err != nil { 184 | return nil, err 185 | } 186 | info := NetworkInfo{} 187 | json.Unmarshal(body, &info) 188 | return &info, nil 189 | } 190 | 191 | // Commit sends a transaction to the weave with a context 192 | func (c *Client) Commit(ctx context.Context, data []byte) (string, error) { 193 | body, err := c.post(ctx, "tx", data) 194 | if err != nil { 195 | return "", err 196 | } 197 | return string(body), nil 198 | } 199 | 200 | func getResponse(resp io.ReadCloser, returnedError error) ([]byte, error) { 201 | if resp != nil { 202 | defer resp.Close() 203 | } 204 | if returnedError != nil { 205 | return handleHTTPError(resp, returnedError) 206 | } 207 | 208 | b, err := ioutil.ReadAll(resp) 209 | if err != nil { 210 | return nil, err 211 | } 212 | return b, nil 213 | } 214 | 215 | func handleHTTPError(resp io.Reader, returnedError error) ([]byte, error) { 216 | if resp != nil { 217 | buf := new(bytes.Buffer) 218 | if _, err := buf.ReadFrom(resp); err == nil { 219 | return nil, fmt.Errorf("%v %v", returnedError, buf.String()) 220 | } 221 | } 222 | return nil, returnedError 223 | } 224 | 225 | func (c *Client) requestWithContext(ctx context.Context, method string, url string, body []byte) (io.ReadCloser, error) { 226 | req, err := http.NewRequest(method, url, ioutil.NopCloser(bytes.NewReader(body))) 227 | if err != nil { 228 | return nil, err 229 | } 230 | reqWithContext := req.WithContext(ctx) 231 | reqWithContext.ContentLength = int64(len(body)) 232 | if method == "POST" { 233 | reqWithContext.Header.Set("Content-type", "application/json") 234 | } 235 | 236 | resp, err := c.client.Do(req) 237 | if err != nil { 238 | return nil, err 239 | } 240 | if resp.StatusCode < 200 || resp.StatusCode >= 300 { 241 | return resp.Body, errors.New(resp.Status) 242 | } 243 | return resp.Body, nil 244 | } 245 | 246 | func (c *Client) post(ctx context.Context, endpoint string, body []byte) ([]byte, error) { 247 | resp, err := c.requestWithContext(ctx, "POST", c.formatURL(endpoint), body) 248 | return getResponse(resp, err) 249 | } 250 | 251 | func (c *Client) get(ctx context.Context, endpoint string) ([]byte, error) { 252 | resp, err := c.requestWithContext(ctx, "GET", c.formatURL(endpoint), nil) 253 | return getResponse(resp, err) 254 | } 255 | 256 | func (c *Client) formatURL(endpoint string) string { 257 | return fmt.Sprintf("%s/%s", c.url, endpoint) 258 | } 259 | -------------------------------------------------------------------------------- /api/types.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | // NetworkInfo struct 4 | type NetworkInfo struct { 5 | Network string `json:"network"` 6 | Version int `json:"version"` 7 | Release int `json:"release"` 8 | Height int `json:"height"` 9 | Current string `json:"current"` 10 | Blocks int `json:"blocks"` 11 | Peers int `json:"peers"` 12 | QueueLength int `json:"queue_length"` 13 | NodeStateLatency int `json:"node_state_latency"` 14 | } 15 | 16 | // Block struct 17 | type Block struct { 18 | HashList []string `json:"hash_list"` 19 | Nonce string `json:"nonce"` 20 | PreviousBlock string `json:"previous_block"` 21 | Timestamp int `json:"timestamp"` 22 | LastRetarget int `json:"last_retarget"` 23 | Diff int `json:"diff"` 24 | Height int `json:"height"` 25 | Hash string `json:"hash"` 26 | IndepHash string `json:"indep_hash"` 27 | Txs []interface{} `json:"txs"` 28 | WalletList []struct { 29 | Wallet string `json:"wallet"` 30 | Quantity int64 `json:"quantity"` 31 | LastTx string `json:"last_tx"` 32 | } `json:"wallet_list"` 33 | RewardAddr string `json:"reward_addr"` 34 | Tags []interface{} `json:"tags"` 35 | RewardPool int `json:"reward_pool"` 36 | WeaveSize int `json:"weave_size"` 37 | BlockSize int `json:"block_size"` 38 | } 39 | 40 | var allowedFields = map[string]bool{ 41 | "id": true, 42 | "last_tx": true, 43 | "owner": true, 44 | "target": true, 45 | "quantity": true, 46 | "type": true, 47 | "data": true, 48 | "reward": true, 49 | "signature": true, 50 | "data.html": true, 51 | } 52 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Dev43/arweave-go 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/mendsley/gojwk v0.0.0-20141217222730-4d5ec6e58103 7 | github.com/stretchr/testify v1.6.1 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/mendsley/gojwk v0.0.0-20141217222730-4d5ec6e58103 h1:Z/i1e+gTZrmcGeZyWckaLfucYG6KYOXLWo4co8pZYNY= 4 | github.com/mendsley/gojwk v0.0.0-20141217222730-4d5ec6e58103/go.mod h1:o9YPB5aGP8ob35Vy6+vyq3P3bWe7NQWzf+JLiXCiMaE= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 8 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 9 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 11 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 12 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 13 | -------------------------------------------------------------------------------- /interfaces.go: -------------------------------------------------------------------------------- 1 | // Package arweave defines interfaces for interacting with the Arweave Blockchain. 2 | package arweave 3 | 4 | import "math/big" 5 | 6 | // WalletSigner is the interface needed to be able to sign an arweave 7 | type WalletSigner interface { 8 | Sign(msg []byte) ([]byte, error) 9 | Verify(msg []byte, sig []byte) error 10 | Address() string 11 | PubKeyModulus() *big.Int 12 | } 13 | -------------------------------------------------------------------------------- /transactor/transactor.go: -------------------------------------------------------------------------------- 1 | package transactor 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "net/url" 9 | "time" 10 | 11 | "github.com/Dev43/arweave-go" 12 | "github.com/Dev43/arweave-go/api" 13 | "github.com/Dev43/arweave-go/tx" 14 | ) 15 | 16 | // defaultPort of the arweave client 17 | const defaultPort = "1984" 18 | 19 | // defaultURL is the local host url 20 | const defaultURL = "http://127.0.0.1" + ":" + defaultPort 21 | 22 | // ClientCaller is the base interface needed to create a Transactor 23 | type ClientCaller interface { 24 | TxAnchor(ctx context.Context) (string, error) 25 | LastTransaction(ctx context.Context, address string) (string, error) 26 | GetReward(ctx context.Context, data []byte) (string, error) 27 | Commit(ctx context.Context, data []byte) (string, error) 28 | GetTransaction(ctx context.Context, txID string) (*tx.Transaction, error) 29 | } 30 | 31 | // Transactor type, allows one to create transactions 32 | type Transactor struct { 33 | Client ClientCaller 34 | } 35 | 36 | // NewTransactor creates a new arweave transactor. You need to pass in a context and a url 37 | // If sending an empty string, the default url is localhosts 38 | func NewTransactor(fullURL string) (*Transactor, error) { 39 | if fullURL == "" { 40 | c, err := api.Dial(defaultURL) 41 | if err != nil { 42 | return nil, err 43 | } 44 | return &Transactor{ 45 | Client: c, 46 | }, nil 47 | } 48 | u, err := url.Parse(fullURL) 49 | if err != nil { 50 | return nil, err 51 | } 52 | formattedURL := fullURL 53 | if u.Scheme == "" { 54 | formattedURL = fmt.Sprintf("http://%s:%s", formattedURL, defaultPort) 55 | } 56 | c, err := api.Dial(formattedURL) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | return &Transactor{ 62 | Client: c, 63 | }, nil 64 | } 65 | 66 | // CreateTransaction creates a brand new transaction 67 | func (tr *Transactor) CreateTransaction(ctx context.Context, w arweave.WalletSigner, amount string, data []byte, target string) (*tx.Transaction, error) { 68 | lastTx, err := tr.Client.TxAnchor(ctx) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | price, err := tr.Client.GetReward(ctx, []byte(data)) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | // Non encoded transaction fields 79 | tx := tx.NewTransaction( 80 | lastTx, 81 | w.PubKeyModulus(), 82 | amount, 83 | target, 84 | data, 85 | price, 86 | ) 87 | 88 | return tx, nil 89 | } 90 | 91 | // SendTransaction formats the transactions (base64url encodes the necessary fields) 92 | // marshalls the Json and sends it to the arweave network 93 | func (tr *Transactor) SendTransaction(ctx context.Context, tx *tx.Transaction) (string, error) { 94 | if len(tx.Signature()) == 0 { 95 | return "", errors.New("transaction missing signature") 96 | } 97 | serialized, err := json.Marshal(tx) 98 | if err != nil { 99 | return "", err 100 | } 101 | return tr.Client.Commit(ctx, serialized) 102 | } 103 | 104 | // WaitMined waits for the transaction to be mined 105 | func (tr *Transactor) WaitMined(ctx context.Context, tx *tx.Transaction) (*tx.Transaction, error) { 106 | ticker := time.NewTicker(time.Second) 107 | defer ticker.Stop() 108 | 109 | for { 110 | receipt, err := tr.Client.GetTransaction(ctx, tx.Hash()) 111 | if receipt != nil { 112 | return receipt, nil 113 | } 114 | if err != nil { 115 | fmt.Printf("Error retrieving transaction %s \n", err.Error()) 116 | } else { 117 | fmt.Printf("Transaction not yet mined \n") 118 | } 119 | select { 120 | case <-ctx.Done(): 121 | return nil, ctx.Err() 122 | case <-ticker.C: 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /transactor/transactor_test.go: -------------------------------------------------------------------------------- 1 | package transactor 2 | 3 | import ( 4 | "context" 5 | "math/big" 6 | "testing" 7 | 8 | "github.com/Dev43/arweave-go/tx" 9 | "github.com/Dev43/arweave-go/utils" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | var ctx = context.TODO() 14 | 15 | type mockCaller struct { 16 | LastTx string 17 | Reward string 18 | Txn *tx.Transaction 19 | } 20 | 21 | func (m *mockCaller) TxAnchor(ctx context.Context) (string, error) { 22 | return m.LastTx, nil 23 | } 24 | 25 | func (m *mockCaller) LastTransaction(ctx context.Context, address string) (string, error) { 26 | return m.LastTx, nil 27 | } 28 | 29 | func (m *mockCaller) GetReward(ctx context.Context, data []byte) (string, error) { 30 | return m.Reward, nil 31 | } 32 | 33 | func (m *mockCaller) Commit(ctx context.Context, data []byte) (string, error) { 34 | return "TESTOK", nil 35 | } 36 | 37 | func (m *mockCaller) GetTransaction(ctx context.Context, txID string) (*tx.Transaction, error) { 38 | return m.Txn, nil 39 | } 40 | 41 | type mockWallet struct { 42 | Signature []byte 43 | TestAddress string 44 | TestPubKeyModulus *big.Int 45 | } 46 | 47 | func (w *mockWallet) Sign(msg []byte) ([]byte, error) { 48 | return w.Signature, nil 49 | } 50 | 51 | func (w *mockWallet) Verify(msg []byte, sig []byte) error { 52 | return nil 53 | } 54 | 55 | func (w *mockWallet) Address() string { 56 | return w.TestAddress 57 | } 58 | 59 | func (w *mockWallet) PubKeyModulus() *big.Int { 60 | return w.TestPubKeyModulus 61 | } 62 | 63 | func TestCreateTransaction(t *testing.T) { 64 | 65 | cases := []struct { 66 | caller *mockCaller 67 | wallet *mockWallet 68 | quantity string 69 | target string 70 | data []byte 71 | tag []tx.Tag 72 | }{ 73 | { 74 | &mockCaller{ 75 | LastTx: "0xA", 76 | Reward: "1000", 77 | Txn: nil}, 78 | &mockWallet{ 79 | Signature: nil, 80 | TestAddress: "0xB", 81 | TestPubKeyModulus: big.NewInt(1), 82 | }, 83 | "1", 84 | "0xC", 85 | []byte("hello"), 86 | make([]tx.Tag, 0), 87 | }, 88 | } 89 | 90 | for _, c := range cases { 91 | tr := Transactor{Client: c.caller} 92 | tx, err := tr.CreateTransaction(ctx, c.wallet, c.quantity, c.data, c.target) 93 | if err != nil { 94 | t.Fatal(err) 95 | } 96 | assert.Equal(t, c.quantity, tx.Quantity(), "quantity field does not match") 97 | assert.Equal(t, c.target, tx.Target(), "target field does not match") 98 | assert.Equal(t, c.caller.LastTx, tx.LastTx(), "last tx field does not match") 99 | assert.Equal(t, c.caller.Reward, tx.Reward(), "reward field does not match") 100 | assert.Equal(t, utils.EncodeToBase64(c.wallet.PubKeyModulus().Bytes()), tx.Owner(), "owner field does not match") 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /tx/transaction.go: -------------------------------------------------------------------------------- 1 | package tx 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/json" 6 | "math/big" 7 | 8 | "github.com/Dev43/arweave-go" 9 | "github.com/Dev43/arweave-go/utils" 10 | ) 11 | 12 | // NewTransaction creates a brand new transaction struct 13 | func NewTransaction(lastTx string, owner *big.Int, quantity string, target string, data []byte, reward string) *Transaction { 14 | return &Transaction{ 15 | lastTx: lastTx, 16 | owner: owner, 17 | quantity: quantity, 18 | target: target, 19 | data: data, 20 | reward: reward, 21 | tags: make([]Tag, 0), 22 | } 23 | } 24 | 25 | // Data returns the data of the transaction 26 | func (t *Transaction) Data() string { 27 | return utils.EncodeToBase64(t.data) 28 | } 29 | 30 | // RawData returns the unencoded data 31 | func (t *Transaction) RawData() []byte { 32 | return t.data 33 | } 34 | 35 | // LastTx returns the last transaction of the account 36 | func (t *Transaction) LastTx() string { 37 | return t.lastTx 38 | } 39 | 40 | // Owner returns the Owner of the transaction 41 | func (t *Transaction) Owner() string { 42 | return utils.EncodeToBase64(t.owner.Bytes()) 43 | } 44 | 45 | // Quantity returns the quantity of the transaction 46 | func (t *Transaction) Quantity() string { 47 | return t.quantity 48 | } 49 | 50 | // Reward returns the reward of the transaction 51 | func (t *Transaction) Reward() string { 52 | return t.reward 53 | } 54 | 55 | // Target returns the target of the transaction 56 | func (t *Transaction) Target() string { 57 | return t.target 58 | } 59 | 60 | // ID returns the id of the transaction which is the SHA256 of the signature 61 | func (t *Transaction) ID() []byte { 62 | return t.id 63 | } 64 | 65 | // Hash returns the base64 RawURLEncoding of the transaction hash 66 | func (t *Transaction) Hash() string { 67 | return utils.EncodeToBase64(t.id) 68 | } 69 | 70 | // Tags returns the tags of the transaction in plain text 71 | func (t *Transaction) Tags() ([]Tag, error) { 72 | tags := []Tag{} 73 | for _, tag := range t.tags { 74 | // access name 75 | tagName, err := utils.DecodeString(tag.Name) 76 | if err != nil { 77 | return nil, err 78 | } 79 | tagValue, err := utils.DecodeString(tag.Value) 80 | if err != nil { 81 | return nil, err 82 | } 83 | tags = append(tags, Tag{Name: string(tagName), Value: string(tagValue)}) 84 | } 85 | return tags, nil 86 | } 87 | 88 | // RawTags returns the unencoded tags of the transaction 89 | func (t *Transaction) RawTags() []Tag { 90 | return t.tags 91 | } 92 | 93 | // AddTag adds a new tag to the transaction 94 | func (t *Transaction) AddTag(name string, value string) error { 95 | tag := Tag{ 96 | Name: utils.EncodeToBase64([]byte(name)), 97 | Value: utils.EncodeToBase64([]byte(value)), 98 | } 99 | t.tags = append(t.tags, tag) 100 | return nil 101 | } 102 | 103 | func (t *Transaction) SetID(id []byte) { 104 | t.id = id 105 | } 106 | 107 | func (t *Transaction) SetSignature(signature []byte) { 108 | t.signature = signature 109 | } 110 | 111 | // Signature returns the signature of the transaction 112 | func (t *Transaction) Signature() string { 113 | return utils.EncodeToBase64(t.signature) 114 | } 115 | 116 | // Sign creates the signing message, and signs it using the private key, 117 | // It takes the SHA256 of the resulting signature to calculate the id of 118 | // the signature 119 | func (t *Transaction) Sign(w arweave.WalletSigner) (*Transaction, error) { 120 | // format the message 121 | payload, err := t.FormatMsgBytes() 122 | if err != nil { 123 | return nil, err 124 | } 125 | msg := sha256.Sum256(payload) 126 | 127 | sig, err := w.Sign(msg[:]) 128 | if err != nil { 129 | return nil, err 130 | } 131 | 132 | err = w.Verify(msg[:], sig) 133 | if err != nil { 134 | return nil, err 135 | } 136 | 137 | id := sha256.Sum256((sig)) 138 | 139 | idB := make([]byte, len(id)) 140 | copy(idB, id[:]) 141 | t.SetID(idB) 142 | 143 | // we copy t into tx 144 | tx := Transaction(*t) 145 | // add the signature and ID to our new signature struct 146 | tx.signature = sig 147 | 148 | return &tx, nil 149 | } 150 | 151 | // MarshalJSON marshals as JSON 152 | func (t *Transaction) MarshalJSON() ([]byte, error) { 153 | return json.Marshal(t.format()) 154 | } 155 | 156 | // UnmarshalJSON unmarshals as JSON 157 | func (t *Transaction) UnmarshalJSON(input []byte) error { 158 | txn := transactionJSON{} 159 | err := json.Unmarshal(input, &txn) 160 | if err != nil { 161 | return err 162 | } 163 | id, err := utils.DecodeString(txn.ID) 164 | if err != nil { 165 | return err 166 | } 167 | t.id = id 168 | 169 | t.lastTx = txn.LastTx 170 | 171 | // gives me byte representation of the big num 172 | owner, err := utils.DecodeString(txn.Owner) 173 | if err != nil { 174 | return err 175 | } 176 | n := new(big.Int) 177 | t.owner = n.SetBytes(owner) 178 | 179 | t.tags = txn.Tags 180 | t.target = txn.Target 181 | t.quantity = txn.Quantity 182 | 183 | data, err := utils.DecodeString(txn.Data) 184 | if err != nil { 185 | return err 186 | } 187 | t.data = data 188 | t.reward = txn.Reward 189 | 190 | sig, err := utils.DecodeString(txn.Signature) 191 | if err != nil { 192 | return err 193 | } 194 | t.signature = sig 195 | 196 | return nil 197 | } 198 | 199 | // FormatMsgBytes formats the message that needs to be signed. All fields 200 | // need to be an array of bytes originating from the necessary data (not base64url encoded). 201 | // The signing message is the SHA256 of the concatenation of the byte arrays 202 | // of the owner public key, target address, data, quantity, reward and last transaction 203 | func (t *Transaction) FormatMsgBytes() ([]byte, error) { 204 | var msg []byte 205 | lastTx, err := utils.DecodeString(t.LastTx()) 206 | if err != nil { 207 | return nil, err 208 | } 209 | target, err := utils.DecodeString(t.Target()) 210 | if err != nil { 211 | return nil, err 212 | } 213 | 214 | tags, err := t.encodeTagData() 215 | if err != nil { 216 | return nil, err 217 | } 218 | 219 | msg = append(msg, t.owner.Bytes()...) 220 | msg = append(msg, target...) 221 | msg = append(msg, t.data...) 222 | msg = append(msg, t.quantity...) 223 | msg = append(msg, t.reward...) 224 | msg = append(msg, lastTx...) 225 | msg = append(msg, tags...) 226 | 227 | return msg, nil 228 | } 229 | 230 | // We need to encode the tag data properly for the signature. This means having the unencoded 231 | // value of the Name field concatenated with the unencoded value of the Value field 232 | func (t *Transaction) encodeTagData() (string, error) { 233 | tagString := "" 234 | unencodedTags, err := t.Tags() 235 | if err != nil { 236 | return "", err 237 | } 238 | for _, tag := range unencodedTags { 239 | tagString += tag.Name + tag.Value 240 | } 241 | 242 | return tagString, nil 243 | } 244 | 245 | // Format formats the transactions to a JSONTransaction that can be sent out to an arweave node 246 | func (t *Transaction) format() *transactionJSON { 247 | return &transactionJSON{ 248 | ID: utils.EncodeToBase64(t.id), 249 | LastTx: t.lastTx, 250 | Owner: utils.EncodeToBase64(t.owner.Bytes()), 251 | Tags: t.tags, 252 | Target: t.target, 253 | Quantity: t.quantity, 254 | Data: utils.EncodeToBase64(t.data), 255 | Reward: t.reward, 256 | Signature: utils.EncodeToBase64(t.signature), 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /tx/types.go: -------------------------------------------------------------------------------- 1 | package tx 2 | 3 | import "math/big" 4 | 5 | // Transaction struct 6 | type Transaction struct { 7 | id []byte // A SHA2-256 hash of the signature 8 | lastTx string // The ID of the last transaction made from the account. If no previous transactions have been made from the address this field is set to an empty string. 9 | owner *big.Int // The modulus of the RSA key pair corresponding to the wallet making the transaction 10 | target string // If making a financial transaction this field contains the wallet address of the recipient base64url encoded. If the transaction is not a financial this field is set to an empty string. 11 | quantity string // If making a financial transaction this field contains the amount in Winston to be sent to the receiving wallet. If the transaction is not financial this field is set to the string "0". 1 AR = 1000000000000 (1e+12) Winston 12 | data []byte // If making an archiving transaction this field contains the data to be archived base64url encoded. If the transaction is not archival this field is set to an empty string. 13 | reward string // This field contains the mining reward for the transaction in Winston. 14 | tags []Tag // Transaction tags 15 | signature []byte // Signature using the RSA-PSS signature scheme using SHA256 as the MGF1 masking algorithm 16 | } 17 | 18 | // Transaction encoded transaction to send to the arweave client 19 | type transactionJSON struct { 20 | // Id A SHA2-256 hash of the signature, based 64 URL encoded. 21 | ID string `json:"id"` 22 | // LastTx represents the ID of the last transaction made from the same address base64url encoded. If no previous transactions have been made from the address this field is set to an empty string. 23 | LastTx string `json:"last_tx"` 24 | //Owner is the modulus of the RSA key pair corresponding to the wallet making the transaction, base64url encoded. 25 | Owner string `json:"owner"` 26 | // Target if making a financial transaction this field contains the wallet address of the recipient base64url encoded. If the transaction is not a financial this field is set to an empty string. 27 | Target string `json:"target"` 28 | // Quantity If making a financial transaction this field contains the amount in Winston to be sent to the receiving wallet. If the transaction is not financial this field is set to the string "0". 1 AR = 1000000000000 (1e+12) Winston 29 | Quantity string `json:"quantity"` 30 | // Data If making an archiving transaction this field contains the data to be archived base64url encoded. If the transaction is not archival this field is set to an empty string. 31 | Data string `json:"data"` 32 | // Reward This field contains the mining reward for the transaction in Winston. 33 | Reward string `json:"reward"` 34 | // Signature using the RSA-PSS signature scheme using SHA256 as the MGF1 masking algorithm 35 | Signature string `json:"signature"` 36 | // Tags Transaction tags 37 | Tags []Tag `json:"tags"` 38 | } 39 | 40 | // Tag contains any tags the user wants to add to the transaction 41 | type Tag struct { 42 | Name string `json:"name"` 43 | Value string `json:"value"` 44 | } 45 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "encoding/base64" 4 | 5 | // EncodeToBase64 encodes a byte array to base64 raw url encoding 6 | func EncodeToBase64(toEncode []byte) string { 7 | return base64.RawURLEncoding.EncodeToString(toEncode) 8 | } 9 | 10 | // DecodeString decodes from base64 raw url encoding to byte array 11 | func DecodeString(toDecode string) ([]byte, error) { 12 | return base64.RawURLEncoding.DecodeString(toDecode) 13 | } 14 | -------------------------------------------------------------------------------- /wallet/testdata/arweave-test.json: -------------------------------------------------------------------------------- 1 | {"kty":"RSA","e":"AQAB","n":"z3KaHo82nvA_LQmDo_gcsz9_U_HFSkmr3CPfl_bPi5HZGj6Qlov3rcqyfIangs4Kc6WzKjppjoI403xYrB2C0Sd4P4AgKafGc3tO7d9p2XO3B8kPrDK-vL6A0xaSYmAvRccBPX3i9lTiWHO7uCtNuwCLNfxKXOMWr0-sUmVFdMhSIb8pxOc_STE9S0qiwYIDIBL5O6TsDBh_Grd50-6opzARmRqZBE7cdfQcZJzM1BOF4BI-UJ093AEGSBySvG4MH3zB_gH-bve3M3lebNwZPNzR74NCeXoWSFyELSXv59KAYSYCFWwHM02Gl_9okCK3hHNf6Gb0cHaiRoiqUboxPt50FRlJ7BXBvzOICI1_VCAwRAjguuxKDiUh7fMsfUOpuiKi2gzoglmGpBd73QejSN_FNjBNPGs8T9FihcxXZrEsqvW4RkjWKCQd8YH-PAgBh9y9v3rkWTSLIxysARj84mpF7ojqdeSRjA-OEIgA-q_PAtCLU4eV6MwwImtivGj6_RHqRAzy7e-8Wg-v0NgVkQiRbHkRYAMCdw9AL3V4xJnspx9sNsXMj8x06X2u-S4NPeJUZO9fvwITG80VVaeYcODWh6wEU8e9_dRMhMXVP_vMANeigUCdRyJg6CTevpqT5vVU8Wrw1oUw2KFFtcKtFXueexp2UiPzeGWg7h-M8o0","d":"A-9xX4eJennlxOmzitqD1t2rQXEqdDaSEU7H6NbYiJYHd5E8xnJYzD32ZVjbmCZm-dsomzI4-uhbAh4Pg5JavzN_uzqnwYqKf_nUnTwaArNIg1gL2VEsH6IAiSCaPc4Dz4_PJPa8pb4l_hYR91Qw-Q1xVqCW24y-SyyYgQalSuoa7sN03CSj5XRO7Jz5mfCotQTPIBPIm5EiZiQZZk0We3d0ugIQaY3CbmVnXXxBAXvuU_ocnwRgnkoRr7AFE5iF19oSxwoMJNhDrDEO-sMxIl-xBtlnWy0W-5_vrPiVhMEQSOeC7huaxl0f4sXIWsWh1BwpM1wyomiJuwtQKdU9pmIs6bHk6ZQ_AD77tvuXD6phYne4uIFbf6z2RReSHgTYWsWIc1LVMrWrcB3_XStWSv0yXFWHT-4_nvt9PwERjjBWUdmbu7Xfkh5h0ZIMKjjlcrvPdQo8IKBAWF07Hv0zwFIE1XV4A5iM-2zKyF_p73YS5ZZ-oH4EUM-JDyX-gzKOUmBNNnV4vt-UcUacNkbGoEYuK4-HWntAdfv2tEWeYosSfnao8cGrrAQFYmerAHbC-U0KG5swFOyHRYkT8TNqSgHqUVUXfDftH7BAgewPSjghuO1YG5QG35moCUU7UxlBrVln8c3iMz2Sg9BW6TKKIFhGicFRzQUlNy8gDWLiIXk","p":"6aedc48Vqh62ug_zV6ZvlUr8y6qArt_zeNEUxokCv3EBPwHGkLyG5QQclITMX4_0lz8j8YDPxsXAT7RlxT_Ak4Q3meFs0m4DqOM0RiyQVNJLdYW6oGVhEYm6BaBp7c8P9UrbmT1mlJfOZfDkX3zWNZviAjI8FRpZdhH1Xdo1Zj4VB6TI_3E9x4VcLq7eqtHktzfjKfi4vxqpw3EYdgtAl9PR3p4RcUJv0uE2UpV_BzYzn43ePzj1DSb3DlKTijBTl1nMVtr8P2OMv5wwOauPRTA7SWcZugX_gJ_STMsVlLcter9DRTkGW_PtH9x8qpxx7iE6grFE4hrexvY4eMLn9Q","q":"40lhMPSnIYsmIQm4lxSAyk5gSXntAcArJ3ZdWeVnRX73M1_U4YcJveMYLZEn4hlWI32SMN6Nk2nMR4O1Z0reMo5qapimX9pfsNS4N2UZfWgKd8FknU4_j8jP-pS76KvjFIfe7sFG_40GPdPpJRs0YdQRBk_UCG0UIPfJe8QZc5Lc5T2NCDa3c5ClBF8fYIFILnWKz_af1KxmM4YmA6sMy0PpqIA5sRyXxqJi50VPKjLqV30Je_czQgwzf7RxA9L_pSdu6BDTdVbDXkMO4ksX8VtcUiZq2BctKoDN7PUQ1le8MKc-EE1h8S1lpCny8JDyINfmEoh-xviaX47BMiL5OQ","dp":"vc1n7Lhdu-zpOGm-ngPYfqkEg5lEoUnQ6OLHAqPHtKXigkE6ANb0qVLWuJrLvhEdekNTOloN7oUzPEvpo_quv-ZWmEzzbljp5iZyj1KiCBUwMQ5p1OOln26Gyw1NH13ls7eUfX3QN4K9qjet_BdXlwLC7E_MWMQnye66p_ubBywFP0lP8ZkWu14MzOtGe_K23VKr2Ktvv-Wz48yaN0HavbrWsAUtXBjA58gErb89Cimwc7r1v9vPzFhLtrnDmiy5qqfKgv-uivueGQSSdDZ10plkf7MdxewbamvplHIgXMcUTbLSvYeeL9INXKVqK9mb_vnFIudHcnirYWof45FIhQ","dq":"LhAXFHbg0FmFFrkDogrTtyx_MyT0taAentbMQIXPkjQTIWRQnoxhmwxFQCbk8Fu2K2ctk53nD3MnpW65-v8fXRuj0YqyXUbCkfHqi79_lPza8j5Nh9vt4dQO4nf2RUdgtC59LeIZ43zPQQSLgauryA_Ui3TuQe7pX20Ydm7dwaCICCWbSYjdURyFdQdaMpLA8Dl-MnuHFPEDnXmYhrHJ3AAdgLrL9msXD3illr1jN-gtuiFrJKaKyt3yKfWF2gKPMQd24K1YTbWRzq7Ee3RAm_eqFDQY_0ZjLvP_yAA6s1YAm9OpLmCgcuKCcOzXgY2-pI1c4XCsUOLdVnR2DxUrcQ","qi":"r0dKlStKfYvVU7guprqd4LvfoNbih2-k7qIqJZNDJhE4PS-JgCKav0Lpxd361TZS1pgTkmVrJrAPfIQIpLPHqilLi6PSgb-fUKnf9GaGR9io5E6LqdPjdVED9LDz9fV9N9XBYwrHfLXYoW38A3TEX9Hzbn-ulfy9eF_PsvwxepzPU3w3CWzRSr6wgPnzMUBVPDpcMoVfdodzVGWYrlYt-LLViHnHLB7ORQGifm2yKByXZTLQM84982QT8jCS06eQJT5sb_NuWOUDSnuVMocNlccUJJcj8d6TOzKGsReCbSmOdTBhvUXu5GekHR1H-rFfzjqFwrRWya1bCBb9byPeng"} -------------------------------------------------------------------------------- /wallet/wallet.go: -------------------------------------------------------------------------------- 1 | package wallet 2 | 3 | import ( 4 | "crypto" 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "crypto/sha256" 8 | "fmt" 9 | "io/ioutil" 10 | "math/big" 11 | 12 | "github.com/Dev43/arweave-go/utils" 13 | "github.com/mendsley/gojwk" 14 | ) 15 | 16 | var opts = &rsa.PSSOptions{ 17 | SaltLength: rsa.PSSSaltLengthAuto, 18 | Hash: crypto.SHA256, 19 | } 20 | 21 | // Wallet struct 22 | type Wallet struct { 23 | address string 24 | key *gojwk.Key 25 | publicKey string 26 | pubKey *rsa.PublicKey 27 | } 28 | 29 | // NewWallet creates a new wallet instance 30 | func NewWallet() *Wallet { 31 | return &Wallet{} 32 | } 33 | 34 | // GenerateWallet generates a new JWK wallet. 35 | func GenerateWallet() *Wallet { 36 | reader := rand.Reader 37 | rsaKey, _ := rsa.GenerateKey(reader, 4096) 38 | w := &Wallet{} 39 | 40 | w.key = &gojwk.Key{ 41 | Kty: "RSA", 42 | N: utils.EncodeToBase64(rsaKey.N.Bytes()), 43 | E: utils.EncodeToBase64(big.NewInt(int64(rsaKey.E)).Bytes()), 44 | D: utils.EncodeToBase64(rsaKey.D.Bytes()), 45 | } 46 | w.pubKey = rsaKey.Public().(*rsa.PublicKey) 47 | // Take the "n", in bytes and hash it using SHA256 48 | h := sha256.New() 49 | h.Write(rsaKey.N.Bytes()) 50 | 51 | // Finally base64url encode it to have the resulting address 52 | w.address = utils.EncodeToBase64(h.Sum(nil)) 53 | w.publicKey = utils.EncodeToBase64(rsaKey.N.Bytes()) 54 | return w 55 | } 56 | 57 | // Address returns the address of the account 58 | func (w *Wallet) Address() string { 59 | return w.address 60 | } 61 | 62 | // PubKeyModulus returns the modulus of the RSA public key 63 | func (w *Wallet) PubKeyModulus() *big.Int { 64 | return w.pubKey.N 65 | } 66 | 67 | // Sign signs a message using the RSA-PSS scheme with an MGF SHA256 masking function 68 | func (w *Wallet) Sign(msg []byte) ([]byte, error) { 69 | priv, err := w.key.DecodePrivateKey() 70 | if err != nil { 71 | return nil, err 72 | } 73 | rng := rand.Reader 74 | privRsa, ok := priv.(*rsa.PrivateKey) 75 | if !ok { 76 | return nil, fmt.Errorf("could not typecast key to %T", rsa.PrivateKey{}) 77 | } 78 | 79 | sig, err := rsa.SignPSS(rng, privRsa, crypto.SHA256, msg, opts) 80 | if err != nil { 81 | return nil, err 82 | } 83 | return sig, nil 84 | } 85 | 86 | // Verify verifies the signature for the specific message 87 | func (w *Wallet) Verify(msg []byte, sig []byte) error { 88 | pub, err := w.key.DecodePublicKey() 89 | if err != nil { 90 | return err 91 | } 92 | pubKey, ok := pub.(*rsa.PublicKey) 93 | if !ok { 94 | return fmt.Errorf("could not typecast key to %T", rsa.PublicKey{}) 95 | } 96 | 97 | err = rsa.VerifyPSS(pubKey, crypto.SHA256, msg, sig, opts) 98 | if err != nil { 99 | return err 100 | } 101 | return nil 102 | } 103 | 104 | // LoadKeyFromFile loads and Arweave RSA key from a file to our wallet 105 | func (w *Wallet) LoadKeyFromFile(path string) error { 106 | b, err := ioutil.ReadFile(path) 107 | if err != nil { 108 | return err 109 | } 110 | return w.LoadKey(b) 111 | } 112 | 113 | // LoadKey loads an Arweave RSA key into our wallet 114 | func (w *Wallet) LoadKey(rsaKeyBytes []byte) error { 115 | 116 | key, err := gojwk.Unmarshal(rsaKeyBytes) 117 | if err != nil { 118 | return err 119 | } 120 | 121 | publicKey, err := key.DecodePublicKey() 122 | if err != nil { 123 | return err 124 | } 125 | pubKey, ok := publicKey.(*rsa.PublicKey) 126 | if !ok { 127 | return fmt.Errorf("could not typecast key to %T", rsa.PublicKey{}) 128 | } 129 | w.pubKey = pubKey 130 | // Take the "n", in bytes and hash it using SHA256 131 | h := sha256.New() 132 | h.Write(pubKey.N.Bytes()) 133 | 134 | // Finally base64url encode it to have the resulting address 135 | w.address = utils.EncodeToBase64(h.Sum(nil)) 136 | w.publicKey = utils.EncodeToBase64(pubKey.N.Bytes()) 137 | w.key = key 138 | 139 | return nil 140 | } 141 | -------------------------------------------------------------------------------- /wallet/wallet_test.go: -------------------------------------------------------------------------------- 1 | package wallet 2 | 3 | import ( 4 | "crypto/sha256" 5 | "io/ioutil" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | const keyfileName = "arweave-test.json" 13 | const address = "WWMgP35v3BRciaex-sdy-VRfd254M8bqK52v0zRH0Lc" 14 | const modulus = int64(8675517201791120013) 15 | const signature = "Tqq9J7St5qX3OcItXtVdiIzSzbrY2BQSRhmQoYPs4GItaQp_Y2tIqSTxHAQLh6X7MjmLHEQFh4L_5u3JAErT10Z22yFSLOYXwmmCDg8CApkWKuanNTs_YjkTQzvFvPfYy5qcCZipVPcR4unuOORJ0_naGbCxMgHkypgNcdWIepDkjqpqBVWu2VPRCWN4Mw-w7v58kJAKTV8fZnj1n4uuhGmCpd6_WvZFaRAl-LJr-iYUNu6oeZoSuzeH-3Y8k-n6erLq1sbIU6NAbGZheG0KViQt4kpPQPtkRzED3avACIxg224qhQ-elze4BjJRVHZ-SnMQQ-O2TJ90dniD6YhU0KazpDEYfix33Ev0MXrlCY9gHIhQFMwOkTWZtDv4gN76daV0x4J2MQH8q2HK8axLcRTJC-uTpQ7PlhqXyU1VER7MIXX7UX4LMgq1lkoU9PY123wDkstJ-TOjix_iZOMdGQ7GqahV70mZk458kgHYgxVkC8g0PtBE55MGBTojWzFv-hxfNAYXbq4yLzx2akdSMlbtL2LrFPZ19bDHUBLdb9Lq09fIYvQjcjZYi1QyPRFDhnhfBXRkrdLh9WVKpmRpGVyZLbnAlQ1Wkw6zeUZhjIs2mGAh0WbEcVQzVK1_I5cfwoTAr0z8cFP26eJGfXnJ4rd1xCdlszSwWFb112Suc6c" 16 | 17 | func helperLoadBytes(t *testing.T, name string) []byte { 18 | path := filepath.Join("testdata", name) // relative path 19 | bytes, err := ioutil.ReadFile(path) 20 | if err != nil { 21 | t.Fatal(err) 22 | } 23 | return bytes 24 | } 25 | func TestLoadKey(t *testing.T) { 26 | b := helperLoadBytes(t, keyfileName) 27 | w := NewWallet() 28 | 29 | w.LoadKey(b) 30 | ensureCorrectCryptoValues(t, w) 31 | 32 | w.LoadKeyFromFile(filepath.Join("testdata", keyfileName)) 33 | ensureCorrectCryptoValues(t, w) 34 | 35 | } 36 | 37 | func ensureCorrectCryptoValues(t *testing.T, w *Wallet) { 38 | assert.Equal(t, address, w.Address(), "Address is not the same") 39 | assert.Equal(t, modulus, w.PubKeyModulus().Int64(), "Modulus is not the same") 40 | } 41 | 42 | func TestSignature(t *testing.T) { 43 | w := NewWallet() 44 | w.LoadKeyFromFile(filepath.Join("testdata", keyfileName)) 45 | 46 | toSign := []byte("hello") 47 | msg := sha256.Sum256(toSign) 48 | 49 | sig, err := w.Sign(msg[:]) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | 54 | err = w.Verify(msg[:], sig) 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | 59 | } 60 | 61 | func TestGeneration(t *testing.T) { 62 | w := GenerateWallet() 63 | 64 | toSign := []byte("hello") 65 | msg := sha256.Sum256(toSign) 66 | 67 | sig, err := w.Sign(msg[:]) 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | 72 | err = w.Verify(msg[:], sig) 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | } --------------------------------------------------------------------------------