├── .envrc.sample ├── .github └── workflows │ └── lint.yml ├── .gitignore ├── LICENSE ├── README.md ├── cmd ├── block │ └── main.go ├── edkeypair │ └── main.go ├── funcall │ └── main.go ├── genesis │ └── main.go ├── keys │ └── main.go └── transfer │ └── main.go ├── flake.lock ├── flake.nix ├── go.mod ├── go.sum ├── nearnet ├── Dockerfile ├── docker-compose.yml ├── entrypoint.sh ├── get_creds.sh └── tail_logs.sh └── pkg ├── client ├── access_key.go ├── access_key_structs.go ├── account.go ├── account_structs.go ├── block.go ├── block │ └── block.go ├── block_structs.go ├── chunk.go ├── chunk_structs.go ├── client.go ├── common.go ├── context.go ├── contract.go ├── contract_structs.go ├── gas.go ├── gas_structs.go ├── genesis.go ├── network.go ├── network_structs.go ├── protocol.go ├── transaction.go ├── transaction_structs.go ├── transaction_wrapper.go └── utils.go ├── config ├── network.go └── network_test.go ├── jsonrpc ├── client.go ├── error.go ├── opts.go └── structs.go └── types ├── action ├── access_key.go └── action.go ├── balance.go ├── balance_test.go ├── basic.go ├── constants.go ├── hash └── crypto_hash.go ├── key ├── aliases.go ├── base58_public_key.go ├── common.go ├── key_pair.go ├── key_pair_test.go ├── public_key.go └── public_key_test.go ├── signature ├── base58_signature.go ├── common.go ├── signature.go └── signature_test.go └── transaction ├── signed_transaction.go ├── transaction.go └── utils.go /.envrc.sample: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - master 8 | pull_request: 9 | 10 | jobs: 11 | golangci: 12 | name: "Run linter (golangci-lint)" 13 | runs-on: "ubuntu-latest" 14 | steps: 15 | - name: "Checkout code" 16 | uses: "actions/checkout@v2" 17 | 18 | - name: "Run golangci-lint" 19 | id: "run-linter" 20 | uses: "golangci/golangci-lint-action@v2" 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.direnv 2 | /.envrc 3 | .idea/ 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 eTEU Technologies Ltd 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # near-api-go 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/eteu-technologies/near-api-go.svg)](https://pkg.go.dev/github.com/eteu-technologies/near-api-go) 4 | [![CI](https://github.com/eteu-technologies/near-api-go/actions/workflows/lint.yml/badge.svg)](https://github.com/eteu-technologies/near-api-go/actions/workflows/lint.yml) 5 | 6 | **WARNING**: This library is still work in progress. While it covers about 90% of the use-cases, it does not have many batteries included. 7 | 8 | ## Usage 9 | 10 | ``` 11 | go get github.com/eteu-technologies/near-api-go 12 | ``` 13 | 14 | ### Notes 15 | 16 | What this library does not (and probably won't) provide: 17 | - Access key caching & management 18 | - Retry solution for `TransactionSendAwait` 19 | 20 | What this library doesn't have yet: 21 | - Response types for RPC queries marked as experimental by NEAR (prefixed with `EXPERIMENTAL_`) 22 | - Few type definitions (especially complex ones, for example it's not very comfortable to reflect enum types in Go) 23 | 24 | ## Examples 25 | 26 | See [cmd/](cmd/) in this repo for more fully featured CLI examples. 27 | See [NEARKit](https://github.com/eteu-technologies/nearkit) for a project which makes use of this API. 28 | 29 | ### Query latest block on NEAR testnet 30 | ```go 31 | package main 32 | 33 | import ( 34 | "context" 35 | "fmt" 36 | 37 | "github.com/eteu-technologies/near-api-go/pkg/client" 38 | "github.com/eteu-technologies/near-api-go/pkg/client/block" 39 | ) 40 | 41 | func main() { 42 | rpc, err := client.NewClient("https://rpc.testnet.near.org") 43 | if err != nil { 44 | panic(err) 45 | } 46 | 47 | ctx := context.Background() 48 | 49 | res, err := rpc.BlockDetails(ctx, block.FinalityFinal()) 50 | if err != nil { 51 | panic(err) 52 | } 53 | 54 | fmt.Println("latest block: ", res.Header.Hash) 55 | } 56 | ``` 57 | 58 | ### Transfer 1 NEAR token between accounts 59 | 60 | ```go 61 | package main 62 | 63 | import ( 64 | "context" 65 | "fmt" 66 | 67 | "github.com/eteu-technologies/near-api-go/pkg/client" 68 | "github.com/eteu-technologies/near-api-go/pkg/types" 69 | "github.com/eteu-technologies/near-api-go/pkg/types/action" 70 | "github.com/eteu-technologies/near-api-go/pkg/types/key" 71 | ) 72 | 73 | var ( 74 | sender = "mikroskeem.testnet" 75 | recipient = "mikroskeem2.testnet" 76 | 77 | senderPrivateKey = `ed25519:...` 78 | ) 79 | 80 | func main() { 81 | rpc, err := client.NewClient("https://rpc.testnet.near.org") 82 | if err != nil { 83 | panic(err) 84 | } 85 | 86 | keyPair, err := key.NewBase58KeyPair(senderPrivateKey) 87 | if err != nil { 88 | panic(err) 89 | } 90 | 91 | ctx := client.ContextWithKeyPair(context.Background(), keyPair) 92 | res, err := rpc.TransactionSendAwait(ctx, sender, recipient, []action.Action{ 93 | action.NewTransfer(types.NEARToYocto(1)), 94 | }) 95 | if err != nil { 96 | panic(err) 97 | } 98 | 99 | fmt.Printf("https://rpc.testnet.near.org/transactions/%s\n", res.Transaction.Hash) 100 | } 101 | ``` 102 | 103 | ## License 104 | 105 | MIT 106 | -------------------------------------------------------------------------------- /cmd/block/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/davecgh/go-spew/spew" 10 | "github.com/urfave/cli/v2" 11 | 12 | "github.com/eteu-technologies/near-api-go/pkg/client" 13 | "github.com/eteu-technologies/near-api-go/pkg/client/block" 14 | "github.com/eteu-technologies/near-api-go/pkg/config" 15 | ) 16 | 17 | func main() { 18 | app := &cli.App{ 19 | Name: "block", 20 | Usage: "View latest or specified block info", 21 | Flags: []cli.Flag{ 22 | &cli.StringFlag{ 23 | Name: "network", 24 | Usage: "NEAR network", 25 | Value: "testnet", 26 | EnvVars: []string{"NEAR_ENV"}, 27 | }, 28 | &cli.StringFlag{ 29 | Name: "block", 30 | Usage: "Block hash", 31 | }, 32 | }, 33 | Action: entrypoint, 34 | } 35 | 36 | if err := app.Run(os.Args); err != nil { 37 | log.Fatal(err) 38 | } 39 | } 40 | 41 | func entrypoint(cctx *cli.Context) (err error) { 42 | networkID := cctx.String("network") 43 | network, ok := config.Networks[networkID] 44 | if !ok { 45 | return fmt.Errorf("unknown network '%s'", networkID) 46 | } 47 | 48 | rpc, err := client.NewClient(network.NodeURL) 49 | if err != nil { 50 | return fmt.Errorf("failed to create rpc client: %w", err) 51 | } 52 | 53 | characteristic := block.FinalityFinal() 54 | if v := cctx.String("block"); v != "" { 55 | characteristic = block.BlockHashRaw(v) 56 | } 57 | 58 | blockDetailsResp, err := rpc.BlockDetails(context.Background(), characteristic) 59 | if err != nil { 60 | return fmt.Errorf("failed to query latest block info: %w", err) 61 | } 62 | 63 | spew.Dump(blockDetailsResp) 64 | 65 | return 66 | } 67 | -------------------------------------------------------------------------------- /cmd/edkeypair/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/eteu-technologies/near-api-go/pkg/types/key" 10 | ) 11 | 12 | func main() { 13 | keyPair, err := key.GenerateKeyPair(key.KeyTypeED25519, rand.Reader) 14 | if err != nil { 15 | fmt.Fprintf(os.Stderr, "failed to generate keypair: %s\n", err) 16 | os.Exit(1) 17 | } 18 | 19 | pub := keyPair.PublicKey 20 | 21 | _ = json.NewEncoder(os.Stdout).Encode(struct { 22 | AccountID string `json:"account_id"` 23 | PublicKey key.Base58PublicKey `json:"public_key"` 24 | PrivateKey string `json:"private_key"` 25 | }{pub.ToPublicKey().Hash(), pub, keyPair.PrivateEncoded()}) 26 | } 27 | -------------------------------------------------------------------------------- /cmd/funcall/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/hex" 6 | "encoding/json" 7 | "fmt" 8 | "log" 9 | "os" 10 | "path/filepath" 11 | 12 | "github.com/urfave/cli/v2" 13 | 14 | "github.com/eteu-technologies/near-api-go/pkg/client" 15 | "github.com/eteu-technologies/near-api-go/pkg/client/block" 16 | "github.com/eteu-technologies/near-api-go/pkg/config" 17 | "github.com/eteu-technologies/near-api-go/pkg/types" 18 | "github.com/eteu-technologies/near-api-go/pkg/types/action" 19 | "github.com/eteu-technologies/near-api-go/pkg/types/hash" 20 | "github.com/eteu-technologies/near-api-go/pkg/types/key" 21 | ) 22 | 23 | func main() { 24 | app := &cli.App{ 25 | Name: "funcall", 26 | Usage: "Calls function on a smart contract", 27 | Flags: []cli.Flag{ 28 | &cli.StringFlag{ 29 | Name: "account", 30 | Usage: "Account id", 31 | }, 32 | &cli.StringFlag{ 33 | Name: "target", 34 | Required: true, 35 | Usage: "Account id whose smart contract to call", 36 | }, 37 | &cli.StringFlag{ 38 | Name: "mode", 39 | Usage: "Call mode, either 'view' or 'change'", 40 | Value: "view", 41 | }, 42 | &cli.StringFlag{ 43 | Name: "deposit", 44 | Usage: "Amount of NEAR to deposit", 45 | }, 46 | &cli.Uint64Flag{ 47 | Name: "gas", 48 | Usage: "Amount of gas to attach for this transaction", 49 | Value: types.DefaultFunctionCallGas, 50 | }, 51 | &cli.StringFlag{ 52 | Name: "method", 53 | Usage: "Method to call on specified contract", 54 | Required: true, 55 | }, 56 | &cli.StringFlag{ 57 | Name: "args", 58 | Usage: "Arguments to pass for specified method. Accepts both JSON and Base64 payload", 59 | }, 60 | &cli.StringFlag{ 61 | Name: "network", 62 | Usage: "NEAR network", 63 | Value: "testnet", 64 | EnvVars: []string{"NEAR_ENV"}, 65 | }, 66 | }, 67 | Action: entrypoint, 68 | } 69 | 70 | if err := app.Run(os.Args); err != nil { 71 | log.Fatal(err) 72 | } 73 | } 74 | 75 | func entrypoint(cctx *cli.Context) (err error) { 76 | network, ok := config.Networks[cctx.String("network")] 77 | if !ok { 78 | return fmt.Errorf("unknown network '%s'", cctx.String("network")) 79 | } 80 | 81 | rpc, err := client.NewClient(network.NodeURL) 82 | if err != nil { 83 | return fmt.Errorf("failed to create rpc client: %w", err) 84 | } 85 | 86 | log.Printf("near network: %s", rpc.NetworkAddr()) 87 | 88 | var args []byte = nil 89 | if a := cctx.String("args"); cctx.IsSet("args") { 90 | args = []byte(a) 91 | } 92 | 93 | switch cctx.String("mode") { 94 | case "view": 95 | if err := viewFunction(cctx, rpc, args); err != nil { 96 | return fmt.Errorf("failed to call view function: %w", err) 97 | } 98 | case "change": 99 | if !cctx.IsSet("account") { 100 | return fmt.Errorf("--account is required for change function call") 101 | } 102 | 103 | keyPair, err := resolveCredentials(network.NetworkID, cctx.String("account")) 104 | if err != nil { 105 | return fmt.Errorf("failed to load private key: %w", err) 106 | } 107 | 108 | if err := changeFunction(cctx, rpc, keyPair, network, args); err != nil { 109 | return fmt.Errorf("failed to call change function: %w", err) 110 | } 111 | default: 112 | return fmt.Errorf("either 'change' or 'view' is accepted, you supplied '%s'", cctx.String("mode")) 113 | } 114 | 115 | return 116 | } 117 | 118 | func viewFunction(cctx *cli.Context, rpc client.Client, args []byte) (err error) { 119 | res, err := rpc.ContractViewCallFunction(cctx.Context, cctx.String("target"), cctx.String("method"), base64.StdEncoding.EncodeToString(args), block.FinalityFinal()) 120 | if err != nil { 121 | return 122 | } 123 | 124 | if l := res.Logs; len(l) > 0 { 125 | log.Println("logs:") 126 | for _, line := range l { 127 | log.Printf("- %s", line) 128 | } 129 | } 130 | 131 | log.Println("result:") 132 | if len(res.Result) == 0 { 133 | fmt.Println("(empty)") 134 | return 135 | } 136 | 137 | fmt.Printf("%s", hex.Dump(res.Result)) 138 | 139 | return 140 | } 141 | 142 | func changeFunction(cctx *cli.Context, rpc client.Client, keyPair key.KeyPair, network config.NetworkInfo, args []byte) (err error) { 143 | var deposit types.Balance = types.NEARToYocto(0) 144 | var gas types.Gas = cctx.Uint64("gas") 145 | 146 | if cctx.IsSet("deposit") { 147 | depositValue := cctx.String("deposit") 148 | deposit, err = types.BalanceFromString(depositValue) 149 | if err != nil { 150 | return fmt.Errorf("failed to parse amount '%s': %w", depositValue, err) 151 | } 152 | } 153 | 154 | // Make a transaction 155 | res, err := rpc.TransactionSendAwait( 156 | cctx.Context, 157 | cctx.String("account"), 158 | cctx.String("target"), 159 | []action.Action{ 160 | action.NewFunctionCall(cctx.String("method"), args, gas, deposit), 161 | }, 162 | client.WithLatestBlock(), 163 | client.WithKeyPair(keyPair), 164 | ) 165 | if err != nil { 166 | return fmt.Errorf("failed to do txn: %w", err) 167 | } 168 | 169 | // Try to get logs 170 | type LogEntry struct { 171 | Executor types.AccountID 172 | Lines []string 173 | } 174 | logEntries := map[hash.CryptoHash]*LogEntry{} 175 | for _, receipt := range res.ReceiptsOutcome { 176 | if len(receipt.Outcome.Logs) == 0 { 177 | continue 178 | } 179 | 180 | entry, ok := logEntries[receipt.ID] 181 | if !ok { 182 | entry = &LogEntry{ 183 | Executor: receipt.Outcome.ExecutorID, 184 | Lines: []string{}, 185 | } 186 | logEntries[receipt.ID] = entry 187 | } 188 | 189 | entry.Lines = append(entry.Lines, receipt.Outcome.Logs...) 190 | } 191 | 192 | if len(logEntries) > 0 { 193 | oneEntry := len(logEntries) == 1 194 | 195 | log.Println("logs:") 196 | for _, receipt := range res.ReceiptsOutcome { 197 | logEntry, ok := logEntries[receipt.ID] 198 | if !ok { 199 | continue 200 | } 201 | 202 | for _, line := range logEntry.Lines { 203 | if oneEntry { 204 | log.Printf("- %s", line) 205 | } else { 206 | log.Printf("- (%s / %s) %s", receipt.ID, logEntry.Executor, line) 207 | } 208 | } 209 | } 210 | } 211 | 212 | log.Printf("tx id: %s/transactions/%s", network.ExplorerURL, res.Transaction.Hash) 213 | return 214 | } 215 | 216 | func resolveCredentials(networkName string, id types.AccountID) (kp key.KeyPair, err error) { 217 | var creds struct { 218 | AccountID types.AccountID `json:"account_id"` 219 | PublicKey key.Base58PublicKey `json:"public_key"` 220 | PrivateKey key.KeyPair `json:"private_key"` 221 | } 222 | 223 | var home string 224 | home, err = os.UserHomeDir() 225 | if err != nil { 226 | return 227 | } 228 | 229 | credsFile := filepath.Join(home, ".near-credentials", networkName, fmt.Sprintf("%s.json", id)) 230 | 231 | var cf *os.File 232 | if cf, err = os.Open(credsFile); err != nil { 233 | return 234 | } 235 | defer cf.Close() 236 | 237 | if err = json.NewDecoder(cf).Decode(&creds); err != nil { 238 | return 239 | } 240 | 241 | if creds.PublicKey.String() != creds.PrivateKey.PublicKey.String() { 242 | err = fmt.Errorf("inconsistent public key, %s != %s", creds.PublicKey.String(), creds.PrivateKey.PublicKey.String()) 243 | return 244 | } 245 | kp = creds.PrivateKey 246 | 247 | return 248 | } 249 | -------------------------------------------------------------------------------- /cmd/genesis/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/urfave/cli/v2" 10 | 11 | "github.com/eteu-technologies/near-api-go/pkg/client" 12 | "github.com/eteu-technologies/near-api-go/pkg/config" 13 | ) 14 | 15 | func main() { 16 | app := &cli.App{ 17 | Name: "genesis", 18 | Usage: "Gets genesis config for the network", 19 | Flags: []cli.Flag{ 20 | &cli.StringFlag{ 21 | Name: "network", 22 | Usage: "NEAR network", 23 | Value: "testnet", 24 | EnvVars: []string{"NEAR_ENV"}, 25 | }, 26 | }, 27 | Action: entrypoint, 28 | } 29 | 30 | if err := app.Run(os.Args); err != nil { 31 | log.Fatal(err) 32 | } 33 | } 34 | 35 | func entrypoint(cctx *cli.Context) (err error) { 36 | networkID := cctx.String("network") 37 | network, ok := config.Networks[networkID] 38 | if !ok { 39 | return fmt.Errorf("unknown network '%s'", networkID) 40 | } 41 | 42 | rpc, err := client.NewClient(network.NodeURL) 43 | if err != nil { 44 | return fmt.Errorf("failed to create rpc client: %w", err) 45 | } 46 | 47 | genesisConfig, err := rpc.GenesisConfig(cctx.Context) 48 | if err != nil { 49 | return fmt.Errorf("failed to query genesis config: %w", err) 50 | } 51 | 52 | encoder := json.NewEncoder(os.Stdout) 53 | encoder.SetIndent("", " ") 54 | _ = encoder.Encode(genesisConfig) 55 | 56 | return 57 | } 58 | -------------------------------------------------------------------------------- /cmd/keys/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/davecgh/go-spew/spew" 10 | "github.com/urfave/cli/v2" 11 | 12 | "github.com/eteu-technologies/near-api-go/pkg/client" 13 | "github.com/eteu-technologies/near-api-go/pkg/client/block" 14 | "github.com/eteu-technologies/near-api-go/pkg/config" 15 | "github.com/eteu-technologies/near-api-go/pkg/types/key" 16 | ) 17 | 18 | func main() { 19 | app := &cli.App{ 20 | Name: "keys", 21 | Usage: "Display access keys attached to an account", 22 | Flags: []cli.Flag{ 23 | &cli.StringFlag{ 24 | Name: "account", 25 | Required: true, 26 | Usage: "Account id", 27 | }, 28 | &cli.StringFlag{ 29 | Name: "key", 30 | Usage: "Specific key to query. Otherwise shows all access keys", 31 | }, 32 | &cli.StringFlag{ 33 | Name: "network", 34 | Usage: "NEAR network", 35 | Value: "testnet", 36 | EnvVars: []string{"NEAR_ENV"}, 37 | }, 38 | }, 39 | Action: entrypoint, 40 | } 41 | 42 | if err := app.Run(os.Args); err != nil { 43 | log.Fatal(err) 44 | } 45 | } 46 | 47 | func entrypoint(cctx *cli.Context) (err error) { 48 | network, ok := config.Networks[cctx.String("network")] 49 | if !ok { 50 | return fmt.Errorf("unknown network '%s'", cctx.String("network")) 51 | } 52 | 53 | rpc, err := client.NewClient(network.NodeURL) 54 | if err != nil { 55 | return fmt.Errorf("failed to create rpc client: %w", err) 56 | } 57 | 58 | log.Printf("near network: %s", rpc.NetworkAddr()) 59 | 60 | ctx := context.Background() 61 | if rawKey := cctx.String("key"); cctx.IsSet("key") { 62 | pubKey, err := key.NewBase58PublicKey(rawKey) 63 | if err != nil { 64 | return fmt.Errorf("failed to parse access pubkey: %w", err) 65 | } 66 | 67 | accessKeyViewResp, err := rpc.AccessKeyView(ctx, cctx.String("account"), pubKey, block.FinalityFinal()) 68 | if err != nil { 69 | return fmt.Errorf("failed to query access key: %w", err) 70 | } 71 | 72 | spew.Dump(accessKeyViewResp) 73 | } else { 74 | accessKeyViewListResp, err := rpc.AccessKeyViewList(ctx, cctx.String("account"), block.FinalityFinal()) 75 | if err != nil { 76 | return fmt.Errorf("failed to query access key list: %w", err) 77 | } 78 | 79 | spew.Dump(accessKeyViewListResp) 80 | } 81 | 82 | return 83 | } 84 | -------------------------------------------------------------------------------- /cmd/transfer/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/urfave/cli/v2" 12 | 13 | "github.com/eteu-technologies/near-api-go/pkg/client" 14 | "github.com/eteu-technologies/near-api-go/pkg/config" 15 | "github.com/eteu-technologies/near-api-go/pkg/types" 16 | "github.com/eteu-technologies/near-api-go/pkg/types/action" 17 | "github.com/eteu-technologies/near-api-go/pkg/types/key" 18 | ) 19 | 20 | func main() { 21 | app := &cli.App{ 22 | Name: "transfer", 23 | Usage: "Transfer NEAR between accounts", 24 | Flags: []cli.Flag{ 25 | &cli.StringFlag{ 26 | Name: "from", 27 | Required: true, 28 | Usage: "Sender account id", 29 | }, 30 | &cli.StringFlag{ 31 | Name: "to", 32 | Aliases: []string{"recipient"}, 33 | Required: true, 34 | Usage: "Recipient account id", 35 | }, 36 | &cli.StringFlag{ 37 | Name: "amount", 38 | Required: true, 39 | Usage: "Amount of NEAR to send", 40 | }, 41 | &cli.StringFlag{ 42 | Name: "network", 43 | Usage: "NEAR network", 44 | Value: "testnet", 45 | EnvVars: []string{"NEAR_ENV"}, 46 | }, 47 | }, 48 | Action: entrypoint, 49 | } 50 | 51 | if err := app.Run(os.Args); err != nil { 52 | log.Fatal(err) 53 | } 54 | } 55 | 56 | func entrypoint(cctx *cli.Context) (err error) { 57 | networkID := cctx.String("network") 58 | senderID := cctx.String("from") 59 | recipientID := cctx.String("to") 60 | amountValue := cctx.String("amount") 61 | 62 | var amount types.Balance 63 | 64 | amount, err = types.BalanceFromString(amountValue) 65 | if err != nil { 66 | return fmt.Errorf("failed to parse amount '%s': %w", amountValue, err) 67 | } 68 | 69 | network, ok := config.Networks[networkID] 70 | if !ok { 71 | return fmt.Errorf("unknown network '%s'", networkID) 72 | } 73 | 74 | keyPair, err := resolveCredentials(networkID, senderID) 75 | if err != nil { 76 | return fmt.Errorf("failed to load private key: %w", err) 77 | } 78 | 79 | rpc, err := client.NewClient(network.NodeURL) 80 | if err != nil { 81 | return fmt.Errorf("failed to create rpc client: %w", err) 82 | } 83 | 84 | log.Printf("near network: %s", rpc.NetworkAddr()) 85 | 86 | ctx := client.ContextWithKeyPair(context.Background(), keyPair) 87 | res, err := rpc.TransactionSendAwait( 88 | ctx, senderID, recipientID, 89 | []action.Action{ 90 | action.NewTransfer(amount), 91 | }, 92 | client.WithLatestBlock(), 93 | ) 94 | if err != nil { 95 | return fmt.Errorf("failed to do txn: %w", err) 96 | } 97 | 98 | log.Printf("tx url: %s/transactions/%s", network.ExplorerURL, res.Transaction.Hash) 99 | return 100 | } 101 | 102 | func resolveCredentials(networkName string, id types.AccountID) (kp key.KeyPair, err error) { 103 | var creds struct { 104 | AccountID types.AccountID `json:"account_id"` 105 | PublicKey key.Base58PublicKey `json:"public_key"` 106 | PrivateKey key.KeyPair `json:"private_key"` 107 | } 108 | 109 | var home string 110 | home, err = os.UserHomeDir() 111 | if err != nil { 112 | return 113 | } 114 | 115 | credsFile := filepath.Join(home, ".near-credentials", networkName, fmt.Sprintf("%s.json", id)) 116 | 117 | var cf *os.File 118 | if cf, err = os.Open(credsFile); err != nil { 119 | return 120 | } 121 | defer cf.Close() 122 | 123 | if err = json.NewDecoder(cf).Decode(&creds); err != nil { 124 | return 125 | } 126 | 127 | if creds.PublicKey.String() != creds.PrivateKey.PublicKey.String() { 128 | err = fmt.Errorf("inconsistent public key, %s != %s", creds.PublicKey.String(), creds.PrivateKey.PublicKey.String()) 129 | return 130 | } 131 | kp = creds.PrivateKey 132 | 133 | return 134 | } 135 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "locked": { 5 | "lastModified": 1659877975, 6 | "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", 7 | "owner": "numtide", 8 | "repo": "flake-utils", 9 | "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "numtide", 14 | "repo": "flake-utils", 15 | "type": "github" 16 | } 17 | }, 18 | "nixpkgs": { 19 | "locked": { 20 | "lastModified": 1660551188, 21 | "narHash": "sha256-a1LARMMYQ8DPx1BgoI/UN4bXe12hhZkCNqdxNi6uS0g=", 22 | "owner": "NixOS", 23 | "repo": "nixpkgs", 24 | "rev": "441dc5d512153039f19ef198e662e4f3dbb9fd65", 25 | "type": "github" 26 | }, 27 | "original": { 28 | "owner": "NixOS", 29 | "ref": "nixpkgs-unstable", 30 | "repo": "nixpkgs", 31 | "type": "github" 32 | } 33 | }, 34 | "root": { 35 | "inputs": { 36 | "flake-utils": "flake-utils", 37 | "nixpkgs": "nixpkgs" 38 | } 39 | } 40 | }, 41 | "root": "root", 42 | "version": 7 43 | } 44 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "near-api-go development shell"; 3 | 4 | inputs = { 5 | flake-utils.url = "github:numtide/flake-utils"; 6 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 7 | }; 8 | 9 | outputs = { self, nixpkgs, flake-utils }: 10 | let 11 | supportedSystems = [ 12 | "aarch64-linux" 13 | "aarch64-darwin" 14 | "x86_64-linux" 15 | "x86_64-darwin" 16 | ]; 17 | in 18 | flake-utils.lib.eachSystem supportedSystems (system: 19 | let 20 | pkgs = nixpkgs.legacyPackages.${system}; 21 | in 22 | { 23 | devShell = pkgs.mkShell { 24 | nativeBuildInputs = [ 25 | pkgs.go 26 | pkgs.golangci-lint 27 | pkgs.gopls 28 | pkgs.act 29 | ]; 30 | }; 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/eteu-technologies/near-api-go 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/davecgh/go-spew v1.1.1 7 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 8 | github.com/eteu-technologies/borsh-go v0.3.2 9 | github.com/eteu-technologies/golang-uint128 v1.1.2-eteu 10 | github.com/google/gofuzz v1.2.0 11 | github.com/mr-tron/base58 v1.2.0 12 | github.com/shopspring/decimal v1.3.1 13 | github.com/urfave/cli/v2 v2.3.0 14 | ) 15 | 16 | require ( 17 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect 18 | github.com/russross/blackfriday/v2 v2.0.1 // indirect 19 | github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= 3 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= 7 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= 8 | github.com/eteu-technologies/borsh-go v0.3.2 h1:2c4H5yNtgzvgiYOZbN/DKE+iFjS6hK0QibEvw/5ELsI= 9 | github.com/eteu-technologies/borsh-go v0.3.2/go.mod h1:WK4tVIecqMGB9WXrvULSeEe3POIXw+ZUFWV4HRTUCZA= 10 | github.com/eteu-technologies/golang-uint128 v1.1.1-eteu/go.mod h1:5Vr5yDsJV9eBl44ziSgt1xDZbVfJv4+IpqnUf0wa8t4= 11 | github.com/eteu-technologies/golang-uint128 v1.1.2-eteu h1:hEc9sN76qbV0l40GUq2lMyBDXZlTkts7cLCd+3vHkao= 12 | github.com/eteu-technologies/golang-uint128 v1.1.2-eteu/go.mod h1:5Vr5yDsJV9eBl44ziSgt1xDZbVfJv4+IpqnUf0wa8t4= 13 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 14 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 15 | github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= 16 | github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= 17 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 18 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 19 | github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= 20 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 21 | github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= 22 | github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 23 | github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= 24 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 25 | github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= 26 | github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= 27 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 28 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 29 | -------------------------------------------------------------------------------- /nearnet/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nearprotocol/nearup:latest 2 | 3 | COPY --from=nearprotocol/nearcore:latest /usr/local/bin/neard /neard-local/neard 4 | COPY ./entrypoint.sh /entrypoint.sh 5 | 6 | ENTRYPOINT [ "/entrypoint.sh" ] 7 | -------------------------------------------------------------------------------- /nearnet/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.4' 2 | 3 | services: 4 | nearup: 5 | #image: nearprotocol/nearup:latest 6 | build: . 7 | command: 'run localnet --verbose --binary-path /neard-local' 8 | init: true 9 | volumes: 10 | - data:/root/.near 11 | ports: 12 | - 127.0.0.1:3030:3030 13 | 14 | volumes: 15 | data: 16 | -------------------------------------------------------------------------------- /nearnet/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | datadir=/root/.near 5 | pidfile="${HOME}/.nearup/node.pid" 6 | 7 | if [ -f "${pidfile}" ]; then 8 | rm "${pidfile}" 9 | fi 10 | 11 | nearup "${@}" 12 | sleep 1 13 | 14 | pid="$(sed 's/^\(\w\+\)|.*/\1/g' "${pidfile}" | head -1)" 15 | while (kill -0 "${pid}" &>/dev/null); do 16 | sleep 2 17 | done 18 | -------------------------------------------------------------------------------- /nearnet/get_creds.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | datadir="$(docker volume inspect nearnet_data --format '{{ .Mountpoint }}')" 5 | sudo cat "${datadir}"/localnet/node0/validator_key.json 6 | -------------------------------------------------------------------------------- /nearnet/tail_logs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | docker-compose exec nearup tail -f /root/.nearup/logs/localnet/node0.log 5 | -------------------------------------------------------------------------------- /pkg/client/access_key.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/eteu-technologies/near-api-go/pkg/client/block" 9 | "github.com/eteu-technologies/near-api-go/pkg/jsonrpc" 10 | "github.com/eteu-technologies/near-api-go/pkg/types" 11 | "github.com/eteu-technologies/near-api-go/pkg/types/key" 12 | ) 13 | 14 | // https://docs.near.org/docs/api/rpc#view-access-key 15 | func (c *Client) AccessKeyView(ctx context.Context, accountID types.AccountID, publicKey key.Base58PublicKey, block block.BlockCharacteristic) (resp AccessKeyView, err error) { 16 | _, err = c.doRPC(ctx, &resp, "query", block, map[string]interface{}{ 17 | "request_type": "view_access_key", 18 | "account_id": accountID, 19 | "public_key": publicKey, 20 | }) 21 | 22 | if resp.Error != nil { 23 | err = fmt.Errorf("RPC returned an error: %w", errors.New(*resp.Error)) 24 | } 25 | 26 | return 27 | } 28 | 29 | // https://docs.near.org/docs/api/rpc#view-access-key-list 30 | func (c *Client) AccessKeyViewList(ctx context.Context, accountID types.AccountID, block block.BlockCharacteristic) (resp AccessKeyList, err error) { 31 | _, err = c.doRPC(ctx, &resp, "query", block, map[string]interface{}{ 32 | "request_type": "view_access_key_list", 33 | "account_id": accountID, 34 | }) 35 | 36 | return 37 | } 38 | 39 | // TODO: decode response 40 | // https://docs.near.org/docs/api/rpc#view-access-key-changes-single 41 | func (c *Client) AccessKeyViewChanges(ctx context.Context, accountID types.AccountID, publicKey key.Base58PublicKey, block block.BlockCharacteristic) (res jsonrpc.Response, err error) { 42 | res, err = c.doRPC(ctx, nil, "EXPERIMENTAL_changes", block, map[string]interface{}{ 43 | "changes_type": "single_access_key_changes", 44 | "keys": map[string]interface{}{ 45 | "account_id": accountID, 46 | "public_key": publicKey, 47 | }, 48 | }) 49 | 50 | return 51 | } 52 | 53 | // TODO: decode response 54 | // https://docs.near.org/docs/api/rpc#view-access-key-changes-all 55 | func (c *Client) AccessKeyViewChangesAll(ctx context.Context, accountIDs []types.AccountID, block block.BlockCharacteristic) (res jsonrpc.Response, err error) { 56 | res, err = c.doRPC(ctx, nil, "EXPERIMENTAL_changes", block, map[string]interface{}{ 57 | "changes_type": "all_access_key_changes", 58 | "account_ids": accountIDs, 59 | }) 60 | 61 | return 62 | } 63 | -------------------------------------------------------------------------------- /pkg/client/access_key_structs.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/eteu-technologies/near-api-go/pkg/types" 8 | "github.com/eteu-technologies/near-api-go/pkg/types/key" 9 | ) 10 | 11 | type AccessKey struct { 12 | Nonce types.Nonce `json:"nonce"` 13 | 14 | // Permission holds parsed access key permission info 15 | Permission AccessKeyPermission `json:"-"` 16 | } 17 | 18 | func (ak *AccessKey) UnmarshalJSON(b []byte) (err error) { 19 | // Unmarshal into inline struct to avoid recursion 20 | var tmp struct { 21 | Nonce types.Nonce `json:"nonce"` 22 | RawPermission json.RawMessage `json:"permission"` 23 | } 24 | if err = json.Unmarshal(b, &tmp); err != nil { 25 | return 26 | } 27 | 28 | *ak = AccessKey{ 29 | Nonce: tmp.Nonce, 30 | } 31 | err = ak.Permission.UnmarshalJSON(tmp.RawPermission) 32 | 33 | return 34 | } 35 | 36 | // AccessKeyPermission holds info whether access key is a FullAccess, or FunctionCall key 37 | type AccessKeyPermission struct { 38 | FullAccess bool `json:"-"` 39 | FunctionCall FunctionCallPermission `json:"-"` 40 | } 41 | 42 | func (akp *AccessKeyPermission) UnmarshalJSON(b []byte) (err error) { 43 | *akp = AccessKeyPermission{} 44 | 45 | // Option 1: "FullAccess" 46 | var s string 47 | if err = json.Unmarshal(b, &s); err == nil { 48 | switch s { 49 | case "FullAccess": 50 | akp.FullAccess = true 51 | return 52 | default: 53 | return fmt.Errorf("'%s' is neither object or 'FullAccess'", s) 54 | } 55 | } else if jerr, ok := err.(*json.UnmarshalTypeError); ok && jerr.Value != "object" { 56 | // If trying to unmarshal object into string, then continue. Otherwise return here 57 | return 58 | } 59 | 60 | // Option 2: Function call 61 | var perm struct { 62 | FunctionCall FunctionCallPermission `json:"FunctionCall"` 63 | } 64 | err = json.Unmarshal(b, &perm) 65 | akp.FunctionCall = perm.FunctionCall 66 | 67 | return 68 | } 69 | 70 | // FunctionCallPermission represents a function call permission 71 | type FunctionCallPermission struct { 72 | // Allowance for this function call (default 0.25 NEAR). Can be absent. 73 | Allowance *types.Balance `json:"allowance"` 74 | // ReceiverID holds the contract the key is allowed to call methods on 75 | ReceiverID types.AccountID `json:"receiver_id"` 76 | // MethodNames hold which functions are allowed to call. Can be empty (all methods are allowed) 77 | MethodNames []string `json:"method_names"` 78 | } 79 | 80 | type AccessKeyView struct { 81 | AccessKey 82 | QueryResponse 83 | } 84 | 85 | func (a *AccessKeyView) UnmarshalJSON(data []byte) (err error) { 86 | var qr QueryResponse 87 | var ak AccessKey 88 | 89 | if err = json.Unmarshal(data, &qr); err != nil { 90 | err = fmt.Errorf("unable to parse QueryResponse: %w", err) 91 | return 92 | } 93 | 94 | if qr.Error == nil { 95 | if err = json.Unmarshal(data, &ak); err != nil { 96 | err = fmt.Errorf("unable to parse AccessKey: %w", err) 97 | return 98 | } 99 | } 100 | 101 | *a = AccessKeyView{ 102 | AccessKey: ak, 103 | QueryResponse: qr, 104 | } 105 | return 106 | } 107 | 108 | type AccessKeyViewInfo struct { 109 | PublicKey key.Base58PublicKey `json:"public_key"` 110 | AccessKey AccessKey `json:"access_key"` 111 | } 112 | 113 | type AccessKeyList struct { 114 | Keys []AccessKeyViewInfo `json:"keys"` 115 | } 116 | -------------------------------------------------------------------------------- /pkg/client/account.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/eteu-technologies/near-api-go/pkg/client/block" 7 | "github.com/eteu-technologies/near-api-go/pkg/jsonrpc" 8 | "github.com/eteu-technologies/near-api-go/pkg/types" 9 | ) 10 | 11 | // https://docs.near.org/docs/api/rpc#view-account 12 | func (c *Client) AccountView(ctx context.Context, accountID types.AccountID, block block.BlockCharacteristic) (res AccountView, err error) { 13 | _, err = c.doRPC(ctx, &res, "query", block, map[string]interface{}{ 14 | "request_type": "view_account", 15 | "account_id": accountID, 16 | }) 17 | 18 | return 19 | } 20 | 21 | // TODO: decode response 22 | // https://docs.near.org/docs/api/rpc#view-account-changes 23 | func (c *Client) AccountViewChanges(ctx context.Context, accountIDs []types.AccountID, block block.BlockCharacteristic) (res jsonrpc.Response, err error) { 24 | res, err = c.doRPC(ctx, nil, "EXPERIMENTAL_changes", block, map[string]interface{}{ 25 | "changes_type": "account_changes", 26 | "account_ids": accountIDs, 27 | }) 28 | 29 | return 30 | } 31 | -------------------------------------------------------------------------------- /pkg/client/account_structs.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/eteu-technologies/near-api-go/pkg/types" 5 | "github.com/eteu-technologies/near-api-go/pkg/types/hash" 6 | ) 7 | 8 | type AccountView struct { 9 | Amount types.Balance `json:"amount"` 10 | Locked types.Balance `json:"locked"` 11 | CodeHash hash.CryptoHash `json:"code_hash"` 12 | StorageUsage types.StorageUsage `json:"storage_usage"` 13 | StoragePaidAt types.BlockHeight `json:"storage_paid_at"` 14 | 15 | QueryResponse 16 | } 17 | -------------------------------------------------------------------------------- /pkg/client/block.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/eteu-technologies/near-api-go/pkg/client/block" 7 | "github.com/eteu-technologies/near-api-go/pkg/jsonrpc" 8 | ) 9 | 10 | // https://docs.near.org/docs/api/rpc#block-details 11 | func (c *Client) BlockDetails(ctx context.Context, block block.BlockCharacteristic) (resp BlockView, err error) { 12 | _, err = c.doRPC(ctx, &resp, "block", block, map[string]interface{}{}) 13 | 14 | return 15 | } 16 | 17 | // TODO: decode resposne 18 | // https://docs.near.org/docs/api/rpc#changes-in-block 19 | func (c *Client) BlockChanges(ctx context.Context, block block.BlockCharacteristic) (res jsonrpc.Response, err error) { 20 | res, err = c.doRPC(ctx, nil, "EXPERIMENTAL_changes_in_block", block, map[string]interface{}{}) 21 | 22 | return 23 | } 24 | -------------------------------------------------------------------------------- /pkg/client/block/block.go: -------------------------------------------------------------------------------- 1 | package block 2 | 3 | import ( 4 | "github.com/eteu-technologies/near-api-go/pkg/types/hash" 5 | ) 6 | 7 | // BlockCharacteristic is a function type aiding with specifying a block 8 | type BlockCharacteristic func(map[string]interface{}) 9 | 10 | // FinalityOptimistic specifies the latest block recorded on the node that responded to your query (<1 second delay after the transaction is submitted) 11 | func FinalityOptimistic() BlockCharacteristic { 12 | return func(params map[string]interface{}) { 13 | params["finality"] = "optimistic" 14 | } 15 | } 16 | 17 | // FinalityFinal specifies a block that has been validated on at least 66% of the nodes in the network (usually takes 2 blocks / approx. 2 second delay) 18 | func FinalityFinal() BlockCharacteristic { 19 | return func(params map[string]interface{}) { 20 | params["finality"] = "final" 21 | } 22 | } 23 | 24 | // BlockID specifies a block id/height 25 | func BlockID(blockID uint) BlockCharacteristic { 26 | return func(params map[string]interface{}) { 27 | params["block_id"] = blockID 28 | } 29 | } 30 | 31 | // BlockHash specifies a block hash 32 | func BlockHash(blockHash hash.CryptoHash) BlockCharacteristic { 33 | return func(params map[string]interface{}) { 34 | params["block_id"] = blockHash 35 | } 36 | } 37 | 38 | // BlockHashRaw is a variant of `BlockHash` function accepting a raw block hash (string) 39 | func BlockHashRaw(blockHash string) BlockCharacteristic { 40 | return func(params map[string]interface{}) { 41 | params["block_id"] = blockHash 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pkg/client/block_structs.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/eteu-technologies/near-api-go/pkg/types" 5 | "github.com/eteu-technologies/near-api-go/pkg/types/hash" 6 | "github.com/eteu-technologies/near-api-go/pkg/types/key" 7 | "github.com/eteu-technologies/near-api-go/pkg/types/signature" 8 | ) 9 | 10 | type ChallengesResult = []SlashedValidator 11 | 12 | type SlashedValidator struct { 13 | AccountID types.AccountID `json:"account_id"` 14 | IsDoubleSign bool `json:"is_double_sign"` 15 | } 16 | 17 | // ValidatorStakeView is based on ValidatorStakeV1 struct in nearcore 18 | type ValidatorStakeView struct { 19 | AccountID types.AccountID `json:"account_id"` 20 | PublicKey key.Base58PublicKey `json:"public_key"` 21 | Stake types.Balance `json:"stake"` 22 | } 23 | 24 | type BlockView struct { 25 | Author types.AccountID `json:"author"` 26 | Header BlockHeaderView `json:"header"` 27 | Chunks []ChunkHeaderView `json:"chunks"` 28 | } 29 | 30 | type BlockHeaderView struct { 31 | Height types.BlockHeight `json:"height"` 32 | EpochID hash.CryptoHash `json:"epoch_id"` 33 | NextEpochID hash.CryptoHash `json:"next_epoch_id"` 34 | Hash hash.CryptoHash `json:"hash"` 35 | PrevHash hash.CryptoHash `json:"prev_hash"` 36 | PrevStateRoot hash.CryptoHash `json:"prev_state_root"` 37 | ChunkReceiptsRoot hash.CryptoHash `json:"chunk_receipts_root"` 38 | ChunkHeadersRoot hash.CryptoHash `json:"chunk_headers_root"` 39 | ChunkTxRoot hash.CryptoHash `json:"chunk_tx_root"` 40 | OutcomeRoot hash.CryptoHash `json:"outcome_root"` 41 | ChunksIncluded uint64 `json:"chunks_included"` 42 | ChallengesRoot hash.CryptoHash `json:"challenges_root"` 43 | Timestamp uint64 `json:"timestamp"` // milliseconds 44 | TimestampNanosec types.TimeNanos `json:"timestamp_nanosec"` // nanoseconds, uint128 45 | RandomValue hash.CryptoHash `json:"random_value"` 46 | ValidatorProposals []ValidatorStakeView `json:"validator_proposals"` 47 | ChunkMask []bool `json:"chunk_mask"` 48 | GasPrice types.Balance `json:"gas_price"` 49 | RentPaid types.Balance `json:"rent_paid"` // NOTE: deprecated - 2021-05-14 50 | ValidatorReward types.Balance `json:"validator_reward"` // NOTE: deprecated - 2021-05-14 51 | TotalSupply types.Balance `json:"total_supply"` 52 | ChallengesResult ChallengesResult `json:"challenges_result"` 53 | LastFinalBlock hash.CryptoHash `json:"last_final_block"` 54 | LastDSFinalBlock hash.CryptoHash `json:"last_ds_final_block"` 55 | NextBPHash hash.CryptoHash `json:"next_bp_hash"` 56 | BlockMerkleRoot hash.CryptoHash `json:"block_merkle_root"` 57 | Approvals []*signature.Base58Signature `json:"approvals"` 58 | Signature signature.Base58Signature `json:"signature"` 59 | LatestProtocolVersion uint64 `json:"latest_protocol_version"` 60 | } 61 | -------------------------------------------------------------------------------- /pkg/client/chunk.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/eteu-technologies/near-api-go/pkg/types/hash" 7 | ) 8 | 9 | // https://docs.near.org/docs/api/rpc#chunk-details 10 | func (c *Client) ChunkDetails(ctx context.Context, chunkHash hash.CryptoHash) (res ChunkView, err error) { 11 | _, err = c.doRPC(ctx, &res, "chunk", nil, []string{chunkHash.String()}) 12 | 13 | return 14 | } 15 | -------------------------------------------------------------------------------- /pkg/client/chunk_structs.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/eteu-technologies/near-api-go/pkg/types" 7 | "github.com/eteu-technologies/near-api-go/pkg/types/hash" 8 | "github.com/eteu-technologies/near-api-go/pkg/types/signature" 9 | ) 10 | 11 | type ChunkView struct { 12 | Author types.AccountID `json:"author"` 13 | Header ChunkHeaderView `json:"header"` 14 | Transactions []SignedTransactionView `json:"transactions"` 15 | Receipts []ReceiptView `json:"receipts"` 16 | } 17 | 18 | type ChunkHeaderView struct { 19 | ChunkHash hash.CryptoHash `json:"chunk_hash"` 20 | PrevBlockHash hash.CryptoHash `json:"prev_block_hash"` 21 | OutcomeRoot hash.CryptoHash `json:"outcome_root"` 22 | PrevStateRoot json.RawMessage `json:"prev_state_root"` // TODO: needs a type! 23 | EncodedMerkleRoot hash.CryptoHash `json:"encoded_merkle_root"` 24 | EncodedLength uint64 `json:"encoded_length"` 25 | HeightCreated types.BlockHeight `json:"height_created"` 26 | HeightIncluded types.BlockHeight `json:"height_included"` 27 | ShardID types.ShardID `json:"shard_id"` 28 | GasUsed types.Gas `json:"gas_used"` 29 | GasLimit types.Gas `json:"gas_limit"` 30 | RentPaid types.Balance `json:"rent_paid"` // TODO: deprecated 31 | ValidatorReward types.Balance `json:"validator_reward"` // TODO: deprecated 32 | BalanceBurnt types.Balance `json:"balance_burnt"` 33 | OutgoingReceiptsRoot hash.CryptoHash `json:"outgoing_receipts_root"` 34 | TxRoot hash.CryptoHash `json:"tx_root"` 35 | ValidatorProposals []ValidatorStakeView `json:"validator_proposals"` 36 | Signature signature.Base58Signature `json:"signature"` 37 | } 38 | -------------------------------------------------------------------------------- /pkg/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/eteu-technologies/near-api-go/pkg/client/block" 8 | "github.com/eteu-technologies/near-api-go/pkg/jsonrpc" 9 | ) 10 | 11 | type Client struct { 12 | RPCClient jsonrpc.Client 13 | } 14 | 15 | func NewClient(networkAddr string) (client Client, err error) { 16 | client.RPCClient, err = jsonrpc.NewClient(networkAddr) 17 | if err != nil { 18 | return 19 | } 20 | 21 | return 22 | } 23 | 24 | func NewClientWithOpts(opts ...jsonrpc.ClientOptFn) (client Client, err error) { 25 | client.RPCClient, err = jsonrpc.NewClientWithOpts(opts...) 26 | if err != nil { 27 | return 28 | } 29 | 30 | return 31 | } 32 | 33 | func (c *Client) NetworkAddr() string { 34 | return c.RPCClient.URL 35 | } 36 | 37 | func (c *Client) doRPC(ctx context.Context, result interface{}, method string, block block.BlockCharacteristic, params interface{}) (res jsonrpc.Response, err error) { 38 | if block != nil { 39 | if mapv, ok := params.(map[string]interface{}); ok { 40 | block(mapv) 41 | } 42 | } 43 | 44 | res, err = c.RPCClient.CallRPC(ctx, method, params) 45 | if err != nil { 46 | return 47 | } 48 | 49 | // If JSON-RPC error happens, conveniently set it as err to avoid duplicating code 50 | // XXX: using plain assignment makes `err != nil` true for some reason 51 | if err := res.Error; err != nil { 52 | return res, err 53 | } 54 | 55 | if result != nil { 56 | if err = json.Unmarshal(res.Result, result); err != nil { 57 | return 58 | } 59 | } 60 | 61 | return 62 | } 63 | -------------------------------------------------------------------------------- /pkg/client/common.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/eteu-technologies/near-api-go/pkg/types" 5 | "github.com/eteu-technologies/near-api-go/pkg/types/hash" 6 | ) 7 | 8 | type QueryResponse struct { 9 | BlockHeight types.BlockHeight `json:"block_height"` 10 | BlockHash hash.CryptoHash `json:"block_hash"` 11 | Error *string `json:"error"` 12 | Logs []interface{} `json:"logs"` // TODO: use correct type 13 | } 14 | -------------------------------------------------------------------------------- /pkg/client/context.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/eteu-technologies/near-api-go/pkg/types/key" 7 | ) 8 | 9 | type rpcContext int 10 | 11 | const ( 12 | clientCtx = rpcContext(iota) 13 | keyPairCtx 14 | ) 15 | 16 | func ContextWithKeyPair(ctx context.Context, keyPair key.KeyPair) context.Context { 17 | kp := keyPair 18 | return context.WithValue(ctx, keyPairCtx, &kp) 19 | } 20 | 21 | func getKeyPair(ctx context.Context) *key.KeyPair { 22 | v, ok := ctx.Value(keyPairCtx).(*key.KeyPair) 23 | if ok { 24 | return v 25 | } 26 | 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /pkg/client/contract.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/eteu-technologies/near-api-go/pkg/client/block" 7 | "github.com/eteu-technologies/near-api-go/pkg/jsonrpc" 8 | "github.com/eteu-technologies/near-api-go/pkg/types" 9 | ) 10 | 11 | // https://docs.near.org/docs/api/rpc#view-contract-state 12 | func (c *Client) ContractViewState(ctx context.Context, accountID types.AccountID, prefixBase64 string, block block.BlockCharacteristic) (res ViewStateResult, err error) { 13 | _, err = c.doRPC(ctx, &res, "query", block, map[string]interface{}{ 14 | "request_type": "view_state", 15 | "account_id": accountID, 16 | "prefix_base64": prefixBase64, 17 | }) 18 | 19 | return 20 | } 21 | 22 | // TODO: decode response 23 | // https://docs.near.org/docs/api/rpc#view-contract-state-changes 24 | func (c *Client) ContractViewStateChanges(ctx context.Context, accountIDs []types.AccountID, keyPrefixBase64 string, block block.BlockCharacteristic) (res jsonrpc.Response, err error) { 25 | res, err = c.doRPC(ctx, nil, "EXPERIMENTAL_changes", block, map[string]interface{}{ 26 | "changes_type": "data_changes", 27 | "account_ids": accountIDs, 28 | "key_prefix_base64": keyPrefixBase64, 29 | }) 30 | 31 | return 32 | } 33 | 34 | // TODO: decode response 35 | // https://docs.near.org/docs/api/rpc#view-contract-code-changes 36 | func (c *Client) ContractViewCodeChanges(ctx context.Context, accountIDs []types.AccountID, block block.BlockCharacteristic) (res jsonrpc.Response, err error) { 37 | res, err = c.doRPC(ctx, nil, "EXPERIMENTAL_changes", block, map[string]interface{}{ 38 | "changes_type": "contract_code_changes", 39 | "account_ids": accountIDs, 40 | }) 41 | 42 | return 43 | } 44 | 45 | // https://docs.near.org/docs/api/rpc#call-a-contract-function 46 | func (c *Client) ContractViewCallFunction(ctx context.Context, accountID, methodName, argsBase64 string, block block.BlockCharacteristic) (res CallResult, err error) { 47 | _, err = c.doRPC(ctx, &res, "query", block, map[string]interface{}{ 48 | "request_type": "call_function", 49 | "account_id": accountID, 50 | "method_name": methodName, 51 | "args_base64": argsBase64, 52 | }) 53 | 54 | return 55 | } 56 | -------------------------------------------------------------------------------- /pkg/client/contract_structs.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | type ViewStateResult struct { 4 | Values []StateItem `json:"values"` 5 | Proof TrieProofPath `json:"proof"` 6 | 7 | QueryResponse 8 | } 9 | 10 | type StateItem struct { 11 | Key string `json:"key"` 12 | Value string `json:"value"` 13 | Proof TrieProofPath `json:"proof"` 14 | } 15 | 16 | type CallResult struct { 17 | Result []byte `json:"result"` 18 | Logs []string `json:"logs"` 19 | 20 | QueryResponse 21 | } 22 | 23 | // TrieProofPath is a set of serialized TrieNodes that are encoded in base64. Represent proof of inclusion of some TrieNode in the MerkleTrie. 24 | type TrieProofPath = []string 25 | -------------------------------------------------------------------------------- /pkg/client/gas.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/eteu-technologies/near-api-go/pkg/client/block" 7 | ) 8 | 9 | // https://docs.near.org/docs/api/rpc#gas-price 10 | func (c *Client) GasPriceView(ctx context.Context, block block.BlockCharacteristic) (res GasPrice, err error) { 11 | _, err = c.doRPC(ctx, &res, "gas_price", nil, blockIDArrayParams(block)) 12 | 13 | return 14 | } 15 | -------------------------------------------------------------------------------- /pkg/client/gas_structs.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import "github.com/eteu-technologies/near-api-go/pkg/types" 4 | 5 | type GasPrice struct { 6 | GasPrice types.Balance `json:"gas_price"` 7 | } 8 | -------------------------------------------------------------------------------- /pkg/client/genesis.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // https://docs.near.org/docs/api/rpc#genesis-config 8 | func (c *Client) GenesisConfig(ctx context.Context) (res map[string]interface{}, err error) { 9 | _, err = c.doRPC(ctx, &res, "EXPERIMENTAL_genesis_config", nil, nil) 10 | 11 | return 12 | } 13 | -------------------------------------------------------------------------------- /pkg/client/network.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/eteu-technologies/near-api-go/pkg/client/block" 7 | ) 8 | 9 | // https://docs.near.org/docs/api/rpc#network-info 10 | func (c *Client) NetworkInfo(ctx context.Context) (res NetworkInfo, err error) { 11 | _, err = c.doRPC(ctx, &res, "network_info", nil, []string{}) 12 | 13 | return 14 | } 15 | 16 | // https://docs.near.org/docs/api/rpc#general-validator-status 17 | func (c *Client) NetworkStatusValidators(ctx context.Context) (res StatusResponse, err error) { 18 | _, err = c.doRPC(ctx, &res, "status", nil, []string{}) 19 | 20 | return 21 | } 22 | 23 | // https://docs.near.org/docs/api/rpc#detailed-validator-status 24 | func (c *Client) NetworkStatusValidatorsDetailed(ctx context.Context, block block.BlockCharacteristic) (res ValidatorsResponse, err error) { 25 | _, err = c.doRPC(ctx, nil, "validators", nil, blockIDArrayParams(block)) 26 | 27 | return 28 | } 29 | -------------------------------------------------------------------------------- /pkg/client/network_structs.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "github.com/eteu-technologies/near-api-go/pkg/types" 8 | "github.com/eteu-technologies/near-api-go/pkg/types/hash" 9 | "github.com/eteu-technologies/near-api-go/pkg/types/key" 10 | "github.com/eteu-technologies/near-api-go/pkg/types/signature" 11 | ) 12 | 13 | // NetworkInfo holds network information 14 | type NetworkInfo struct { 15 | ActivePeers []FullPeerInfo `json:"active_peers"` 16 | NumActivePeers uint `json:"num_active_peers"` 17 | PeerMaxCount uint32 `json:"peer_max_count"` 18 | HighestHeightPeers []FullPeerInfo `json:"highest_height_peers"` 19 | SentBytesPerSec uint64 `json:"sent_bytes_per_sec"` 20 | ReceivedBytesPerSec uint64 `json:"received_bytes_per_sec"` 21 | KnownProducers []KnownProducer `json:"known_producers"` 22 | MetricRecorder MetricRecorder `json:"metric_recorder"` 23 | PeerCounter uint `json:"peer_counter"` 24 | } 25 | 26 | type FullPeerInfo struct { 27 | PeerInfo PeerInfo `json:"peer_info"` 28 | ChainInfo PeerChainInfo `json:"chain_info"` 29 | EdgeInfo EdgeInfo `json:"edge_info"` 30 | } 31 | 32 | // PeerInfo holds peer information 33 | type PeerInfo struct { 34 | ID key.PeerID `json:"id"` 35 | Addr *string `json:"addr"` 36 | AccountID *types.AccountID `json:"account_id"` 37 | } 38 | 39 | // PeerChainInfo contains peer chain information. This is derived from PeerCHainInfoV2 in nearcore 40 | type PeerChainInfo struct { 41 | // Chain Id and hash of genesis block. 42 | GenesisID GenesisID `json:"genesis_id"` 43 | // Last known chain height of the peer. 44 | Height types.BlockHeight `json:"height"` 45 | // Shards that the peer is tracking. 46 | TrackedShards []types.ShardID `json:"tracked_shards"` 47 | // Denote if a node is running in archival mode or not. 48 | Archival bool `json:"archival"` 49 | } 50 | 51 | // EdgeInfo contains information that will be ultimately used to create a new edge. It contains nonce proposed for the edge with signature from peer. 52 | type EdgeInfo struct { 53 | Nonce types.Nonce `json:"nonce"` 54 | Signature signature.Signature `json:"signature"` 55 | } 56 | 57 | // KnownProducer is basically PeerInfo, but AccountID is known 58 | type KnownProducer struct { 59 | AccountID types.AccountID `json:"account_id"` 60 | Addr *string `json:"addr"` 61 | PeerID key.PeerID `json:"peer_id"` 62 | } 63 | 64 | // TODO: chain/network/src/recorder.rs 65 | type MetricRecorder = json.RawMessage 66 | 67 | type GenesisID struct { 68 | // Chain Id 69 | ChainID string `json:"chain_id"` 70 | // Hash of genesis block 71 | Hash hash.CryptoHash `json:"hash"` 72 | } 73 | 74 | type StatusResponse struct { 75 | // Binary version 76 | Version NodeVersion `json:"version"` 77 | // Unique chain id. 78 | ChainID string `json:"chain_id"` 79 | // Currently active protocol version. 80 | ProtocolVersion uint32 `json:"protocol_version"` 81 | // Latest protocol version that this client supports. 82 | LatestProtocolVersion uint32 `json:"latest_protocol_version"` 83 | // Address for RPC server. 84 | RPCAddr string `json:"rpc_addr"` 85 | // Current epoch validators. 86 | Validators []ValidatorInfo `json:"validators"` 87 | // Sync status of the node. 88 | SyncInfo StatusSyncInfo `json:"sync_info"` 89 | // Validator id of the node 90 | ValidatorAccountID *types.AccountID `json:"validator_account_id"` 91 | } 92 | 93 | type NodeVersion struct { 94 | Version string `json:"version"` 95 | Build string `json:"build"` 96 | } 97 | 98 | type ValidatorInfo struct { 99 | AccountID types.AccountID `json:"account_id"` 100 | Slashed bool `json:"is_slashed"` 101 | } 102 | 103 | type StatusSyncInfo struct { 104 | LatestBlockHash hash.CryptoHash `json:"latest_block_hash"` 105 | LatestBlockHeight types.BlockHeight `json:"latest_block_height"` 106 | LatestBlockTime time.Time `json:"latest_block_time"` 107 | Syncing bool `json:"syncing"` 108 | } 109 | 110 | type ValidatorsResponse struct { 111 | CurrentValidators []CurrentEpochValidatorInfo `json:"current_validator"` 112 | } 113 | 114 | type CurrentEpochValidatorInfo struct { 115 | ValidatorInfo 116 | PublicKey key.Base58PublicKey `json:"public_key"` 117 | Stake types.Balance `json:"stake"` 118 | Shards []types.ShardID `json:"shards"` 119 | NumProducedBlocks types.NumBlocks `json:"num_produced_blocks"` 120 | NumExpectedBlocks types.NumBlocks `json:"num_expected_blocks"` 121 | } 122 | -------------------------------------------------------------------------------- /pkg/client/protocol.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/eteu-technologies/near-api-go/pkg/client/block" 7 | ) 8 | 9 | // https://docs.near.org/api/rpc/protocol#protocol-config 10 | func (c *Client) ProtocolConfig(ctx context.Context, block block.BlockCharacteristic) (res map[string]interface{}, err error) { 11 | _, err = c.doRPC(ctx, &res, "EXPERIMENTAL_protocol_config", block, map[string]interface{}{}) 12 | 13 | return 14 | } 15 | -------------------------------------------------------------------------------- /pkg/client/transaction.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/eteu-technologies/near-api-go/pkg/types" 7 | "github.com/eteu-technologies/near-api-go/pkg/types/hash" 8 | ) 9 | 10 | // https://docs.near.org/docs/api/rpc#send-transaction-async 11 | func (c *Client) RPCTransactionSend(ctx context.Context, signedTxnBase64 string) (resp hash.CryptoHash, err error) { 12 | _, err = c.doRPC(ctx, &resp, "broadcast_tx_async", nil, []string{signedTxnBase64}) 13 | 14 | return 15 | } 16 | 17 | // https://docs.near.org/docs/api/rpc#send-transaction-await 18 | func (c *Client) RPCTransactionSendAwait(ctx context.Context, signedTxnBase64 string) (resp FinalExecutionOutcomeView, err error) { 19 | _, err = c.doRPC(ctx, &resp, "broadcast_tx_commit", nil, []string{signedTxnBase64}) 20 | 21 | return 22 | } 23 | 24 | // https://docs.near.org/docs/api/rpc#transaction-status 25 | func (c *Client) TransactionStatus(ctx context.Context, tx hash.CryptoHash, sender types.AccountID) (resp FinalExecutionOutcomeView, err error) { 26 | _, err = c.doRPC(ctx, &resp, "tx", nil, []string{ 27 | tx.String(), sender, 28 | }) 29 | 30 | return 31 | } 32 | 33 | // https://docs.near.org/docs/api/rpc#transaction-status-with-receipts 34 | func (c *Client) TransactionStatusWithReceipts(ctx context.Context, tx hash.CryptoHash, sender types.AccountID) (resp FinalExecutionOutcomeWithReceiptView, err error) { 35 | _, err = c.doRPC(ctx, &resp, "EXPERIMENTAL_tx_status", nil, []string{ 36 | tx.String(), sender, 37 | }) 38 | 39 | return 40 | } 41 | -------------------------------------------------------------------------------- /pkg/client/transaction_structs.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/eteu-technologies/near-api-go/pkg/types" 7 | "github.com/eteu-technologies/near-api-go/pkg/types/action" 8 | "github.com/eteu-technologies/near-api-go/pkg/types/hash" 9 | "github.com/eteu-technologies/near-api-go/pkg/types/key" 10 | "github.com/eteu-technologies/near-api-go/pkg/types/signature" 11 | ) 12 | 13 | type TransactionStatus struct { 14 | SuccessValue string `json:"SuccessValue"` 15 | SuccessReceiptID string `json:"SuccessReceiptId"` 16 | Failure json.RawMessage `json:"Failure"` // TODO 17 | } 18 | 19 | type SignedTransactionView struct { 20 | SignerID types.AccountID `json:"signer_id"` 21 | PublicKey key.Base58PublicKey `json:"public_key"` 22 | Nonce types.Nonce `json:"nonce"` 23 | ReceiverID types.AccountID `json:"receiver_id"` 24 | Actions []action.Action `json:"actions"` 25 | Signature signature.Base58Signature `json:"signature"` 26 | Hash hash.CryptoHash `json:"hash"` 27 | } 28 | 29 | type FinalExecutionOutcomeView struct { 30 | Status TransactionStatus `json:"status"` 31 | Transaction SignedTransactionView `json:"transaction"` 32 | TransactionOutcome ExecutionOutcomeWithIdView `json:"transaction_outcome"` 33 | ReceiptsOutcome []ExecutionOutcomeWithIdView `json:"receipts_outcome"` 34 | } 35 | 36 | type FinalExecutionOutcomeWithReceiptView struct { 37 | FinalExecutionOutcomeView 38 | Receipts []ReceiptView `json:"receipts"` 39 | } 40 | 41 | type ReceiptView struct { 42 | PredecessorID types.AccountID `json:"predecessor_id"` 43 | ReceiverID types.AccountID `json:"receiver_id"` 44 | ReceiptID hash.CryptoHash `json:"receipt_id"` 45 | Receipt json.RawMessage `json:"receipt"` // TODO: needs a type! 46 | } 47 | 48 | type ExecutionOutcomeView struct { 49 | Logs []string `json:"logs"` 50 | ReceiptIDs []hash.CryptoHash `json:"receipt_ids"` 51 | GasBurnt types.Gas `json:"gas_burnt"` 52 | TokensBurnt types.Balance `json:"tokens_burnt"` 53 | ExecutorID types.AccountID `json:"executor_id"` 54 | Status TransactionStatus `json:"status"` 55 | } 56 | 57 | type MerklePathItem struct { 58 | Hash hash.CryptoHash `json:"hash"` 59 | Direction string `json:"direction"` // TODO: enum type, either 'Left' or 'Right' 60 | } 61 | 62 | type MerklePath = []MerklePathItem 63 | 64 | type ExecutionOutcomeWithIdView struct { 65 | Proof MerklePath `json:"proof"` 66 | BlockHash hash.CryptoHash `json:"block_hash"` 67 | ID hash.CryptoHash `json:"id"` 68 | Outcome ExecutionOutcomeView `json:"outcome"` 69 | } 70 | -------------------------------------------------------------------------------- /pkg/client/transaction_wrapper.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/eteu-technologies/near-api-go/pkg/client/block" 8 | "github.com/eteu-technologies/near-api-go/pkg/types" 9 | "github.com/eteu-technologies/near-api-go/pkg/types/action" 10 | "github.com/eteu-technologies/near-api-go/pkg/types/hash" 11 | "github.com/eteu-technologies/near-api-go/pkg/types/key" 12 | "github.com/eteu-technologies/near-api-go/pkg/types/transaction" 13 | ) 14 | 15 | type transactionCtx struct { 16 | txn transaction.Transaction 17 | keyPair *key.KeyPair 18 | keyNonceSet bool 19 | } 20 | 21 | type TransactionOpt func(context.Context, *transactionCtx) error 22 | 23 | func (c *Client) PrepareTransaction(ctx context.Context, from, to types.AccountID, actions []action.Action, txnOpts ...TransactionOpt) (ctx2 context.Context, blob string, err error) { 24 | ctx2 = context.WithValue(ctx, clientCtx, c) 25 | txn := transaction.Transaction{ 26 | SignerID: from, 27 | ReceiverID: to, 28 | Actions: actions, 29 | } 30 | txnCtx := transactionCtx{ 31 | txn: txn, 32 | keyPair: getKeyPair(ctx2), 33 | keyNonceSet: false, 34 | } 35 | 36 | for _, opt := range txnOpts { 37 | if err = opt(ctx2, &txnCtx); err != nil { 38 | return 39 | } 40 | } 41 | 42 | if txnCtx.keyPair == nil { 43 | err = errors.New("no keypair specified") 44 | return 45 | } 46 | 47 | txnCtx.txn.PublicKey = txnCtx.keyPair.PublicKey.ToPublicKey() 48 | 49 | // Query the access key nonce, if not specified 50 | if !txnCtx.keyNonceSet { 51 | var accessKey AccessKeyView 52 | accessKey, err = c.AccessKeyView(ctx2, txnCtx.txn.SignerID, txnCtx.keyPair.PublicKey, block.FinalityFinal()) 53 | if err != nil { 54 | return 55 | } 56 | 57 | nonce := accessKey.Nonce 58 | 59 | // Increment nonce by 1 60 | txnCtx.txn.Nonce = nonce + 1 61 | txnCtx.keyNonceSet = true 62 | } 63 | 64 | blob, err = transaction.SignAndSerializeTransaction(*txnCtx.keyPair, txnCtx.txn) 65 | return 66 | } 67 | 68 | // https://docs.near.org/docs/api/rpc#send-transaction-async 69 | func (c *Client) TransactionSend(ctx context.Context, from, to types.AccountID, actions []action.Action, txnOpts ...TransactionOpt) (res hash.CryptoHash, err error) { 70 | ctx2, blob, err := c.PrepareTransaction(ctx, from, to, actions, txnOpts...) 71 | if err != nil { 72 | return 73 | } 74 | return c.RPCTransactionSend(ctx2, blob) 75 | } 76 | 77 | // https://docs.near.org/docs/api/rpc#send-transaction-await 78 | func (c *Client) TransactionSendAwait(ctx context.Context, from, to types.AccountID, actions []action.Action, txnOpts ...TransactionOpt) (res FinalExecutionOutcomeView, err error) { 79 | ctx2, blob, err := c.PrepareTransaction(ctx, from, to, actions, txnOpts...) 80 | if err != nil { 81 | return 82 | } 83 | return c.RPCTransactionSendAwait(ctx2, blob) 84 | } 85 | 86 | func WithBlockCharacteristic(block block.BlockCharacteristic) TransactionOpt { 87 | return func(ctx context.Context, txnCtx *transactionCtx) (err error) { 88 | client := ctx.Value(clientCtx).(*Client) 89 | 90 | var res BlockView 91 | if res, err = client.BlockDetails(ctx, block); err != nil { 92 | return 93 | } 94 | 95 | txnCtx.txn.BlockHash = res.Header.Hash 96 | return 97 | } 98 | 99 | } 100 | 101 | // WithBlockHash sets block hash to attach this transaction to 102 | func WithBlockHash(hash hash.CryptoHash) TransactionOpt { 103 | return func(_ context.Context, txnCtx *transactionCtx) (err error) { 104 | txnCtx.txn.BlockHash = hash 105 | return 106 | } 107 | } 108 | 109 | // WithLatestBlock is alias to `WithBlockCharacteristic(block.FinalityFinal())` 110 | func WithLatestBlock() TransactionOpt { 111 | return WithBlockCharacteristic(block.FinalityFinal()) 112 | } 113 | 114 | // WithKeyPair sets key pair to use sign this transaction with 115 | func WithKeyPair(keyPair key.KeyPair) TransactionOpt { 116 | return func(_ context.Context, txnCtx *transactionCtx) (err error) { 117 | kp := keyPair 118 | txnCtx.keyPair = &kp 119 | return 120 | } 121 | } 122 | 123 | // WithKeyNonce sets key nonce to use with this transaction. If not set via this function, a RPC query will be done to query current nonce and 124 | // (nonce+1) will be used 125 | func WithKeyNonce(nonce types.Nonce) TransactionOpt { 126 | return func(_ context.Context, txnCtx *transactionCtx) (err error) { 127 | txnCtx.txn.Nonce = nonce 128 | txnCtx.keyNonceSet = true 129 | return 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /pkg/client/utils.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import "github.com/eteu-technologies/near-api-go/pkg/client/block" 4 | 5 | // HACK 6 | func blockIDArrayParams(block block.BlockCharacteristic) []interface{} { 7 | params := []interface{}{nil} 8 | p := map[string]interface{}{} 9 | 10 | block(p) 11 | if v, ok := p["block_id"]; ok { 12 | params[0] = v 13 | } 14 | 15 | return params 16 | } 17 | -------------------------------------------------------------------------------- /pkg/config/network.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "fmt" 4 | 5 | type NetworkInfo struct { 6 | NetworkID string 7 | NodeURL string 8 | WalletURL string 9 | HelperURL string 10 | ExplorerURL string 11 | 12 | archive string 13 | nonArchive string 14 | } 15 | 16 | func (n NetworkInfo) Archival() (ni NetworkInfo, ok bool) { 17 | ni, ok = Networks[n.archive] 18 | return 19 | } 20 | 21 | func (n NetworkInfo) NonArchival() (ni NetworkInfo, ok bool) { 22 | ni, ok = Networks[n.nonArchive] 23 | return 24 | } 25 | 26 | func (n NetworkInfo) IsArchival() bool { 27 | return n.nonArchive != "" 28 | } 29 | 30 | func buildNetworkConfig(networkID string, hasArchival bool) (ni NetworkInfo) { 31 | ni.NetworkID = networkID 32 | ni.NodeURL = fmt.Sprintf("https://rpc.%s.near.org", networkID) 33 | ni.WalletURL = fmt.Sprintf("https://wallet.%s.near.org", networkID) 34 | ni.HelperURL = fmt.Sprintf("https://helper.%s.near.org", networkID) 35 | ni.ExplorerURL = fmt.Sprintf("https://explorer.%s.near.org", networkID) 36 | if hasArchival { 37 | ni.archive = fmt.Sprintf("archival-%s", networkID) 38 | } 39 | return 40 | } 41 | 42 | func buildArchivalNetworkConfig(networkID string) (ni NetworkInfo) { 43 | ni = buildNetworkConfig(networkID, false) 44 | ni.NetworkID = fmt.Sprintf("archival-%s", networkID) 45 | ni.NodeURL = fmt.Sprintf("https://archival-rpc.%s.near.org", networkID) 46 | ni.nonArchive = networkID 47 | return 48 | } 49 | 50 | var Networks = map[string]NetworkInfo{ 51 | "mainnet": buildNetworkConfig("mainnet", true), 52 | "testnet": buildNetworkConfig("testnet", true), 53 | "betanet": buildNetworkConfig("betanet", false), 54 | "local": { 55 | NetworkID: "local", 56 | NodeURL: "http://127.0.0.1:3030", 57 | }, 58 | // From https://docs.near.org/docs/api/rpc#setup: 59 | // > Querying historical data (older than 5 epochs or ~2.5 days), you may get responses that the data is not available anymore. 60 | // > In that case, archival RPC nodes will come to your rescue 61 | "archival-mainnet": buildArchivalNetworkConfig("mainnet"), 62 | "archival-testnet": buildArchivalNetworkConfig("testnet"), 63 | } 64 | -------------------------------------------------------------------------------- /pkg/config/network_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/eteu-technologies/near-api-go/pkg/config" 7 | ) 8 | 9 | func TestArchivalConfigLink(t *testing.T) { 10 | amni, ok := config.Networks["mainnet"].Archival() 11 | if !ok { 12 | t.Fatal("mainnet should have archival link") 13 | } 14 | 15 | mni, ok := amni.NonArchival() 16 | if !ok { 17 | t.Fatal("archival-mainnet should have non-archival link") 18 | } 19 | 20 | atni, ok := config.Networks["testnet"].Archival() 21 | if !ok { 22 | t.Fatal("testnet should have archival link") 23 | } 24 | 25 | tni, ok := atni.NonArchival() 26 | if !ok { 27 | t.Fatal("archival-testnet should have non-archival link") 28 | } 29 | 30 | _ = mni 31 | _ = tni 32 | } 33 | -------------------------------------------------------------------------------- /pkg/jsonrpc/client.go: -------------------------------------------------------------------------------- 1 | package jsonrpc 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "net/http" 10 | "sync/atomic" 11 | ) 12 | 13 | const JSONRPCVersion = "2.0" 14 | 15 | type Client struct { 16 | URL string 17 | 18 | client *http.Client 19 | nextReqId uint64 20 | } 21 | 22 | func NewClient(networkAddr string) (client Client, err error) { 23 | return NewClientWithOpts( 24 | WithNetworkAddr(networkAddr), 25 | WithHTTPClient(new(http.Client)), 26 | ) 27 | } 28 | 29 | func NewClientWithOpts(opts ...ClientOptFn) (client Client, err error) { 30 | var co ClientOpts 31 | for idx, fn := range opts { 32 | if err = fn(&co); err != nil { 33 | err = fmt.Errorf("client option at index %d failed: %w", idx, err) 34 | return 35 | } 36 | } 37 | 38 | if client.client = co.HTTPClient; client.client == nil { 39 | client.client = http.DefaultClient 40 | } 41 | 42 | if client.URL = co.NetworkAddr; client.URL == "" { 43 | err = fmt.Errorf("network address is not set") 44 | return 45 | } 46 | 47 | atomic.StoreUint64(&client.nextReqId, 0) 48 | 49 | return 50 | } 51 | 52 | func (c *Client) nextId() uint64 { 53 | return atomic.AddUint64(&c.nextReqId, 1) 54 | } 55 | 56 | func (c *Client) CallRPC(ctx context.Context, method string, params interface{}) (res Response, err error) { 57 | reqId := fmt.Sprintf("%d", c.nextId()) 58 | body, err := json.Marshal(Request{ 59 | JSONRPC{JSONRPCVersion, reqId, method}, 60 | params, 61 | }) 62 | if err != nil { 63 | return 64 | } 65 | 66 | request, err := http.NewRequestWithContext(ctx, http.MethodPost, c.URL, bytes.NewBuffer(body)) 67 | if err != nil { 68 | return 69 | } 70 | 71 | request.Header.Add("Content-Type", "application/json") 72 | 73 | response, err := c.client.Do(request) 74 | if err != nil { 75 | return 76 | } 77 | 78 | return parseRPCBody(response) 79 | } 80 | 81 | func parseRPCBody(r *http.Response) (res Response, err error) { 82 | //fmt.Printf("%#v\n", r) 83 | 84 | body := r.Body 85 | if body == nil { 86 | err = errors.New("nil body") 87 | return 88 | } 89 | defer func() { _ = body.Close() }() 90 | 91 | // TODO: check for Content-Type header 92 | decoder := json.NewDecoder(body) 93 | decoder.DisallowUnknownFields() 94 | 95 | if err = decoder.Decode(&res); err != nil { 96 | return 97 | } 98 | 99 | return 100 | } 101 | -------------------------------------------------------------------------------- /pkg/jsonrpc/error.go: -------------------------------------------------------------------------------- 1 | package jsonrpc 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strconv" 7 | ) 8 | 9 | const ( 10 | CodeParseError = -32700 11 | CodeInvalidRequest = -32600 12 | CodeMethodNotFound = -32601 13 | CodeInvalidParams = -32602 14 | CodeInternalError = -32603 15 | 16 | CodeServerErrorRangeStart = -32099 17 | CodeServerErrorRangeEnd = -32000 18 | ) 19 | 20 | type Error struct { 21 | Name string `json:"name"` 22 | Cause ErrorCause `json:"cause"` 23 | 24 | // Legacy - do not rely on them 25 | Code int `json:"code"` 26 | Message string `json:"message"` 27 | Data json.RawMessage `json:"data"` 28 | } 29 | 30 | func (err *Error) Error() string { 31 | return fmt.Sprintf("RPC error %s (%s)", err.Name, err.Cause.String()) 32 | } 33 | 34 | type ErrorCause struct { 35 | Name string `json:"name"` 36 | Info json.RawMessage `json:"info"` 37 | 38 | message *ErrorCauseMessage 39 | } 40 | 41 | type ErrorCauseMessage struct { 42 | ErrorMessage string `json:"error_message"` 43 | } 44 | 45 | func (cause *ErrorCause) UnmarshalJSON(b []byte) (err error) { 46 | var data struct { 47 | Name string `json:"name"` 48 | Info json.RawMessage `json:"info"` 49 | } 50 | 51 | if err = json.Unmarshal(b, &data); err != nil { 52 | err = fmt.Errorf("unable to unmarshal error cause: %w", err) 53 | return 54 | } 55 | 56 | var info map[string]interface{} 57 | if err = json.Unmarshal(data.Info, &info); err != nil { 58 | err = fmt.Errorf("unable to unmarshal error cause info: %w", err) 59 | return 60 | } 61 | 62 | var message *ErrorCauseMessage 63 | if v, ok := info["error_message"]; ok { 64 | message = &ErrorCauseMessage{ 65 | ErrorMessage: v.(string), 66 | } 67 | } 68 | 69 | *cause = ErrorCause{ 70 | Name: data.Name, 71 | Info: data.Info, 72 | message: message, 73 | } 74 | 75 | return 76 | } 77 | 78 | func (cause ErrorCause) String() string { 79 | if cause.message != nil { 80 | return fmt.Sprintf("name=%s, message=%s", cause.Name, cause.message.ErrorMessage) 81 | } 82 | return fmt.Sprintf("name=%s, info=%s", cause.Name, strconv.Quote(string(cause.Info))) 83 | } 84 | -------------------------------------------------------------------------------- /pkg/jsonrpc/opts.go: -------------------------------------------------------------------------------- 1 | package jsonrpc 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | ) 7 | 8 | type ClientOpts struct { 9 | NetworkAddr string 10 | HTTPClient *http.Client 11 | } 12 | 13 | type ClientOptFn func(*ClientOpts) error 14 | 15 | func WithNetworkAddr(networkAddr string) ClientOptFn { 16 | return func(co *ClientOpts) (err error) { 17 | if _, err = url.Parse(networkAddr); err != nil { 18 | return 19 | } 20 | 21 | co.NetworkAddr = networkAddr 22 | return 23 | } 24 | } 25 | 26 | func WithHTTPClient(client *http.Client) ClientOptFn { 27 | return func(co *ClientOpts) (err error) { 28 | co.HTTPClient = client 29 | return 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pkg/jsonrpc/structs.go: -------------------------------------------------------------------------------- 1 | package jsonrpc 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type JSONRPC struct { 8 | JSONRPC string `json:"jsonrpc"` 9 | ID string `json:"id"` 10 | Method string `json:"method"` 11 | } 12 | 13 | type Request struct { 14 | JSONRPC 15 | Params interface{} `json:"params,omitempty"` 16 | } 17 | 18 | type Response struct { 19 | JSONRPC 20 | Error *Error `json:"error"` 21 | Result json.RawMessage `json:"result"` 22 | } 23 | -------------------------------------------------------------------------------- /pkg/types/action/access_key.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/eteu-technologies/borsh-go" 8 | "github.com/eteu-technologies/near-api-go/pkg/types" 9 | ) 10 | 11 | type AccessKeyPermission struct { 12 | Enum borsh.Enum `borsh_enum:"true"` 13 | 14 | FunctionCallPermission AccessKeyFunctionCallPermission 15 | FullAccessPermission struct{} 16 | } 17 | 18 | type fullAccessPermissionWrapper struct { 19 | FunctionCall AccessKeyFunctionCallPermission `json:"FunctionCall"` 20 | } 21 | 22 | func NewFunctionCallPermission(allowance types.Balance, receiverID types.AccountID, methodNames []string) AccessKeyPermission { 23 | return AccessKeyPermission{ 24 | Enum: borsh.Enum(0), 25 | FunctionCallPermission: AccessKeyFunctionCallPermission{ 26 | Allowance: &allowance, 27 | ReceiverID: receiverID, 28 | MethodNames: methodNames, 29 | }, 30 | } 31 | } 32 | 33 | func NewFunctionCallUnlimitedAllowancePermission(receiverID types.AccountID, methodNames []string) AccessKeyPermission { 34 | return AccessKeyPermission{ 35 | Enum: borsh.Enum(0), 36 | FunctionCallPermission: AccessKeyFunctionCallPermission{ 37 | Allowance: nil, 38 | ReceiverID: receiverID, 39 | MethodNames: methodNames, 40 | }, 41 | } 42 | } 43 | 44 | func NewFullAccessPermission() AccessKeyPermission { 45 | return AccessKeyPermission{ 46 | Enum: borsh.Enum(1), 47 | } 48 | } 49 | 50 | func (a AccessKeyPermission) MarshalJSON() (b []byte, err error) { 51 | if a.IsFullAccess() { 52 | b = []byte(`"FullAccess"`) 53 | return 54 | } 55 | 56 | var v fullAccessPermissionWrapper 57 | v.FunctionCall = a.FunctionCallPermission 58 | 59 | b, err = json.Marshal(&v) 60 | return 61 | } 62 | 63 | func (a *AccessKeyPermission) UnmarshalJSON(b []byte) (err error) { 64 | if len(b) > 0 && b[0] == '{' { 65 | var permission fullAccessPermissionWrapper 66 | if err = json.Unmarshal(b, &permission); err != nil { 67 | return 68 | } 69 | 70 | *a = AccessKeyPermission{ 71 | Enum: borsh.Enum(0), 72 | FunctionCallPermission: permission.FunctionCall, 73 | } 74 | return 75 | } 76 | 77 | var value string 78 | if err = json.Unmarshal(b, &value); err != nil { 79 | return 80 | } 81 | 82 | if value == "FullAccess" { 83 | *a = NewFullAccessPermission() 84 | return 85 | } 86 | 87 | err = fmt.Errorf("unknown permission '%s'", value) 88 | return 89 | } 90 | 91 | func (a AccessKeyPermission) String() string { 92 | var value string = "FullAccess" 93 | if a.IsFunctionCall() { 94 | value = a.FunctionCallPermission.String() 95 | } 96 | return fmt.Sprintf("AccessKeyPermission{%s}", value) 97 | } 98 | 99 | func (a *AccessKeyPermission) IsFunctionCall() bool { 100 | return a.Enum == 0 101 | } 102 | 103 | func (a *AccessKeyPermission) IsFullAccess() bool { 104 | return a.Enum == 1 105 | } 106 | 107 | type AccessKeyFunctionCallPermission struct { 108 | Allowance *types.Balance `json:"allowance"` 109 | ReceiverID types.AccountID `json:"receiver_id"` 110 | MethodNames []string `json:"method_names"` 111 | } 112 | 113 | func (a AccessKeyFunctionCallPermission) String() string { 114 | return fmt.Sprintf("AccessKeyFunctionCallPermission{Allowance=%v, ReceiverID=%v, MethodNames=%v}", a.Allowance, a.ReceiverID, a.MethodNames) 115 | } 116 | -------------------------------------------------------------------------------- /pkg/types/action/action.go: -------------------------------------------------------------------------------- 1 | package action 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/eteu-technologies/borsh-go" 8 | uint128 "github.com/eteu-technologies/golang-uint128" 9 | 10 | "github.com/eteu-technologies/near-api-go/pkg/types" 11 | "github.com/eteu-technologies/near-api-go/pkg/types/key" 12 | ) 13 | 14 | type Action struct { 15 | Enum borsh.Enum `borsh_enum:"true"` 16 | 17 | CreateAccount ActionCreateAccount 18 | DeployContract ActionDeployContract 19 | FunctionCall ActionFunctionCall 20 | Transfer ActionTransfer 21 | Stake ActionStake 22 | AddKey ActionAddKey 23 | DeleteKey ActionDeleteKey 24 | DeleteAccount ActionDeleteAccount 25 | } 26 | 27 | const ( 28 | ordCreateAccount uint8 = iota 29 | ordDeployContract 30 | ordFunctionCall 31 | ordTransfer 32 | ordStake 33 | ordAddKey 34 | ordDeleteKey 35 | ordDeleteAccount 36 | ) 37 | 38 | var ( 39 | ordMappings = map[string]uint8{ 40 | "CreateAccount": ordCreateAccount, 41 | "DeployContract": ordDeployContract, 42 | "FunctionCall": ordFunctionCall, 43 | "Transfer": ordTransfer, 44 | "Stake": ordStake, 45 | "AddKey": ordAddKey, 46 | "DeleteKey": ordDeleteKey, 47 | "DeleteAccount": ordDeleteAccount, 48 | } 49 | 50 | simpleActions = map[string]bool{ 51 | "CreateAccount": true, 52 | } 53 | ) 54 | 55 | func (a *Action) PrepaidGas() types.Gas { 56 | switch uint8(a.Enum) { 57 | case ordFunctionCall: 58 | return a.FunctionCall.Gas 59 | default: 60 | return 0 61 | } 62 | } 63 | 64 | func (a *Action) DepositBalance() types.Balance { 65 | switch uint8(a.Enum) { 66 | case ordFunctionCall: 67 | return a.FunctionCall.Deposit 68 | case ordTransfer: 69 | return a.Transfer.Deposit 70 | default: 71 | return types.Balance(uint128.Zero) 72 | } 73 | } 74 | 75 | func (a *Action) UnderlyingValue() interface{} { 76 | switch uint8(a.Enum) { 77 | case ordCreateAccount: 78 | return &a.CreateAccount 79 | case ordDeployContract: 80 | return &a.DeployContract 81 | case ordFunctionCall: 82 | return &a.FunctionCall 83 | case ordTransfer: 84 | return &a.Transfer 85 | case ordStake: 86 | return &a.Stake 87 | case ordAddKey: 88 | return &a.AddKey 89 | case ordDeleteKey: 90 | return &a.DeleteKey 91 | case ordDeleteAccount: 92 | return &a.DeleteAccount 93 | } 94 | 95 | panic("unreachable") 96 | } 97 | 98 | func (a Action) String() string { 99 | ul := a.UnderlyingValue() 100 | if u, ok := ul.(interface{ String() string }); ok { 101 | return fmt.Sprintf("Action{%s}", u.String()) 102 | } 103 | 104 | return fmt.Sprintf("Action{%#v}", ul) 105 | } 106 | 107 | func (a *Action) UnmarshalJSON(b []byte) (err error) { 108 | var obj map[string]json.RawMessage 109 | 110 | // actions can be either strings, or objects, so try deserializing into string first 111 | var actionType string 112 | if len(b) > 0 && b[0] == '"' { 113 | if err = json.Unmarshal(b, &actionType); err != nil { 114 | return 115 | } 116 | 117 | if _, ok := simpleActions[actionType]; !ok { 118 | err = fmt.Errorf("Action '%s' had no body", actionType) 119 | return 120 | } 121 | 122 | obj = map[string]json.RawMessage{ 123 | actionType: json.RawMessage(`{}`), 124 | } 125 | } else { 126 | if err = json.Unmarshal(b, &obj); err != nil { 127 | return 128 | } 129 | } 130 | 131 | if l := len(obj); l > 1 { 132 | err = fmt.Errorf("action object contains invalid amount of keys (expected: 1, got: %d)", l) 133 | return 134 | } 135 | 136 | for k := range obj { 137 | actionType = k 138 | break 139 | } 140 | 141 | ord := ordMappings[actionType] 142 | *a = Action{Enum: borsh.Enum(ord)} 143 | ul := a.UnderlyingValue() 144 | 145 | if err = json.Unmarshal(obj[actionType], ul); err != nil { 146 | return 147 | } 148 | 149 | return nil 150 | } 151 | 152 | type ActionCreateAccount struct { 153 | } 154 | 155 | // Create an (sub)account using a transaction `receiver_id` as an ID for a new account 156 | func NewCreateAccount() Action { 157 | return Action{ 158 | Enum: borsh.Enum(ordCreateAccount), 159 | CreateAccount: ActionCreateAccount{}, 160 | } 161 | } 162 | 163 | type ActionDeployContract struct { 164 | Code []byte `json:"code"` 165 | } 166 | 167 | func NewDeployContract(code []byte) Action { 168 | return Action{ 169 | Enum: borsh.Enum(ordDeployContract), 170 | DeployContract: ActionDeployContract{ 171 | Code: code, 172 | }, 173 | } 174 | } 175 | 176 | type ActionFunctionCall struct { 177 | MethodName string `json:"method_name"` 178 | Args []byte `json:"args"` 179 | Gas types.Gas `json:"gas"` 180 | Deposit types.Balance `json:"deposit"` 181 | } 182 | 183 | func (f ActionFunctionCall) String() string { 184 | return fmt.Sprintf("FunctionCall{MethodName: %s, Args: %s, Gas: %d, Deposit: %s}", f.MethodName, f.Args, f.Gas, f.Deposit) 185 | } 186 | 187 | func NewFunctionCall(methodName string, args []byte, gas types.Gas, deposit types.Balance) Action { 188 | return Action{ 189 | Enum: borsh.Enum(ordFunctionCall), 190 | FunctionCall: ActionFunctionCall{ 191 | MethodName: methodName, 192 | Args: args, 193 | Gas: gas, 194 | Deposit: deposit, 195 | }, 196 | } 197 | } 198 | 199 | type ActionTransfer struct { 200 | Deposit types.Balance `json:"deposit"` 201 | } 202 | 203 | func (t ActionTransfer) String() string { 204 | return fmt.Sprintf("Transfer{Deposit: %s}", t.Deposit) 205 | } 206 | 207 | func NewTransfer(deposit types.Balance) Action { 208 | return Action{ 209 | Enum: borsh.Enum(ordTransfer), 210 | Transfer: ActionTransfer{ 211 | Deposit: deposit, 212 | }, 213 | } 214 | } 215 | 216 | type ActionStake struct { 217 | // Amount of tokens to stake. 218 | Stake types.Balance `json:"stake"` 219 | // Validator key which will be used to sign transactions on behalf of singer_id 220 | PublicKey key.PublicKey `json:"public_key"` 221 | } 222 | 223 | func NewStake(stake types.Balance, publicKey key.PublicKey) Action { 224 | return Action{ 225 | Enum: borsh.Enum(ordStake), 226 | Stake: ActionStake{ 227 | Stake: stake, 228 | PublicKey: publicKey, 229 | }, 230 | } 231 | } 232 | 233 | type ActionAddKey struct { 234 | PublicKey key.PublicKey `json:"public_key"` 235 | AccessKey ActionAddKeyAccessKey `json:"access_key"` 236 | } 237 | 238 | type ActionAddKeyAccessKey struct { 239 | Nonce types.Nonce `json:"nonce"` 240 | Permission AccessKeyPermission `json:"permission"` 241 | } 242 | 243 | type jsonActionAddKey struct { 244 | PublicKey key.Base58PublicKey `json:"public_key"` 245 | AccessKey ActionAddKeyAccessKey `json:"access_key"` 246 | } 247 | 248 | func NewAddKey(publicKey key.PublicKey, nonce types.Nonce, permission AccessKeyPermission) Action { 249 | return Action{ 250 | Enum: borsh.Enum(ordAddKey), 251 | AddKey: ActionAddKey{}, 252 | } 253 | } 254 | 255 | func (a ActionAddKey) MarshalJSON() (b []byte, err error) { 256 | v := jsonActionAddKey{ 257 | PublicKey: a.PublicKey.ToBase58PublicKey(), 258 | AccessKey: a.AccessKey, 259 | } 260 | b, err = json.Marshal(&v) 261 | return 262 | } 263 | 264 | func (a *ActionAddKey) UnmarshalJSON(b []byte) (err error) { 265 | var v jsonActionAddKey 266 | if err = json.Unmarshal(b, &v); err != nil { 267 | return 268 | } 269 | 270 | *a = ActionAddKey{ 271 | PublicKey: v.PublicKey.ToPublicKey(), 272 | AccessKey: v.AccessKey, 273 | } 274 | 275 | return 276 | } 277 | 278 | type ActionDeleteKey struct { 279 | PublicKey key.PublicKey `json:"public_key"` 280 | } 281 | 282 | type jsonActionDeleteKey struct { 283 | PublicKey key.Base58PublicKey `json:"public_key"` 284 | } 285 | 286 | func NewDeleteKey(publicKey key.PublicKey) Action { 287 | return Action{ 288 | Enum: borsh.Enum(ordDeleteKey), 289 | DeleteKey: ActionDeleteKey{ 290 | PublicKey: publicKey, 291 | }, 292 | } 293 | } 294 | 295 | func (a ActionDeleteKey) MarshalJSON() (b []byte, err error) { 296 | v := jsonActionDeleteKey{ 297 | PublicKey: a.PublicKey.ToBase58PublicKey(), 298 | } 299 | b, err = json.Marshal(&v) 300 | return 301 | } 302 | 303 | func (a *ActionDeleteKey) UnmarshalJSON(b []byte) (err error) { 304 | var v jsonActionDeleteKey 305 | if err = json.Unmarshal(b, &v); err != nil { 306 | return 307 | } 308 | 309 | *a = ActionDeleteKey{ 310 | PublicKey: v.PublicKey.ToPublicKey(), 311 | } 312 | 313 | return 314 | } 315 | 316 | type ActionDeleteAccount struct { 317 | BeneficiaryID types.AccountID `json:"beneficiary_id"` 318 | } 319 | 320 | func NewDeleteAccount(beneficiaryID types.AccountID) Action { 321 | return Action{ 322 | Enum: borsh.Enum(ordDeleteAccount), 323 | DeleteAccount: ActionDeleteAccount{ 324 | BeneficiaryID: beneficiaryID, 325 | }, 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /pkg/types/balance.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "math" 7 | "math/big" 8 | 9 | uint128 "github.com/eteu-technologies/golang-uint128" 10 | "github.com/shopspring/decimal" 11 | ) 12 | 13 | var ( 14 | tenPower24 = uint128.From64(uint64(math.Pow10(12))).Mul64(uint64(math.Pow10(12))) 15 | zeroNEAR = Balance(uint128.From64(0)) 16 | dTenPower24, _ = decimal.NewFromString(tenPower24.String()) 17 | ) 18 | 19 | // Balance holds amount of yoctoNEAR 20 | type Balance uint128.Uint128 21 | 22 | func (bal *Balance) UnmarshalJSON(b []byte) error { 23 | var s string 24 | if err := json.Unmarshal(b, &s); err != nil { 25 | return err 26 | } 27 | 28 | val := big.Int{} 29 | if _, ok := val.SetString(s, 10); !ok { 30 | return fmt.Errorf("unable to parse '%s'", s) 31 | } 32 | 33 | *bal = Balance(uint128.FromBig(&val)) 34 | 35 | return nil 36 | } 37 | 38 | func (bal Balance) MarshalJSON() ([]byte, error) { 39 | return json.Marshal(bal.String()) 40 | } 41 | 42 | func (bal Balance) String() string { 43 | return uint128.Uint128(bal).String() 44 | } 45 | 46 | // Convenience funcs 47 | func (bal Balance) Div64(div uint64) Balance { 48 | return Balance(uint128.Uint128(bal).Div64(div)) 49 | } 50 | 51 | // TODO 52 | func NEARToYocto(near uint64) Balance { 53 | if near == 0 { 54 | return zeroNEAR 55 | } 56 | 57 | return Balance(uint128.From64(near).Mul(tenPower24)) 58 | } 59 | 60 | // TODO 61 | func YoctoToNEAR(yocto Balance) uint64 { 62 | div := uint128.Uint128(yocto).Div(tenPower24) 63 | if h := div.Hi; h != 0 { 64 | panic(fmt.Errorf("yocto div failed, remaining: %d", h)) 65 | } 66 | 67 | return div.Lo 68 | } 69 | 70 | func scaleToYocto(amount decimal.Decimal) (r *big.Int) { 71 | // Multiply base using the supplied float 72 | amount = amount.Mul(dTenPower24) 73 | 74 | // Convert it to big.Int 75 | return amount.BigInt() 76 | } 77 | 78 | func BalanceFromFloat(f float64) (bal Balance) { 79 | bal = Balance(uint128.FromBig(scaleToYocto(decimal.NewFromFloat(f)))) 80 | return 81 | } 82 | 83 | func BalanceFromString(s string) (bal Balance, err error) { 84 | amount, err := decimal.NewFromString(s) 85 | if err != nil { 86 | return 87 | } 88 | 89 | bal = Balance(uint128.FromBig(scaleToYocto(amount))) 90 | return 91 | } 92 | -------------------------------------------------------------------------------- /pkg/types/balance_test.go: -------------------------------------------------------------------------------- 1 | package types_test 2 | 3 | import ( 4 | "testing" 5 | 6 | fuzz "github.com/google/gofuzz" 7 | 8 | . "github.com/eteu-technologies/near-api-go/pkg/types" 9 | ) 10 | 11 | func TestNEARToYocto(t *testing.T) { 12 | var NEAR uint64 = 10 13 | 14 | yoctoValue := NEARToYocto(NEAR) 15 | orig := YoctoToNEAR(yoctoValue) 16 | 17 | if NEAR != orig { 18 | t.Errorf("expected: %d, got: %d", NEAR, orig) 19 | } 20 | 21 | NEAR = 0 22 | yoctoValue = NEARToYocto(NEAR) 23 | orig = YoctoToNEAR(yoctoValue) 24 | 25 | if NEAR != orig { 26 | t.Errorf("expected: %d, got: %d", NEAR, orig) 27 | } 28 | } 29 | 30 | func TestNEARToYocto_Fuzz(t *testing.T) { 31 | f := fuzz.New() 32 | 33 | // TODO: ? 34 | var value uint16 35 | 36 | for i := 0; i < 1000; i++ { 37 | f.Fuzz(&value) 38 | newValue := YoctoToNEAR(NEARToYocto(uint64(value))) 39 | if uint64(value) != newValue { 40 | t.Errorf("expected: %d, got: %d", value, newValue) 41 | } 42 | } 43 | } 44 | 45 | // Tests for Balance UnmarshalJSON 46 | 47 | var valuesForTestBalanceUnmarshalJSON = map[string]string{ 48 | `"0"`: "0", 49 | `"1"`: "1", 50 | `"100000000000"`: "100000000000", 51 | `"340282366920938000000000000000000000000"`: "340282366920938000000000000000000000000", 52 | } 53 | 54 | func TestBalanceUnmarshalJSON(t *testing.T) { 55 | for key, value := range valuesForTestBalanceUnmarshalJSON { 56 | var bal Balance 57 | err := bal.UnmarshalJSON([]byte(key)) 58 | if err != nil { 59 | t.Errorf("Key: %s, expected: %s, got: %s", key, value, err) 60 | } 61 | 62 | balString := bal.String() 63 | if value != balString { 64 | t.Errorf("Key: %s, expected: %s, got: %s", key, value, balString) 65 | } 66 | } 67 | } 68 | 69 | var errorsForTestBalanceUnmarshalJSON = map[string]string{ 70 | `"340.2.2"`: "unable to parse '340.2.2'", 71 | `"340.2"`: "unable to parse '340.2'", 72 | `"abcd"`: "unable to parse 'abcd'", 73 | "abcd": "invalid character 'a' looking for beginning of value", 74 | } 75 | 76 | func TestBalanceUnmarshalJSONError(t *testing.T) { 77 | var bal Balance 78 | var err error 79 | 80 | for v, e := range errorsForTestBalanceUnmarshalJSON { 81 | err = bal.UnmarshalJSON([]byte(v)) 82 | if err == nil || err.Error() != e { 83 | t.Errorf("Key: %s, expected: %s, got: %s", v, e, err) 84 | } 85 | } 86 | } 87 | 88 | // Tests for BalanceFromFloat and BalanceFromFloatNew 89 | 90 | var valuesForTestBalanceFromFloat = map[float64]string{ 91 | 0.100000000000000000000000: "100000000000000000000000", 92 | 0.340282366920938: "340282366920938000000000", 93 | 0.340282366920939: "340282366920939000000000", 94 | 0.340282366920940: "340282366920940000000000", 95 | 0.340282366920941: "340282366920941000000000", 96 | 340282366920938: "340282366920938000000000000000000000000", 97 | 3.40282366920938: "3402823669209380000000000", 98 | 34.0282366920938: "34028236692093800000000000", 99 | 340.282366920938: "340282366920938000000000000", 100 | 340.282: "340282000000000000000000000", 101 | 340.28: "340280000000000000000000000", 102 | 340.2: "340200000000000000000000000", 103 | } 104 | 105 | func TestBalanceFromFloat(t *testing.T) { 106 | for key, value := range valuesForTestBalanceFromFloat { 107 | bal := BalanceFromFloat(key) 108 | 109 | balString := bal.String() 110 | if value != balString { 111 | t.Errorf("Key: %.15f, expected: %s, got: %s", key, value, balString) 112 | } 113 | } 114 | } 115 | 116 | // Tests for BalanceFromString and BalanceFromStringNew 117 | 118 | var valuesForTestBalanceFromString = map[string]string{ 119 | "0.100000000000000000000000": "100000000000000000000000", 120 | "0.340282366920938": "340282366920938000000000", 121 | "0.340282366920939": "340282366920939000000000", 122 | "0.340282366920940": "340282366920940000000000", 123 | "0.340282366920941": "340282366920941000000000", 124 | "340282366920938": "340282366920938000000000000000000000000", 125 | "3.40282366920938": "3402823669209380000000000", 126 | "34.0282366920938": "34028236692093800000000000", 127 | "340.282366920938": "340282366920938000000000000", 128 | "340.282": "340282000000000000000000000", 129 | "340.28": "340280000000000000000000000", 130 | "340.2": "340200000000000000000000000", 131 | } 132 | 133 | func TestBalanceFromString(t *testing.T) { 134 | for key, value := range valuesForTestBalanceFromString { 135 | bal, err := BalanceFromString(key) 136 | if err != nil { 137 | t.Errorf("Key: %s, expected: %s, got: %s", key, value, err) 138 | } 139 | 140 | balString := bal.String() 141 | if value != balString { 142 | t.Errorf("Key: %s, expected: %s, got: %s", key, value, balString) 143 | } 144 | } 145 | } 146 | 147 | func TestBalanceFromStringError(t *testing.T) { 148 | _, err := BalanceFromString("340.2.2") 149 | if err == nil { 150 | t.Errorf("expected error, got: nil") 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /pkg/types/basic.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // Account identifier. Provides access to user's state. 4 | type AccountID = string 5 | 6 | // Gas is a type for storing amounts of gas. 7 | type Gas = uint64 8 | 9 | // Nonce for transactions. 10 | type Nonce = uint64 11 | 12 | // Time nanoseconds fit into uint128. Using existing Balance type which 13 | // implements JSON marshal/unmarshal 14 | type TimeNanos = Balance 15 | 16 | // BlockHeight is used for height of the block 17 | type BlockHeight = uint64 18 | 19 | // ShardID is used for a shard index, from 0 to NUM_SHARDS - 1. 20 | type ShardID = uint64 21 | 22 | // StorageUsage is used to count the amount of storage used by a contract. 23 | type StorageUsage = uint64 24 | 25 | // NumBlocks holds number of blocks in current group. 26 | type NumBlocks = uint64 27 | -------------------------------------------------------------------------------- /pkg/types/constants.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | var ( 4 | // 30 TGas 5 | DefaultFunctionCallGas Gas = 30 * 1000000000000 6 | ) 7 | -------------------------------------------------------------------------------- /pkg/types/hash/crypto_hash.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/mr-tron/base58" 9 | ) 10 | 11 | // CryptoHash is a wrapper for SHA-256 digest byte array. 12 | // Note that nearcore also defines MerkleHash as an alias, but it's omitted from this project. 13 | type CryptoHash [sha256.Size]byte 14 | 15 | func (c *CryptoHash) UnmarshalJSON(b []byte) (err error) { 16 | var s string 17 | if err = json.Unmarshal(b, &s); err != nil { 18 | return 19 | } 20 | 21 | if *c, err = NewCryptoHashFromBase58(s); err != nil { 22 | return 23 | } 24 | 25 | return nil 26 | } 27 | 28 | func (c CryptoHash) MarshalJSON() ([]byte, error) { 29 | return json.Marshal(c.String()) 30 | } 31 | 32 | func (c CryptoHash) String() string { 33 | return base58.Encode(c[:]) 34 | } 35 | 36 | func NewCryptoHash(data []byte) CryptoHash { 37 | return CryptoHash(sha256.Sum256(data)) 38 | } 39 | 40 | func NewCryptoHashFromBase58(blob string) (ch CryptoHash, err error) { 41 | bytes, err := base58.Decode(blob) 42 | if err != nil { 43 | return 44 | } 45 | 46 | if len(bytes) != sha256.Size { 47 | return ch, fmt.Errorf("invalid base58 data size %d", bytes) 48 | } 49 | 50 | copy(ch[:], bytes) 51 | return 52 | } 53 | 54 | func MustCryptoHashFromBase58(blob string) CryptoHash { 55 | if hash, err := NewCryptoHashFromBase58(blob); err != nil { 56 | panic(err) 57 | } else { 58 | return hash 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /pkg/types/key/aliases.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | // PeerID is the public key 4 | type PeerID = PublicKey 5 | -------------------------------------------------------------------------------- /pkg/types/key/base58_public_key.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/mr-tron/base58" 9 | ) 10 | 11 | type Base58PublicKey struct { 12 | Type PublicKeyType 13 | Value string 14 | 15 | pk PublicKey 16 | } 17 | 18 | func NewBase58PublicKey(raw string) (pk Base58PublicKey, err error) { 19 | split := strings.SplitN(raw, ":", 2) 20 | if len(split) != 2 { 21 | return pk, ErrInvalidPublicKey 22 | } 23 | 24 | keyTypeRaw := split[0] 25 | encodedKey := split[1] 26 | 27 | keyType, ok := reverseKeyTypeMapping[keyTypeRaw] 28 | if !ok { 29 | return pk, ErrInvalidKeyType 30 | } 31 | 32 | decoded, err := base58.Decode(encodedKey) 33 | if err != nil { 34 | return pk, fmt.Errorf("failed to decode public key: %w", err) 35 | } 36 | 37 | pk.Type = keyTypes[keyType] 38 | pk.Value = encodedKey 39 | 40 | pk.pk, err = WrapRawKey(pk.Type, decoded) 41 | 42 | return 43 | } 44 | 45 | func (pk Base58PublicKey) String() string { 46 | return fmt.Sprintf("%s:%s", pk.Type, pk.Value) 47 | } 48 | 49 | func (pk Base58PublicKey) MarshalJSON() ([]byte, error) { 50 | return json.Marshal(pk.String()) 51 | } 52 | 53 | func (pk *Base58PublicKey) UnmarshalJSON(b []byte) (err error) { 54 | var s string 55 | if err = json.Unmarshal(b, &s); err != nil { 56 | return 57 | } 58 | 59 | *pk, err = NewBase58PublicKey(s) 60 | return 61 | } 62 | 63 | // Copies Base58PublicKey to PublicKey 64 | func (pk *Base58PublicKey) ToPublicKey() PublicKey { 65 | return pk.pk 66 | } 67 | -------------------------------------------------------------------------------- /pkg/types/key/common.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | import "errors" 4 | 5 | type PublicKeyType string 6 | 7 | const ( 8 | RawKeyTypeED25519 byte = iota 9 | RawKeyTypeSECP256K1 10 | ) 11 | 12 | const ( 13 | KeyTypeED25519 PublicKeyType = "ed25519" 14 | KeyTypeSECP256K1 PublicKeyType = "secp256k1" 15 | ) 16 | 17 | var ( 18 | ErrInvalidPublicKey = errors.New("invalid public key") 19 | ErrInvalidPrivateKey = errors.New("invalid private key") 20 | ErrInvalidKeyType = errors.New("invalid key type") 21 | 22 | // nolint: deadcode,varcheck,unused 23 | keyTypes = map[byte]PublicKeyType{ 24 | RawKeyTypeED25519: KeyTypeED25519, 25 | RawKeyTypeSECP256K1: KeyTypeSECP256K1, 26 | } 27 | reverseKeyTypeMapping = map[string]byte{ 28 | string(KeyTypeED25519): RawKeyTypeED25519, 29 | string(KeyTypeSECP256K1): RawKeyTypeSECP256K1, 30 | } 31 | ) 32 | -------------------------------------------------------------------------------- /pkg/types/key/key_pair.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "strings" 9 | 10 | secp256k1 "github.com/decred/dcrd/dcrec/secp256k1/v4" 11 | ecdsa "github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa" 12 | "github.com/mr-tron/base58" 13 | 14 | "github.com/eteu-technologies/near-api-go/pkg/types/signature" 15 | ) 16 | 17 | type KeyPair struct { 18 | Type PublicKeyType 19 | 20 | PublicKey Base58PublicKey 21 | PrivateKey interface{} 22 | } 23 | 24 | type PrivateKeyType interface { 25 | ed25519.PrivateKey | secp256k1.PrivateKey 26 | } 27 | 28 | func GenerateKeyPair(keyType PublicKeyType, rand io.Reader) (kp KeyPair, err error) { 29 | if _, ok := reverseKeyTypeMapping[string(keyType)]; !ok { 30 | return kp, ErrInvalidKeyType 31 | } 32 | 33 | var rawPub PublicKey 34 | 35 | switch keyType { 36 | case KeyTypeED25519: 37 | var pub ed25519.PublicKey 38 | var priv ed25519.PrivateKey 39 | 40 | pub, priv, err = ed25519.GenerateKey(rand) 41 | if err != nil { 42 | return 43 | } 44 | 45 | rawPub, err = WrapRawKey(keyType, pub) 46 | if err != nil { 47 | return 48 | } 49 | 50 | kp = CreateKeyPair(keyType, rawPub.ToBase58PublicKey(), priv) 51 | case KeyTypeSECP256K1: 52 | var ephemeralPrivKey *secp256k1.PrivateKey 53 | ephemeralPrivKey, err = secp256k1.GeneratePrivateKey() 54 | if err != nil { 55 | return 56 | } 57 | 58 | rawPub, err = WrapRawKey(keyType, ephemeralPrivKey.PubKey().SerializeCompressed()) 59 | if err != nil { 60 | return 61 | } 62 | 63 | kp = CreateKeyPair(keyType, rawPub.ToBase58PublicKey(), *ephemeralPrivKey) 64 | } 65 | 66 | return 67 | } 68 | 69 | func CreateKeyPair[P PrivateKeyType](keyType PublicKeyType, pub Base58PublicKey, priv P) KeyPair { 70 | return KeyPair{ 71 | Type: keyType, 72 | PublicKey: pub, 73 | PrivateKey: priv, 74 | } 75 | } 76 | 77 | func NewBase58KeyPair(raw string) (kp KeyPair, err error) { 78 | split := strings.SplitN(raw, ":", 2) 79 | if len(split) != 2 { 80 | return kp, ErrInvalidPrivateKey 81 | } 82 | 83 | keyTypeRaw := split[0] 84 | encodedKey := split[1] 85 | 86 | keyType, ok := reverseKeyTypeMapping[keyTypeRaw] 87 | if !ok { 88 | return kp, ErrInvalidKeyType 89 | } 90 | 91 | var decoded []byte 92 | 93 | switch keyType { 94 | case RawKeyTypeED25519: 95 | decoded, err = base58.Decode(encodedKey) 96 | if err != nil { 97 | return kp, fmt.Errorf("failed to decode private key: %w", err) 98 | } 99 | 100 | if len(decoded) != ed25519.PrivateKeySize { 101 | return kp, ErrInvalidPrivateKey 102 | } 103 | 104 | var pubKey PublicKey 105 | 106 | theKeyType := keyTypes[keyType] 107 | privKey := ed25519.PrivateKey(decoded) 108 | pubKey, err = WrapRawKey(theKeyType, privKey[32:]) // See ed25519.Public() 109 | if err != nil { 110 | println("wraprawkey failed") 111 | return 112 | } 113 | 114 | kp = CreateKeyPair(theKeyType, pubKey.ToBase58PublicKey(), privKey) 115 | case RawKeyTypeSECP256K1: 116 | decoded, err = base58.Decode(encodedKey) 117 | if err != nil { 118 | return kp, fmt.Errorf("failed to decode private key: %w", err) 119 | } 120 | 121 | privateKey := secp256k1.PrivKeyFromBytes(decoded) 122 | ephemeralPubKey := privateKey.PubKey().SerializeCompressed() 123 | 124 | theKeyType := keyTypes[keyType] 125 | 126 | var pubKey PublicKey 127 | pubKey, err = WrapRawKey(theKeyType, ephemeralPubKey) 128 | if err != nil { 129 | println("wraprawkey failed") 130 | return 131 | } 132 | 133 | kp = CreateKeyPair(theKeyType, pubKey.ToBase58PublicKey(), *privateKey) 134 | } 135 | 136 | return 137 | } 138 | 139 | func (kp *KeyPair) Sign(data []byte) (sig signature.Signature) { 140 | sigType := reverseKeyTypeMapping[string(kp.Type)] 141 | 142 | switch sigType { 143 | case RawKeyTypeED25519: 144 | privateKey := kp.PrivateKey.(ed25519.PrivateKey) 145 | sig = signature.NewSignatureED25519(ed25519.Sign(privateKey, data)) 146 | case RawKeyTypeSECP256K1: 147 | privateKey := kp.PrivateKey.(secp256k1.PrivateKey) 148 | sig = signature.NewSignatureSECP256K1(ecdsa.Sign(&privateKey, data).Serialize()) 149 | } 150 | return 151 | } 152 | 153 | func (kp *KeyPair) PrivateEncoded() string { 154 | var encoded string 155 | 156 | switch kp.Type { 157 | case KeyTypeED25519: 158 | privateKey := kp.PrivateKey.(ed25519.PrivateKey) 159 | encoded = fmt.Sprintf("%s:%s", kp.Type, base58.Encode(privateKey)) 160 | case KeyTypeSECP256K1: 161 | privateKey := kp.PrivateKey.(secp256k1.PrivateKey) 162 | encoded = fmt.Sprintf("%s:%s", kp.Type, base58.Encode(privateKey.Serialize())) 163 | } 164 | 165 | return encoded 166 | } 167 | 168 | func (kp *KeyPair) UnmarshalJSON(b []byte) (err error) { 169 | var s string 170 | if err = json.Unmarshal(b, &s); err != nil { 171 | return 172 | } 173 | 174 | *kp, err = NewBase58KeyPair(s) 175 | return 176 | } 177 | -------------------------------------------------------------------------------- /pkg/types/key/key_pair_test.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "crypto/rand" 6 | "encoding/json" 7 | "testing" 8 | ) 9 | 10 | // ------------------------------------------------ 11 | // Tests for ED25519 12 | 13 | // TestGenerateKeyPairED25519 tests the generation of a key pair. 14 | func TestGenerateKeyPairED25519(t *testing.T) { 15 | keyPair, err := GenerateKeyPair(KeyTypeED25519, rand.Reader) 16 | if err != nil { 17 | t.Errorf("failed to generate key pair: %s", err) 18 | } 19 | 20 | if keyPair.Type != KeyTypeED25519 { 21 | t.Errorf("invalid key type: %s", keyPair.Type) 22 | } 23 | 24 | if keyPair.PublicKey.Value == "" { 25 | t.Errorf("public key is nil") 26 | } 27 | 28 | if len(keyPair.PublicKey.pk.Value()) != ed25519.PublicKeySize { 29 | t.Errorf("public key is not valid, %d != %d", len(keyPair.PublicKey.pk), ed25519.PublicKeySize) 30 | } 31 | 32 | if keyPair.PrivateKey == nil { 33 | t.Errorf("private key is nil") 34 | } 35 | } 36 | 37 | // TestNewBase58KeyPairED25519 tests the creation of a key pair from a base58 encoded string. 38 | func TestNewBase58KeyPairED25519(t *testing.T) { 39 | raw := "ed25519:2MDRrkKRTXFPuMXkcKm39KzLQznuaCAybKKYKie4j26k8S2Nth8SvDyWxfBbFk8MC1svEJbuekRAUpnDRSFXdd9s" // Private key in base58 40 | expectedPubliKey := "CHRMGVtFYyJ1uPWCpne8WRDEhJgaRGTa1akXUuDCfEhF" 41 | 42 | keyPair, err := NewBase58KeyPair(raw) 43 | if err != nil { 44 | t.Errorf("failed to create key pair: %s", err) 45 | } 46 | 47 | if keyPair.Type != KeyTypeED25519 { 48 | t.Errorf("invalid key type: %s", keyPair.Type) 49 | } 50 | 51 | if keyPair.PublicKey.Value != expectedPubliKey { 52 | t.Errorf("public key is not valid: %s", keyPair.PublicKey.Value) 53 | } 54 | 55 | if keyPair.PrivateKey == nil { 56 | t.Errorf("private key is nil") 57 | } 58 | 59 | if keyPair.PrivateEncoded() != raw { 60 | t.Errorf("private key is not valid: %s", keyPair.PrivateEncoded()) 61 | } 62 | } 63 | 64 | // TestSignAndVerifyED25519 tests the signing and verification of a message. 65 | func TestSignAndVerifyED25519(t *testing.T) { 66 | keyPair, err := GenerateKeyPair(KeyTypeED25519, rand.Reader) 67 | if err != nil { 68 | t.Errorf("failed to generate key pair: %s", err) 69 | } 70 | 71 | message := []byte("Hello World") 72 | signature := keyPair.Sign(message) 73 | 74 | ok, err := keyPair.PublicKey.pk.Verify(message, signature) 75 | if err != nil { 76 | t.Errorf("failed to verify signature: %s", err) 77 | } 78 | 79 | if !ok { 80 | t.Errorf("signature is not valid") 81 | } 82 | } 83 | 84 | // TestUnmarshalJSONED25519 tests the unmarshalling of a key pair from a JSON string. 85 | func TestUnmarshalJSONED25519(t *testing.T) { 86 | keyPair, err := GenerateKeyPair(KeyTypeED25519, rand.Reader) 87 | if err != nil { 88 | t.Errorf("failed to generate key pair: %s", err) 89 | } 90 | 91 | keyPairJSON, err := json.Marshal(keyPair.PrivateEncoded()) 92 | if err != nil { 93 | t.Errorf("failed to marshal key pair: %s", err) 94 | } 95 | 96 | var keyPair2 KeyPair 97 | err = keyPair2.UnmarshalJSON(keyPairJSON) 98 | if err != nil { 99 | t.Errorf("failed to unmarshal key pair: %s", err) 100 | } 101 | 102 | if keyPair2.Type != KeyTypeED25519 { 103 | t.Errorf("invalid key type: %s", keyPair2.Type) 104 | } 105 | 106 | if keyPair2.PublicKey.Value != keyPair.PublicKey.Value { 107 | t.Errorf("public key is not valid: %s", keyPair2.PublicKey.Value) 108 | } 109 | 110 | if keyPair2.PrivateEncoded() != keyPair.PrivateEncoded() { 111 | t.Errorf("private key is not valid: %s", keyPair2.PrivateEncoded()) 112 | } 113 | } 114 | 115 | // ------------------------------------------------ 116 | // Tests for secp256k1 117 | 118 | // TestGenerateKeyPair tests the generation of a key pair. 119 | func TestGenerateKeyPairSECP256k1(t *testing.T) { 120 | keyPair, err := GenerateKeyPair(KeyTypeSECP256K1, rand.Reader) 121 | if err != nil { 122 | t.Errorf("failed to generate key pair: %s", err) 123 | } 124 | 125 | if keyPair.Type != KeyTypeSECP256K1 { 126 | t.Errorf("invalid key type: %s", keyPair.Type) 127 | } 128 | 129 | if keyPair.PublicKey.Value == "" { 130 | t.Errorf("public key is nil") 131 | } 132 | 133 | if keyPair.PrivateKey == nil { 134 | t.Errorf("private key is nil") 135 | } 136 | } 137 | 138 | // TestNewBase58KeyPair tests the creation of a key pair from a base58 encoded string. 139 | func TestNewBase58KeyPairSECP256k1(t *testing.T) { 140 | raw := "secp256k1:3aq6RcztvhMw8PMRbUgyechLS9rpNETDAHFqip3Zb4cb" // Private key in base58 141 | expectedPubliKey := "23URfhHiWFYsFArc5nLrmj8qDMXXrgF2iU39Dod3cXpBu" // Public key 142 | 143 | keyPair, err := NewBase58KeyPair(raw) 144 | if err != nil { 145 | t.Errorf("failed to create key pair: %s", err) 146 | } 147 | 148 | if keyPair.Type != KeyTypeSECP256K1 { 149 | t.Errorf("invalid key type: %s", keyPair.Type) 150 | } 151 | 152 | if keyPair.PublicKey.Value != expectedPubliKey { 153 | t.Errorf("public key is not valid: %s", keyPair.PublicKey.Value) 154 | } 155 | 156 | if keyPair.PrivateKey == nil { 157 | t.Errorf("private key is nil") 158 | } 159 | 160 | if keyPair.PrivateEncoded() != raw { 161 | t.Errorf("private key is not valid: %s", keyPair.PrivateEncoded()) 162 | } 163 | } 164 | 165 | // TestSignAndVerify tests the signing and verification of a message. 166 | func TestSignAndVerifySECP256k1(t *testing.T) { 167 | keyPair, err := GenerateKeyPair(KeyTypeSECP256K1, rand.Reader) 168 | if err != nil { 169 | t.Errorf("failed to generate key pair: %s", err) 170 | } 171 | 172 | message := []byte("Hello World") 173 | signature := keyPair.Sign(message) 174 | 175 | ok, err := keyPair.PublicKey.pk.Verify(message, signature) 176 | if err != nil { 177 | t.Errorf("failed to verify signature: %s", err) 178 | } 179 | 180 | if !ok { 181 | t.Errorf("signature is not valid") 182 | } 183 | } 184 | 185 | // TestUnmarshalJSONECP256k1 tests the unmarshalling of a key pair from a JSON string. 186 | func TestUnmarshalJSONECP256k1(t *testing.T) { 187 | keyPair, err := GenerateKeyPair(KeyTypeSECP256K1, rand.Reader) 188 | if err != nil { 189 | t.Errorf("failed to generate key pair: %s", err) 190 | } 191 | 192 | keyPairJSON, err := json.Marshal(keyPair.PrivateEncoded()) 193 | if err != nil { 194 | t.Errorf("failed to marshal key pair: %s", err) 195 | } 196 | 197 | var keyPair2 KeyPair 198 | err = keyPair2.UnmarshalJSON(keyPairJSON) 199 | if err != nil { 200 | t.Errorf("failed to unmarshal key pair: %s", err) 201 | } 202 | 203 | if keyPair2.Type != KeyTypeSECP256K1 { 204 | t.Errorf("invalid key type: %s", keyPair2.Type) 205 | } 206 | 207 | if keyPair2.PublicKey.Value != keyPair.PublicKey.Value { 208 | t.Errorf("public key is not valid: %s", keyPair2.PublicKey.Value) 209 | } 210 | 211 | if keyPair2.PrivateEncoded() != keyPair.PrivateEncoded() { 212 | t.Errorf("private key is not valid: %s", keyPair2.PrivateEncoded()) 213 | } 214 | } 215 | 216 | // ------------------------------------------------ 217 | -------------------------------------------------------------------------------- /pkg/types/key/public_key.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "encoding/hex" 6 | "encoding/json" 7 | "fmt" 8 | 9 | "github.com/decred/dcrd/dcrec/secp256k1/v4" 10 | "github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa" 11 | "github.com/mr-tron/base58" 12 | 13 | "github.com/eteu-technologies/near-api-go/pkg/types/signature" 14 | ) 15 | 16 | type PublicKey []byte 17 | 18 | func (p PublicKey) Hash() string { 19 | return hex.EncodeToString(p[1:]) 20 | } 21 | 22 | func (p PublicKey) TypeByte() byte { 23 | return p[0] 24 | } 25 | 26 | func (p PublicKey) Value() []byte { 27 | return p[1:] 28 | } 29 | 30 | func (p PublicKey) MarshalJSON() ([]byte, error) { 31 | return json.Marshal(base58.Encode(p[:])) 32 | } 33 | 34 | func (p PublicKey) String() string { 35 | return fmt.Sprintf("%s:%s", keyTypes[p.TypeByte()], base58.Encode(p.Value())) 36 | } 37 | 38 | func (p *PublicKey) UnmarshalJSON(b []byte) error { 39 | var s string 40 | if err := json.Unmarshal(b, &s); err != nil { 41 | return err 42 | } 43 | 44 | dec, err := base58.Decode(s) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | *p = dec 50 | return nil 51 | } 52 | 53 | func (p *PublicKey) Verify(data []byte, signature signature.Signature) (ok bool, err error) { 54 | keyType := p.TypeByte() 55 | if signature.Type() != keyType { 56 | return false, fmt.Errorf("cannot verify signature type %d with key type %d", signature.Type(), p.TypeByte()) 57 | } 58 | 59 | switch keyType { 60 | case RawKeyTypeED25519: 61 | ok = ed25519.Verify(ed25519.PublicKey(p.Value()), data, signature.Value()) 62 | case RawKeyTypeSECP256K1: 63 | var pubKey *secp256k1.PublicKey 64 | pubKey, err = secp256k1.ParsePubKey(p.Value()) 65 | if err != nil { 66 | return 67 | } 68 | 69 | var sign *ecdsa.Signature 70 | sign, err = ecdsa.ParseDERSignature(signature.Value()) 71 | if err != nil { 72 | return 73 | } 74 | 75 | ok = sign.Verify(data, pubKey) 76 | } 77 | 78 | return 79 | } 80 | 81 | func (p *PublicKey) ToBase58PublicKey() Base58PublicKey { 82 | return Base58PublicKey{ 83 | Type: keyTypes[p.TypeByte()], 84 | Value: base58.Encode(p.Value()), 85 | pk: *p, 86 | } 87 | } 88 | 89 | func PublicKeyFromBytes(b []byte) (pk PublicKey, err error) { 90 | f := b[0] 91 | l := len(b) - 1 92 | switch f { 93 | case RawKeyTypeED25519: 94 | if l != ed25519.PublicKeySize { 95 | return pk, ErrInvalidPublicKey 96 | } 97 | pk = b 98 | return 99 | case RawKeyTypeSECP256K1: 100 | if l != secp256k1.PubKeyBytesLenCompressed { 101 | return pk, ErrInvalidPublicKey 102 | } 103 | pk = b 104 | return 105 | } 106 | 107 | return pk, ErrInvalidKeyType 108 | } 109 | 110 | func WrapRawKey(keyType PublicKeyType, key []byte) (pk PublicKey, err error) { 111 | switch keyType { 112 | case KeyTypeED25519: 113 | if len(key) != ed25519.PublicKeySize { 114 | return pk, ErrInvalidPublicKey 115 | } 116 | 117 | pk = make([]byte, ed25519.PublicKeySize+1) 118 | pk[0] = RawKeyTypeED25519 119 | copy(pk[1:], key[0:ed25519.PublicKeySize]) 120 | return 121 | case KeyTypeSECP256K1: 122 | if len(key) != secp256k1.PubKeyBytesLenCompressed { 123 | return pk, ErrInvalidPublicKey 124 | } 125 | 126 | pk = make([]byte, secp256k1.PubKeyBytesLenCompressed+1) 127 | pk[0] = RawKeyTypeSECP256K1 128 | copy(pk[1:], key[0:secp256k1.PubKeyBytesLenCompressed]) 129 | return 130 | } 131 | 132 | return pk, ErrInvalidKeyType 133 | } 134 | 135 | func WrapED25519(key ed25519.PublicKey) PublicKey { 136 | if pk, err := WrapRawKey(KeyTypeED25519, key); err != nil { 137 | panic(err) 138 | } else { 139 | return pk 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /pkg/types/key/public_key_test.go: -------------------------------------------------------------------------------- 1 | package key 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestED25519Key(t *testing.T) { 8 | expected := `ed25519:DcA2MzgpJbrUATQLLceocVckhhAqrkingax4oJ9kZ847` 9 | 10 | parsed, err := NewBase58PublicKey(expected) 11 | if err != nil { 12 | t.Errorf("failed to parse public key: %s", err) 13 | } 14 | 15 | if s := parsed.String(); s != expected { 16 | t.Errorf("%s != %s", s, expected) 17 | } 18 | } 19 | 20 | func TestED25519Key_Base58_And_Back(t *testing.T) { 21 | expected := `ed25519:3xCFas58RKvD5UpF9GqvEb6q9rvgfbEJPhLf85zc4HpC` 22 | 23 | parsed, err := NewBase58PublicKey(expected) 24 | if err != nil { 25 | t.Errorf("failed to parse public key: %s", err) 26 | } 27 | 28 | publicKey := parsed.ToPublicKey() 29 | converted := publicKey.ToBase58PublicKey() 30 | 31 | if s := converted.String(); s != expected { 32 | t.Errorf("%s != %s", s, expected) 33 | } 34 | } 35 | 36 | func TestED25519UnmarshalJSON(t *testing.T) { 37 | expected := `ed25519:DcA2MzgpJbrUATQLLceocVckhhAqrkingax4oJ9kZ847` 38 | parsed, err := NewBase58PublicKey(expected) 39 | if err != nil { 40 | t.Errorf("failed to parse public key: %s", err) 41 | } 42 | 43 | var parsed2 Base58PublicKey 44 | err = parsed2.UnmarshalJSON([]byte(`"` + expected + `"`)) 45 | if err != nil { 46 | t.Errorf("failed to parse public key: %s", err) 47 | } 48 | 49 | if s := parsed2.String(); s != expected { 50 | t.Errorf("%s != %s", s, expected) 51 | } 52 | 53 | if parsed2.Type != parsed.Type { 54 | t.Errorf("parsed2 != parsed") 55 | } 56 | 57 | if parsed2.Value != parsed.Value { 58 | t.Errorf("parsed2 != parsed") 59 | } 60 | } 61 | 62 | func TestSECP256k1UnmarshalJSON(t *testing.T) { 63 | expected := `secp256k1:23URfhHiWFYsFArc5nLrmj8qDMXXrgF2iU39Dod3cXpBu` // Public key 64 | parsed, err := NewBase58PublicKey(expected) 65 | if err != nil { 66 | t.Errorf("failed to parse public key: %s", err) 67 | } 68 | 69 | var parsed2 Base58PublicKey 70 | err = parsed2.UnmarshalJSON([]byte(`"` + expected + `"`)) 71 | if err != nil { 72 | t.Errorf("failed to parse public key: %s", err) 73 | } 74 | 75 | if s := parsed2.String(); s != expected { 76 | t.Errorf("%s != %s", s, expected) 77 | } 78 | 79 | if parsed2.Type != parsed.Type { 80 | t.Errorf("parsed2 != parsed") 81 | } 82 | 83 | if parsed2.Value != parsed.Value { 84 | t.Errorf("parsed2 != parsed") 85 | } 86 | } 87 | 88 | func TestPublicKeyHash(t *testing.T) { 89 | raw := "ed25519:2MDRrkKRTXFPuMXkcKm39KzLQznuaCAybKKYKie4j26k8S2Nth8SvDyWxfBbFk8MC1svEJbuekRAUpnDRSFXdd9s" // Private key in base58 90 | expectedHash := "a7a56191a40b5586bee21fb0e4cd711b5b70dadc02b7486f62bbf1b9b3e51992" 91 | 92 | keyPair, err := NewBase58KeyPair(raw) 93 | if err != nil { 94 | t.Errorf("failed to create key pair: %s", err) 95 | } 96 | 97 | hash := keyPair.PublicKey.pk.Hash() 98 | if hash != expectedHash { 99 | t.Errorf("%s != %s", hash, expectedHash) 100 | } 101 | } 102 | 103 | func TestPublicKeyString(t *testing.T) { 104 | raw := "ed25519:2MDRrkKRTXFPuMXkcKm39KzLQznuaCAybKKYKie4j26k8S2Nth8SvDyWxfBbFk8MC1svEJbuekRAUpnDRSFXdd9s" // Private key in base58 105 | expectedString := "ed25519:CHRMGVtFYyJ1uPWCpne8WRDEhJgaRGTa1akXUuDCfEhF" 106 | 107 | keyPair, err := NewBase58KeyPair(raw) 108 | if err != nil { 109 | t.Errorf("failed to create key pair: %s", err) 110 | } 111 | 112 | hash := keyPair.PublicKey.pk.String() 113 | if hash != expectedString { 114 | t.Errorf("%s != %s", hash, expectedString) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /pkg/types/signature/base58_signature.go: -------------------------------------------------------------------------------- 1 | package signature 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/mr-tron/base58" 9 | ) 10 | 11 | type Base58Signature struct { 12 | Type SignatureType 13 | Value string 14 | 15 | //sig Signature 16 | } 17 | 18 | func NewBase58Signature(raw string) (pk Base58Signature, err error) { 19 | split := strings.SplitN(raw, ":", 2) 20 | if len(split) != 2 { 21 | return pk, ErrInvalidSignature 22 | } 23 | 24 | sigTypeRaw := split[0] 25 | encodedSig := split[1] 26 | 27 | sigType, ok := reverseSignatureMapping[sigTypeRaw] 28 | if !ok { 29 | return pk, ErrInvalidSignatureType 30 | } 31 | 32 | decoded, err := base58.Decode(encodedSig) 33 | if err != nil { 34 | return pk, fmt.Errorf("failed to decode signature: %w", err) 35 | } 36 | 37 | pk.Type = signatureTypes[sigType] 38 | pk.Value = encodedSig 39 | 40 | // TODO 41 | _ = decoded 42 | 43 | return 44 | } 45 | 46 | func (sig Base58Signature) String() string { 47 | return fmt.Sprintf("%s:%s", sig.Type, sig.Value) 48 | } 49 | 50 | func (sig Base58Signature) MarshalJSON() ([]byte, error) { 51 | return json.Marshal(sig.String()) 52 | } 53 | 54 | func (sig *Base58Signature) UnmarshalJSON(b []byte) (err error) { 55 | var s string 56 | if err = json.Unmarshal(b, &s); err != nil { 57 | return 58 | } 59 | 60 | *sig, err = NewBase58Signature(s) 61 | return 62 | } 63 | -------------------------------------------------------------------------------- /pkg/types/signature/common.go: -------------------------------------------------------------------------------- 1 | package signature 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | type SignatureType string 8 | 9 | const ( 10 | RawSignatureTypeED25519 byte = iota 11 | RawSignatureTypeSECP256K1 12 | ) 13 | 14 | const ( 15 | SignatureTypeED25519 = SignatureType("ed25519") 16 | SignatureTypeSECP256K1 = SignatureType("secp256k1") 17 | ) 18 | 19 | var ( 20 | ErrInvalidSignature = errors.New("invalid signature") 21 | ErrInvalidSignatureType = errors.New("invalid signature type") 22 | 23 | signatureTypes = map[byte]SignatureType{ 24 | RawSignatureTypeED25519: SignatureTypeED25519, 25 | RawSignatureTypeSECP256K1: SignatureTypeSECP256K1, 26 | } 27 | reverseSignatureMapping = map[string]byte{ 28 | string(SignatureTypeED25519): RawSignatureTypeED25519, 29 | string(SignatureTypeSECP256K1): RawSignatureTypeSECP256K1, 30 | } 31 | ) 32 | -------------------------------------------------------------------------------- /pkg/types/signature/signature.go: -------------------------------------------------------------------------------- 1 | package signature 2 | 3 | import ( 4 | "crypto/ed25519" 5 | ) 6 | 7 | type Signature []byte 8 | 9 | func NewSignatureED25519(data []byte) Signature { 10 | buf := make([]byte, 1+ed25519.SignatureSize) 11 | buf[0] = RawSignatureTypeED25519 12 | copy(buf[1:], data[0:ed25519.SignatureSize]) 13 | return buf 14 | } 15 | 16 | func (s Signature) Type() byte { 17 | return s[0] 18 | } 19 | 20 | func (s Signature) Value() []byte { 21 | return s[1:] 22 | } 23 | 24 | func NewSignatureSECP256K1(data []byte) Signature { 25 | sign := make([]byte, 1+len(data)) 26 | sign[0] = RawSignatureTypeSECP256K1 27 | copy(sign[1:], data) 28 | return sign 29 | } 30 | -------------------------------------------------------------------------------- /pkg/types/signature/signature_test.go: -------------------------------------------------------------------------------- 1 | package signature_test 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestED25519Signature(t *testing.T) { 8 | expected := `ed25519:` 9 | 10 | _ = expected 11 | } 12 | -------------------------------------------------------------------------------- /pkg/types/transaction/signed_transaction.go: -------------------------------------------------------------------------------- 1 | package transaction 2 | 3 | import ( 4 | "encoding/base64" 5 | 6 | "github.com/eteu-technologies/borsh-go" 7 | "github.com/eteu-technologies/near-api-go/pkg/types/hash" 8 | "github.com/eteu-technologies/near-api-go/pkg/types/key" 9 | "github.com/eteu-technologies/near-api-go/pkg/types/signature" 10 | ) 11 | 12 | type SignedTransaction struct { 13 | Transaction Transaction 14 | Signature signature.Signature 15 | 16 | SerializedTransaction []byte `borsh_skip:"true"` 17 | hash hash.CryptoHash `borsh_skip:"true"` 18 | size int `borsh_skip:"true"` 19 | } 20 | 21 | func NewSignedTransaction(keyPair key.KeyPair, transaction Transaction) (stxn SignedTransaction, err error) { 22 | stxn.Transaction = transaction 23 | stxn.hash, stxn.SerializedTransaction, stxn.Signature, err = transaction.HashAndSign(keyPair) 24 | if err != nil { 25 | return 26 | } 27 | 28 | stxn.size = len(stxn.SerializedTransaction) 29 | return 30 | } 31 | 32 | func (st *SignedTransaction) Hash() hash.CryptoHash { 33 | return st.hash 34 | } 35 | 36 | func (st *SignedTransaction) Size() int { 37 | return st.size 38 | } 39 | 40 | func (st *SignedTransaction) Verify(pubKey key.PublicKey) (ok bool, err error) { 41 | var txnHash hash.CryptoHash 42 | txnHash, _, err = st.Transaction.Hash() 43 | if err != nil { 44 | return 45 | } 46 | 47 | return pubKey.Verify(txnHash[:], st.Signature) 48 | } 49 | 50 | func (st SignedTransaction) Serialize() (serialized string, err error) { 51 | var blob []byte 52 | 53 | blob, err = borsh.Serialize(st) 54 | if err != nil { 55 | return 56 | } 57 | 58 | serialized = base64.StdEncoding.EncodeToString(blob) 59 | 60 | return 61 | } 62 | -------------------------------------------------------------------------------- /pkg/types/transaction/transaction.go: -------------------------------------------------------------------------------- 1 | package transaction 2 | 3 | import ( 4 | "github.com/eteu-technologies/borsh-go" 5 | "github.com/eteu-technologies/near-api-go/pkg/types/signature" 6 | 7 | "github.com/eteu-technologies/near-api-go/pkg/types" 8 | "github.com/eteu-technologies/near-api-go/pkg/types/action" 9 | "github.com/eteu-technologies/near-api-go/pkg/types/hash" 10 | "github.com/eteu-technologies/near-api-go/pkg/types/key" 11 | ) 12 | 13 | type Transaction struct { 14 | SignerID types.AccountID 15 | PublicKey key.PublicKey 16 | Nonce types.Nonce 17 | ReceiverID types.AccountID 18 | BlockHash hash.CryptoHash 19 | Actions []action.Action 20 | } 21 | 22 | func (t Transaction) Hash() (txnHash hash.CryptoHash, serialized []byte, err error) { 23 | // Serialize into Borsh 24 | serialized, err = borsh.Serialize(t) 25 | if err != nil { 26 | return 27 | } 28 | txnHash = hash.NewCryptoHash(serialized) 29 | return 30 | } 31 | 32 | func (t Transaction) HashAndSign(keyPair key.KeyPair) (txnHash hash.CryptoHash, serialized []byte, sig signature.Signature, err error) { 33 | txnHash, serialized, err = t.Hash() 34 | if err != nil { 35 | return 36 | } 37 | 38 | sig = keyPair.Sign(txnHash[:]) 39 | return 40 | } 41 | -------------------------------------------------------------------------------- /pkg/types/transaction/utils.go: -------------------------------------------------------------------------------- 1 | package transaction 2 | 3 | import "github.com/eteu-technologies/near-api-go/pkg/types/key" 4 | 5 | func SignAndSerializeTransaction(keyPair key.KeyPair, txn Transaction) (blob string, err error) { 6 | var stxn SignedTransaction 7 | if stxn, err = NewSignedTransaction(keyPair, txn); err != nil { 8 | return 9 | } 10 | 11 | blob, err = stxn.Serialize() 12 | return 13 | } 14 | --------------------------------------------------------------------------------