Tutorial: Build an API requester¶
This tutorial walks you through the process of creating a simple Golang program that can call Insolar MainNet API.
Note
You can also use the CLI tool of the same name (requester) to call the API.
Code examples in this tutorial are straightforward, statements are successive (without conditional expressions and cycles). This lets you focus on substance rather than code structure: how to use cryptographic functions provided by Golang packages to correctly sign API requests to MainNet.
You can learn to sign requests in two ways:
By creating a new private key (together with a new Insolar Wallet), or
By using an existing private key if you have already created your Wallet in the web interface.
Note
This tutorial uses the API endpoint of Insolar TestNet in code examples.
What you will build¶
You will build a program that creates a member on the Insolar network or uses an existing one and, as a member, transfers funds from its account to the account of another member.
What you will need¶
About an hour
Your favorite IDE for Golang and its programming tools
Insolar MainNet API specification as a reference
Insolar TestNet as a testing environment
How to complete this tutorial¶
To start from scratch, go through the step-by-step instructions listed below and pay attention to comments in code examples.
To skip the basics, read (and copy-paste) a working requester code provided at the end of this tutorial.
Building the requester¶
In Insolar MainNet (and TestNet), all requests to contracts go through two phases:
Phase 1: A client requests a seed from a node. The seed is a unique piece of information (explained in more detail in the following sections).
Phase 2: The client forms and sends a contract request in the following way:
Puts the seed value and a public key into a contract request body.
Takes a hash of the body’s bytes.
Signs the hash with a private key.
Puts both the hash and signature into corresponding headers.
Sends the request to the node that provided the seed in the previous phase.
To automate this process, you can build a simple requester program by completing the following steps:
Prepare: install the necessary tools, import the necessary packages, and initialize an HTTP client.
Declare structures that correspond to request bodies described in the Insolar API specification.
Create a seed getter function. The getter is reused to put a new seed into every contract request.
Create a sender function that signs and sends contract requests given a private key.
Form and send a transfer request: get a new seed and call the sender function.
Test the requester against Insolar TestNet.
All the above steps are detailed in sections below.
Step 1: Prepare¶
To build the requester, install, import, and set up the following:
Install a copy of the standard crypto library with the
secp256k1
elliptic curve implementation provided by Insolar:go get -t github.com/insolar/x-crypto/ecdsa/...
In a new
Main.go
file, import the packages your requester will use (or skip this step and let your IDE do it for you along the way). For example:1package main 2 3import ( 4 // You will need: 5 // - Basic Golang functionality. 6 "bytes" 7 "fmt" 8 "golang.org/x/net/publicsuffix" 9 "io/ioutil" 10 "log" 11 "strconv" 12 // - HTTP client and a cookiejar. 13 "net/http" 14 "net/http/cookiejar" 15 // - Big numbers to store signatures. 16 "math/big" 17 // - Basic cryptography. 18 "crypto/rand" 19 "crypto/sha256" 20 // - Basic encoding capabilities. 21 "encoding/asn1" 22 "encoding/base64" 23 "encoding/json" 24 "encoding/pem" 25 // - A copy of the standard crypto library with 26 // the ECDSA secp256k1 curve implementation. 27 xecdsa "github.com/insolar/x-crypto/ecdsa" 28 xelliptic "github.com/insolar/x-crypto/elliptic" 29 "github.com/insolar/x-crypto/x509" 30)
Declare, set, and initialize the following:
Insolar supports ECDSA-signed requests. Since an ECDSA signature in Golang consists of two big integers, declare a single structure to contain it.
Set the API endpoint URL to that of TestNet.
Create and initialize an HTTP client for connection reuse and store a
cookiejar
inside.Create a variable for the JSON RPC 2.0 request identifier. The identifier is to be incremented for every request and each corresponding response will contain it.
31// Declare a structure to contain the ECDSA signature. 32type ecdsaSignature struct { 33 R, S *big.Int 34} 35 36// Set the endpoint URL to that of TestNet. 37const ( 38 TestNetURL = "https://wallet-api.testnet.insolar.io/api/rpc" 39) 40 41// Create and initialize an HTTP client for connection reuse 42// and put a cookiejar into it. 43var client *http.Client 44var jar cookiejar.Jar 45func init() { 46 // All users of cookiejar should import "golang.org/x/net/publicsuffix" 47 jar, err := cookiejar.New(&cookiejar.Options{ 48 PublicSuffixList: publicsuffix.List}) 49 if err != nil { 50 log.Fatal(err) 51 } 52 client = &http.Client{ 53 Jar: jar, 54 } 55} 56 57// Create a variable for the JSON RPC 2.0 request identifier. 58var id int = 1 59// The identifier is incremented in every request 60// and each corresponding response contains it.
With that, everything your requester requires is set up.
Next, declare request structures in accordance with the Insolar API specification.
Step 2: Declare request structures¶
To call the MainNet (or TestNet) API, you need structures for three requests: seed getter, member creation, and transfer.
All the requests have the same base structure in accordance with the JSON RPC 2.0 specification. For example:
{
"jsonrpc": "2.0",
"id": 1,
"method": "contract.call"
"params": { ... }
}
Where "params"
is an optional object that may contain parameters of a particular method.
Define the base structure and nest more structures for all the required parameters. For example:
61// Continue in the Main.go file...
62
63// Declare a base structure to form requests to Insolar API
64// in accordance with the specification.
65type requestBody struct {
66 JSONRPC string `json:"jsonrpc"`
67 ID int `json:"id"`
68 Method string `json:"method"`
69}
70
71type requestBodyWithParams struct {
72 JSONRPC string `json:"jsonrpc"`
73 ID int `json:"id"`
74 Method string `json:"method"`
75 // Params is a structure that depends on a particular method.
76 Params interface{} `json:"params"`
77}
78
79// Insolar MainNet defines params of a contract request as follows.
80type params struct {
81 Seed string `json:"seed"`
82 CallSite string `json:"callSite"`
83 // CallParams is a structure that depends on a particular method.
84 CallParams interface{} `json:"callParams"`
85 PublicKey string `json:"publicKey"`
86}
87
88// The transfer request has a reference in params.
89type paramsWithReference struct {
90 params
91 Reference string `json:"reference"`
92}
93
94// The member.create request has no callParams,
95// so here goes an empty structure.
96type memberCreateCallParams struct {}
97
98// The transfer request sends an amount of funds to
99// a member identified by a reference.
100type transferCallParams struct {
101 Amount string `json:"amount"`
102 ToMemberReference string `json:"toMemberReference"`
103}
Now that the requester has all the requests structures it is supposed to use, the next step is to create the following functions:
A seed getter to retrieve a new seed for each contract request.
A sender function that signs and sends contract requests.
Step 3: Create a seed getter¶
Each signed request to Insolar API has to contain a seed in its body. Seed is a unique piece of information generated by a node that:
Has a short lifespan.
Expires upon first use.
Protects from request duplicates.
Upon receiving a contract request, any node checks if it was the one that generated the seed and if the seed is still alive. So, each contract request with a seed must be sent to the node you requested the seed from.
Tip
To make sure that the contract request is routed to the correct node, retrieve all the cookies with routing information from the node’s response and store them in the HTTP client as described in the preparation step.
To form contract requests, create a seed getter function you can reuse.
The seed getter does the following:
Forms a
node.getSeed
request body in JSON format.Creates an HTTP request with the body and a Content-Type (
application/json
) HTTP header.Sends the request and receives a response.
Retrieves the seed from the response and returns it.
For example:
104// Continue in the Main.go file...
105
106// Create a function to get a new seed for each signed request.
107func getNewSeed() (string) {
108 // Form a request body for getSeed.
109 getSeedReq := requestBody{
110 JSONRPC: "2.0",
111 Method: "node.getSeed",
112 ID: id,
113 }
114 // Increment the id for future requests.
115 id++
116
117 // Marshal the payload into JSON.
118 jsonSeedReq, err := json.Marshal(getSeedReq)
119 if err != nil {
120 log.Fatalln(err)
121 }
122
123 // Create a new HTTP request.
124 seedReq, err := http.NewRequest("POST", TestNetURL,
125 bytes.NewBuffer(jsonSeedReq))
126 if err != nil {
127 log.Fatalln(err)
128 }
129 seedReq.Header.Set("Content-Type", "application/json")
130
131 // Send the request.
132 seedResponse, err := client.Do(seedReq)
133 if err != nil {
134 log.Fatalln(err)
135 }
136 defer seedReq.Body.Close()
137
138 // Receive the response body.
139 seedRespBody, err := ioutil.ReadAll(seedResponse.Body)
140 if err != nil {
141 log.Fatalln(err)
142 }
143
144 // Unmarshal the response.
145 var newSeed map[string]interface{}
146 err = json.Unmarshal(seedRespBody, &newSeed)
147 if err != nil {
148 log.Fatalln(err)
149 }
150
151 // (Optional) Print the request and its response.
152 print := "POST to " + TestNetURL +
153 "\nPayload: " + string(jsonSeedReq) +
154 "\nResponse status code: " + strconv.Itoa(seedResponse.StatusCode) +
155 "\nResponse: " + string(seedRespBody) + "\n"
156 fmt.Println(print)
157
158 // Retrieve and return the new seed.
159 return newSeed["result"].(map[string]interface{})["seed"].(string)
160}
Now, every getNewSeed()
call returns a living seed that can be put into the body of a contract request.
The next step is to create a sender function that signs and sends such requests.
Step 4: Create a sender function¶
The sender function does the following:
Takes a request body and an ECDSA private key as arguments.
Forms an HTTP request with the body and the following HTTP headers:
Content-Type —
application/json
.Digest that contains a Base64 string with an SHA-256 hash of the body’s bytes.
Signature that contains a Base64 string with an ECDSA signature (in ASN.1 DER format) of the hash’s bytes.
Sends the request.
Retrieves the response and returns it as a JSON object.
For example:
Tip
In Golang, the ECDSA signature consists of two big integers. To convert the signature into the ASN.1 DER format, put it into the ecdsaSignature
structure.
161// Continue in the Main.go file...
162
163// Create a function to send signed requests.
164func sendSignedRequest(payload requestBodyWithParams,
165 privateKey *ecdsa.PrivateKey) map[string]interface{} {
166
167 // Marshal the payload into JSON.
168 jsonPayload, err := json.Marshal(payload)
169 if err != nil {
170 log.Fatalln(err)
171 }
172
173 // Take a SHA-256 hash of the payload's bytes.
174 hash := sha256.Sum256(jsonPayload)
175
176 // Sign the hash with the private key.
177 r, s, err := ecdsa.Sign(rand.Reader, privateKey, hash[:])
178 if err != nil {
179 log.Fatalln(err)
180 }
181
182 // Convert the signature into ASN.1 DER format.
183 sig := ecdsaSignature{
184 R: r,
185 S: s,
186 }
187 signature, err := asn1.Marshal(sig)
188 if err != nil {
189 log.Fatalln(err)
190 }
191
192 // Encode both hash and signature to a Base64 string.
193 hash64 := base64.StdEncoding.EncodeToString(hash[:])
194 signature64 := base64.StdEncoding.EncodeToString(signature)
195
196 // Create a new request and set its headers.
197 request, err := http.NewRequest("POST", TestNetURL,
198 bytes.NewBuffer(jsonPayload))
199 if err != nil {
200 log.Fatalln(err)
201 }
202 request.Header.Set("Content-Type", "application/json")
203
204 // Put the hash string into the HTTP Digest header.
205 request.Header.Set("Digest", "SHA-256="+hash64)
206
207 // Put the signature string into the HTTP Signature header.
208 request.Header.Set("Signature", "keyId=\"public-key\", " +
209 "algorithm=\"ecdsa\", headers=\"digest\", signature="+signature64)
210
211 // Send the signed request.
212 response, err := client.Do(request)
213 if err != nil {
214 log.Fatalln(err)
215 }
216 defer response.Body.Close()
217
218 // Receive the response body.
219 responseBody, err := ioutil.ReadAll(response.Body)
220 if err != nil {
221 log.Fatalln(err)
222 }
223
224 // Unmarshal it into a JSON object.
225 var JSONObject map[string]interface{}
226 err = json.Unmarshal(responseBody, &JSONObject)
227 if err != nil {
228 log.Fatalln(err)
229 }
230
231 // (Optional) Print the request and its response.
232 print := "POST to " + TestNetURL +
233 "\nPayload: " + string(jsonPayload) +
234 "\nResponse status code: " + strconv.Itoa(response.StatusCode) +
235 "\nResponse: " + string(responseBody) + "\n"
236 fmt.Println(print)
237
238 // Return the JSON object.
239 return JSONObject
240}
Now, every sendSignedRequest(payload, privateKey)
call returns the result of a contract method execution.
With the seed getter and sender functions, you have everything you need to send a contract request. The next step is to:
Generate a key pair and create a member using a special contract request, or
Use an existing member account by retrieving the corresponding private key from the Insolar Wallet’s web interface and converting the key to PEM format.
Step 5: Generate a new key pair or use an existing one¶
The body of each request that calls a contract method must be hashed by a SHA256
algorithm. Each hash must be signed by a private key generated by a p256k1
elliptic curve.
Depending on whether or not you already have an Insolar Wallet, choose one of the following:
To create a member, send the corresponding member creation request—a signed request to a contract method that does the following:
Creates a new member and corresponding account objects.
Returns a reference to the member—address in the Insolar network.
Binds a given public key to the member.
Insolar uses this public key to identify a member and check the signature generated by the paired private key.
Warning
You will not be able to access your member object without the private key and, as such, transfer funds.
First, take care of the keys by following these steps:
Generate a key pair using the elliptic curve and convert both keys to PEM format.
Export the private key into a file.
Save the file to a secure place.
Next, form and sigh the member creation request:
Call the
getNewSeed()
function and put a new seed into a variable.Form the
member.create
request body with the seed and the generated public key.Call the
sendSignedRequest()
function, pass it the body and the private key, and receive a member reference in response.Put the reference into a variable (the transfer request in the next step requires it).
For example:
Tip
To encode the key to PEM format, first, convert it to ASN.1 DER using the x509
library.
241// Continue in the Main.go file...
242
243// Create the main function to form and send signed requests.
244func main() {
245
246 // Generate a key pair.
247 privateKey := new(xecdsa.PrivateKey)
248 privateKey, err := xecdsa.GenerateKey(xelliptic.P256(), rand.Reader)
249 var publicKey xecdsa.PublicKey
250 publicKey = privateKey.PublicKey
251
252 // Convert both private and public keys into PEM format.
253 x509PublicKey, err := x509.MarshalPKIXPublicKey(&publicKey)
254 if err != nil {
255 log.Fatalln(err)
256 }
257 pemPublicKey := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY",
258 Bytes: x509PublicKey})
259
260 x509PrivateKey, err := x509.MarshalECPrivateKey(privateKey)
261 if err != nil {
262 log.Fatalln(err)
263 }
264 pemPrivateKey := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY",
265 Bytes: x509PrivateKey})
266
267 // The private key is required to sign requests.
268 // Make sure to put it into a file to save it to a secure place later.
269 file, err := os.Create("private.pem")
270 if err != nil {
271 fmt.Println(err)
272 return
273 }
274 file.WriteString(string(pemPrivateKey))
275 file.Close()
276
277 // Get a seed to form the request.
278 seed := getNewSeed()
279 // Form a request body for member.create.
280 createMemberReq := requestBodyWithParams{
281 JSONRPC: "2.0",
282 Method: "contract.call",
283 ID: id,
284 Params:params {
285 Seed: seed,
286 CallSite: "member.create",
287 CallParams:memberCreateCallParams {},
288 PublicKey: string(pemPublicKey)},
289 }
290 // Increment the JSON RPC 2.0 request identifier for future requests.
291 id++
292
293 // Send the signed member.create request.
294 newMember := sendSignedRequest(createMemberReq, privateKey)
295
296 // Put the reference to your new member into a variable
297 // to easily form transfer requests.
298 memberReference := newMember["result"].(
299 map[string]interface{})["callResult"].(
300 map[string]interface{})["reference"].(string)
301 fmt.Println("Member reference is " + memberReference)
302
303 // The main function is to be continued...
Now that you have your member reference and key pair, you can transfer funds to other members.
To use the key, follow these steps:
Log in to your Insolar Wallet.
Note
Remember that this tutorial uses Insolar TestNet as an example, so the Wallet must be created there.
Be ready to copy the address.
In a new tab, log in to your Wallet again, open Settings > Reveal private key, enter your password, and click REVEAL.
Be ready to copy the key.
Next, consider the code sample below that does the following:
Uses the private key and elliptic curve to calculate the public key.
Converts both public and private keys into PEM format as the API requires.
Attention
Copy your XNS address and private key and, in the highlighted lines, replace the insolar:XXX...
and HHH...
placeholders respectively. The XNS address is required to form transfer requests as described in the next step.
241 // Continue in the Main.go file...
242
243 // Create the main function to form and send signed requests.
244 func main() {
245 // Log in to your Insolar Wallet, copy the XNS account address,
246 // allocate a variable for it, and paste the value (replace the Xs).
247 memberReference := "insolar:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
248
249 // In the Wallet, open Settings > Reveal private key, enter your password,
250 // copy the key, allocate a variable for it, and paste the value (replace the Hs).
251 hexPrivate := "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH"
252
253 // Declare a new big int variable, specify the key as its value,
254 // and set its format to base16.
255 i := new(big.Int)
256 i.SetString(hexPrivate,16)
257
258 // Create a new elliptic curve and feed the value to it
259 // to get the X and Y values of the public key.
260 privateKey := new(xecdsa.PrivateKey)
261 privateKey.PublicKey.Curve = xelliptic.P256K()
262 privateKey.D = i
263 privateKey.PublicKey.X, privateKey.PublicKey.Y = xelliptic.P256K(
264 ).ScalarBaseMult(i.Bytes())
265
266 // Convert the private key to PEM.
267 x509Encoded, err := x509.MarshalPKCS8PrivateKey(privateKey)
268 if err != nil {
269 panic(err)
270 }
271 pemPrivateKey := pem.EncodeToMemory(&pem.Block{Type:
272 "PRIVATE KEY", Bytes: x509Encoded})
273
274 // Convert the public key to PEM.
275 x509EncodedPub, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
276 if err != nil {
277 panic(err)
278 }
279 pemPublicKey := pem.EncodeToMemory(&pem.Block{Type:
280 "PUBLIC KEY", Bytes: x509EncodedPub})
281 // (Optional) Print the key pair.
282 fmt.Println(string(pemPrivateKey))
283 fmt.Println(string(pemPublicKey))
284
285 // The main function is to be continued...
Now that you’ve calculated the public key and converted both public and private keys to PEM, you can transfer funds to other members.
Step 6: Form and send a transfer request¶
The transfer request is a signed request to a contract method that transfers an amount of funds to another member.
To transfer funds, follow these steps:
In the web interface, send some funds to your XNS address or member reference returned by the member creation request.
Acquire a recipient reference—reference to an existing member to transfer the funds to.
Call the
getNewSeed()
function and put a new seed into a variable.Form a
member.transfer
request body with the following values:A new seed
An amount of funds to transfer
A recipient reference
Your reference (XNS address)—for identification
Your public key—to check the signature
Call the
sendSignedRequest()
function and pass it the body and the private key.
The transfer request responds with a fee value.
For example:
Attention
In the highlighted line, replace the insolar:YYY...
placeholder with the reference to an existing recipient member.
304// Continue in the main() function...
305
306// Get a new seed to form a transfer request.
307seed = getNewSeed()
308
309// Form a request body for the transfer request.
310transferReq := requestBodyWithParams{
311 JSONRPC: "2.0",
312 Method: "contract.call",
313 ID: id,
314 Params:paramsWithReference{ params:params{
315 Seed: seed,
316 CallSite: "member.transfer",
317 CallParams:transferCallParams {
318 Amount: "100",
319 ToMemberReference: "insolar:YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY",
320 },
321 PublicKey: string(pemPublicKey),
322 },
323 Reference: string(memberReference),
324 },
325}
326// Increment the id for future requests.
327id++
328
329// Send the signed transfer request.
330newTransfer := sendSignedRequest(transferReq, privateKey)
331fee := newTransfer["result"].(
332 map[string]interface{})["callResult"].(
333 map[string]interface{})["fee"].(string)
334
335// (Optional) Print out the fee.
336fmt.Println("Fee is " + fee)
337
338// Remember to close the main function.
339}
With that, the requester, as a member, can send funds to other members of the Insolar network.
Step 7: Test the requester¶
To test the requester, do the following:
Make sure the endpoint URL is set to that of TestNet.
Run the requester:
go run Main.go
Summary¶
Congratulations! You have just developed a requester capable of forming signed contract requests to Insolar MainNet API.
Build upon it:
Create structures for other contract requests.
Export the getter and sender functions to use them in other packages.
Full requester code examples¶
Below are the full requester code examples in Golang. Click the panels to expand and click again to hide.
API requester that generates a new private key
Attention
In the highlighted line, replace the insolar:YYY...
placeholder with a reference to an existing recipient member.
1package main
2
3import (
4 // You will need:
5 // - Basic Golang functionality.
6 "bytes"
7 "fmt"
8 "golang.org/x/net/publicsuffix"
9 "io/ioutil"
10 "log"
11 "os"
12 "strconv"
13 // - HTTP client and a cookiejar.
14 "net/http"
15 "net/http/cookiejar"
16 // - Big numbers to store signatures.
17 "math/big"
18 // - Basic cryptography.
19 "crypto/rand"
20 "crypto/sha256"
21 // - Basic encoding capabilities.
22 "encoding/asn1"
23 "encoding/base64"
24 "encoding/json"
25 "encoding/pem"
26 // - A copy of the standard crypto library with
27 // the ECDSA secp256k1 curve implementation.
28 xecdsa "github.com/insolar/x-crypto/ecdsa"
29 xelliptic "github.com/insolar/x-crypto/elliptic"
30 "github.com/insolar/x-crypto/x509"
31)
32
33// Declare a structure to contain the ECDSA signature.
34type ecdsaSignature struct {
35 R, S *big.Int
36}
37
38// Set the endpoint URL to that of TestNet.
39const (
40 TestNetURL = "https://wallet-api.testnet.insolar.io/api/rpc"
41)
42
43// Create and initialize an HTTP client for connection reuse
44// and put a cookiejar into it.
45var client *http.Client
46var jar cookiejar.Jar
47func init() {
48 // All users of cookiejar should import "golang.org/x/net/publicsuffix"
49 jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
50 if err != nil {
51 log.Fatal(err)
52 }
53 client = &http.Client{
54 Jar: jar,
55 }
56}
57
58// Create a variable for the JSON RPC 2.0 request identifier.
59var id int = 1
60// The identifier is to be incremented for every request and
61// each corresponding response will contain it.
62
63// Declare a nested structure to form requests to Insolar API
64// in accordance with the specification.
65// Insolar MainNet uses the basic JSON RPC 2.0 request structure.
66type requestBody struct {
67 JSONRPC string `json:"jsonrpc"`
68 ID int `json:"id"`
69 Method string `json:"method"`
70}
71
72type requestBodyWithParams struct {
73 JSONRPC string `json:"jsonrpc"`
74 ID int `json:"id"`
75 Method string `json:"method"`
76 // Params is a structure that depends on a particular method.
77 Params interface{} `json:"params"`
78}
79
80// Insolar MainNet defines params of the signed request as follows.
81type params struct {
82 Seed string `json:"seed"`
83 CallSite string `json:"callSite"`
84 // CallParams is a structure that depends on a particular method.
85 CallParams interface{} `json:"callParams"`
86 PublicKey string `json:"publicKey"`
87}
88
89type paramsWithReference struct {
90 params
91 Reference string `json:"reference"`
92}
93
94// The member.create request has no parameters,
95// so it's an empty structure:
96type memberCreateCallParams struct {}
97
98// The transfer request sends an amount of funds
99// to the member identified by a reference:
100type transferCallParams struct {
101 Amount string `json:"amount"`
102 ToMemberReference string `json:"toMemberReference"`
103}
104
105// Create a function to get a new seed for each signed request.
106func getNewSeed() string {
107 // Form a request body for getSeed:
108 getSeedReq := requestBody{
109 JSONRPC: "2.0",
110 Method: "node.getSeed",
111 ID: id,
112 }
113 // Increment the id for future requests.
114 id++
115
116 // Marshal the payload into JSON.
117 jsonSeedReq, err := json.Marshal(getSeedReq)
118 if err != nil {
119 log.Fatalln(err)
120 }
121
122 // Create a new HTTP request and send it.
123 seedReq, err := http.NewRequest("POST", TestNetURL,
124 bytes.NewBuffer(jsonSeedReq))
125 if err != nil {
126 log.Fatalln(err)
127 }
128 seedReq.Header.Set("Content-Type", "application/json")
129
130 // Perform the request.
131 seedResponse, err := client.Do(seedReq)
132 if err != nil {
133 log.Fatalln(err)
134 }
135 defer seedReq.Body.Close()
136
137 // Receive the response body.
138 seedRespBody, err := ioutil.ReadAll(seedResponse.Body)
139 if err != nil {
140 log.Fatalln(err)
141 }
142
143 // Unmarshal the response.
144 var newSeed map[string]interface{}
145 err = json.Unmarshal(seedRespBody, &newSeed)
146 if err != nil {
147 log.Fatalln(err)
148 }
149
150 // (Optional) Print the request and its response.
151 print := "POST to " + TestNetURL +
152 "\nPayload: " + string(jsonSeedReq) +
153 "\nResponse status code: " + strconv.Itoa(seedResponse.StatusCode) +
154 "\nResponse: " + string(seedRespBody) + "\n"
155 fmt.Println(print)
156
157 // Retrieve and return the current seed.
158 return newSeed["result"].(map[string]interface{})["seed"].(string)
159}
160
161// Create a function to send signed requests.
162func sendSignedRequest(payload requestBodyWithParams,
163 privateKey *xecdsa.PrivateKey) map[string]interface{} {
164 // Marshal the payload into JSON:
165 jsonPayload, err := json.Marshal(payload)
166 if err != nil {
167 log.Fatalln(err)
168 }
169
170 // Take a SHA-256 hash of the payload's bytes.
171 hash := sha256.Sum256(jsonPayload)
172
173 // Sign the hash with the private key.
174 r, s, err := xecdsa.Sign(rand.Reader, privateKey, hash[:])
175 if err != nil {
176 log.Fatalln(err)
177 }
178
179 // Convert the signature into ASN.1 DER format.
180 sig := ecdsaSignature{
181 R: r,
182 S: s,
183 }
184 signature, err := asn1.Marshal(sig)
185 if err != nil {
186 log.Fatalln(err)
187 }
188
189 // Convert both hash and signature into a Base64 string.
190 hash64 := base64.StdEncoding.EncodeToString(hash[:])
191 signature64 := base64.StdEncoding.EncodeToString(signature)
192
193 // Create a new request and set its headers.
194 request, err := http.NewRequest("POST", TestNetURL,
195 bytes.NewBuffer(jsonPayload))
196 if err != nil {
197 log.Fatalln(err)
198 }
199 request.Header.Set("Content-Type", "application/json")
200
201 // Put the hash string into the HTTP Digest header.
202 request.Header.Set("Digest", "SHA-256="+hash64)
203
204 // Put the signature string into the HTTP Signature header.
205 request.Header.Set("Signature", "keyId=\"public-key\", " +
206 "algorithm=\"ecdsa\", headers=\"digest\", signature="+signature64)
207
208 // Send the signed request.
209 response, err := client.Do(request)
210 if err != nil {
211 log.Fatalln(err)
212 }
213 defer response.Body.Close()
214
215 // Receive the response body.
216 responseBody, err := ioutil.ReadAll(response.Body)
217 if err != nil {
218 log.Fatalln(err)
219 }
220
221 // Unmarshal it into a JSON object.
222 var JSONObject map[string]interface{}
223 err = json.Unmarshal(responseBody, &JSONObject)
224 if err != nil {
225 log.Fatalln(err)
226 }
227
228 // (Optional) Print the request and its response.
229 print := "POST to " + TestNetURL +
230 "\nPayload: " + string(jsonPayload) +
231 "\nResponse status code: " + strconv.Itoa(response.StatusCode) +
232 "\nResponse: " + string(responseBody) + "\n"
233 fmt.Println(print)
234
235 // Return the response.
236 return JSONObject
237}
238
239// Create the main function to form and send signed requests.
240func main() {
241 // Generate a key pair:
242 privateKey := new(xecdsa.PrivateKey)
243 privateKey, err := xecdsa.GenerateKey(xelliptic.P256(), rand.Reader)
244 var publicKey xecdsa.PublicKey
245 publicKey = privateKey.PublicKey
246
247 // Convert both private and public keys into PEM format.
248 x509PublicKey, err := x509.MarshalPKIXPublicKey(&publicKey)
249 if err != nil {
250 log.Fatalln(err)
251 }
252 pemPublicKey := pem.EncodeToMemory(&pem.Block{Type:
253 "PUBLIC KEY", Bytes: x509PublicKey})
254
255 x509PrivateKey, err := x509.MarshalECPrivateKey(privateKey)
256 if err != nil {
257 log.Fatalln(err)
258 }
259 pemPrivateKey := pem.EncodeToMemory(&pem.Block{Type:
260 "PRIVATE KEY", Bytes: x509PrivateKey})
261
262 // The private key is required to sign requests.
263 // Make sure to put it into a file to save it in a secure place later.
264 file, err := os.Create("private.pem")
265 if err != nil {
266 fmt.Println(err)
267 return
268 }
269 file.WriteString(string(pemPrivateKey))
270 file.Close()
271
272 // Get a seed to form the request:
273 seed := getNewSeed()
274 // Form a request body for member.create:
275 createMemberReq := requestBodyWithParams{
276 JSONRPC: "2.0",
277 Method: "contract.call",
278 ID: id,
279 Params:params {
280 Seed: seed,
281 CallSite: "member.create",
282 CallParams:memberCreateCallParams {},
283 PublicKey: string(pemPublicKey)},
284 }
285 // Increment the JSON RPC 2.0 request identifier for future requests.
286 id++
287
288 // Send the signed member.create request.
289 newMember := sendSignedRequest(createMemberReq, privateKey)
290
291 // Put the reference to your new member into a variable
292 // to send transfer requests.
293 memberReference := newMember["result"].(
294 map[string]interface{})["callResult"].(
295 map[string]interface{})["reference"].(string)
296 fmt.Println("Member reference is " + memberReference)
297
298 // Get a new seed to form a transfer request.
299 seed = getNewSeed()
300 // Form a request body for transfer.
301 transferReq := requestBodyWithParams{
302 JSONRPC: "2.0",
303 Method: "contract.call",
304 ID: id,
305 Params:paramsWithReference{ params:params{
306 Seed: seed,
307 CallSite: "member.transfer",
308 CallParams:transferCallParams {
309 Amount: "10000000",
310
311 ToMemberReference: "insolar:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
312
313 },
314 PublicKey: string(pemPublicKey),
315 },
316 Reference: string(memberReference),
317 },
318 }
319 // Increment the id for future requests:
320 id++
321
322 // Send the signed transfer request.
323 newTransfer := sendSignedRequest(transferReq, privateKey)
324 fee := newTransfer["result"].(
325 map[string]interface{})["callResult"].(
326 map[string]interface{})["fee"].(string)
327
328 // (Optional) Print out the fee.
329 fmt.Println("Fee is " + fee)
330}
API requester that uses an existing private key
Attention
In the highlighted lines, replace the placeholders:
insolar:XXX...
with your XNS addressHHH...
with your private keyYYY...
with the XNS address of the recipient
1package main
2
3import (
4 // You will need:
5 // - Basic Golang functionality.
6 "bytes"
7 "fmt"
8 "golang.org/x/net/publicsuffix"
9 "io/ioutil"
10 "log"
11 "strconv"
12 // - HTTP client and a cookiejar.
13 "net/http"
14 "net/http/cookiejar"
15 // - Big numbers to store signatures.
16 "math/big"
17 // - Basic cryptography.
18 "crypto/rand"
19 "crypto/sha256"
20 "encoding/asn1"
21 "encoding/base64"
22 "encoding/json"
23 // - Basic encoding capabilities.
24 "encoding/pem"
25 // - A copy of the standard crypto library with
26 // the ECDSA secp256k1 curve implementation.
27 xecdsa "github.com/insolar/x-crypto/ecdsa"
28 xelliptic "github.com/insolar/x-crypto/elliptic"
29 "github.com/insolar/x-crypto/x509"
30)
31
32// Declare a structure to contain the ECDSA signature.
33type ecdsaSignature struct {
34 R, S *big.Int
35}
36
37// Set the endpoint URL to that of TestNet.
38const (
39 TestNetURL = "https://wallet-api.testnet.insolar.io/api/rpc"
40)
41
42// Create and initialize an HTTP client for connection reuse
43// and put a cookiejar into it.
44var client *http.Client
45var jar cookiejar.Jar
46func init() {
47 // All users of cookiejar should import "golang.org/x/net/publicsuffix"
48 jar, err := cookiejar.New(&cookiejar.Options{
49 PublicSuffixList: publicsuffix.List})
50 if err != nil {
51 log.Fatal(err)
52 }
53 client = &http.Client{
54 Jar: jar,
55 }
56}
57
58// Create a variable for the JSON RPC 2.0 request identifier.
59var id int = 1
60// The identifier is to be incremented for every request and
61// each corresponding response will contain it.
62
63// Declare a nested structure to form requests to Insolar API
64// in accordance with the specification.
65// The Platform uses the basic JSON RPC 2.0 request structure.
66type requestBody struct {
67 JSONRPC string `json:"jsonrpc"`
68 ID int `json:"id"`
69 Method string `json:"method"`
70}
71
72type requestBodyWithParams struct {
73 JSONRPC string `json:"jsonrpc"`
74 ID int `json:"id"`
75 Method string `json:"method"`
76 // Params is a structure that depends on a particular method.
77 Params interface{} `json:"params"`
78}
79
80// The Platform defines params of the signed request as follows.
81type params struct {
82 Seed string `json:"seed"`
83 CallSite string `json:"callSite"`
84 // CallParams is a structure that depends on a particular method.
85 CallParams interface{} `json:"callParams"`
86 PublicKey string `json:"publicKey"`
87}
88
89type paramsWithReference struct {
90 params
91 Reference string `json:"reference"`
92}
93
94// The member.create request has no parameters,
95// so it's an empty structure.
96type memberCreateCallParams struct {}
97
98// The transfer request sends an amount of funds to
99// the member identified by a reference.
100type transferCallParams struct {
101 Amount string `json:"amount"`
102 ToMemberReference string `json:"toMemberReference"`
103}
104
105// Create a function to get a new seed for each signed request.
106func getNewSeed() string {
107 // Form a request body for getSeed:
108 getSeedReq := requestBody{
109 JSONRPC: "2.0",
110 Method: "node.getSeed",
111 ID: id,
112 }
113 // Increment the id for future requests.
114 id++
115
116 // Marshal the payload into JSON:
117 jsonSeedReq, err := json.Marshal(getSeedReq)
118 if err != nil {
119 log.Fatalln(err)
120 }
121
122 // Create a new HTTP request and send it.
123 seedReq, err := http.NewRequest("POST", TestNetURL,
124 bytes.NewBuffer(jsonSeedReq))
125 if err != nil {
126 log.Fatalln(err)
127 }
128 seedReq.Header.Set("Content-Type", "application/json")
129
130 // Perform the request.
131 seedResponse, err := client.Do(seedReq)
132 if err != nil {
133 log.Fatalln(err)
134 }
135 defer seedReq.Body.Close()
136
137 // Receive the response body.
138 seedRespBody, err := ioutil.ReadAll(seedResponse.Body)
139 if err != nil {
140 log.Fatalln(err)
141 }
142
143 // Unmarshal the response.
144 var newSeed map[string]interface{}
145 err = json.Unmarshal(seedRespBody, &newSeed)
146 if err != nil {
147 log.Fatalln(err)
148 }
149
150 // (Optional) Print the request and its response.
151 print := "POST to " + TestNetURL +
152 "\nPayload: " + string(jsonSeedReq) +
153 "\nResponse status code: " + strconv.Itoa(seedResponse.StatusCode) +
154 "\nResponse: " + string(seedRespBody) + "\n"
155 fmt.Println(print)
156
157 // Retrieve and return the current seed.
158 return newSeed["result"].(map[string]interface{})["seed"].(string)
159}
160
161// Create a function to send signed requests.
162func sendSignedRequest(payload requestBodyWithParams,
163 privateKey *xecdsa.PrivateKey) map[string]interface{} {
164 // Marshal the payload into JSON:
165 jsonPayload, err := json.Marshal(payload)
166 if err != nil {
167 log.Fatalln(err)
168 }
169
170 // Take a SHA-256 hash of the payload's bytes.
171 hash := sha256.Sum256(jsonPayload)
172
173 // Sign the hash with the private key.
174 r, s, err := xecdsa.Sign(rand.Reader, privateKey, hash[:])
175 if err != nil {
176 log.Fatalln(err)
177 }
178
179 // Convert the signature into ASN.1 DER format.
180 sig := ecdsaSignature{
181 R: r,
182 S: s,
183 }
184 signature, err := asn1.Marshal(sig)
185 if err != nil {
186 log.Fatalln(err)
187 }
188
189 // Convert both hash and signature into a Base64 string.
190 hash64 := base64.StdEncoding.EncodeToString(hash[:])
191 signature64 := base64.StdEncoding.EncodeToString(signature)
192
193 // Create a new request and set its headers.
194 request, err := http.NewRequest("POST", TestNetURL,
195 bytes.NewBuffer(jsonPayload))
196 if err != nil {
197 log.Fatalln(err)
198 }
199 request.Header.Set("Content-Type", "application/json")
200
201 // Put the hash string into the HTTP Digest header.
202 request.Header.Set("Digest", "SHA-256="+hash64)
203
204 // Put the signature string into the HTTP Signature header.
205 request.Header.Set("Signature", "keyId=\"public-key\", " +
206 "algorithm=\"ecdsa\", headers=\"digest\", signature="+signature64)
207
208 // Send the signed request.
209 response, err := client.Do(request)
210 if err != nil {
211 log.Fatalln(err)
212 }
213 defer response.Body.Close()
214
215 // Receive the response body.
216 responseBody, err := ioutil.ReadAll(response.Body)
217 if err != nil {
218 log.Fatalln(err)
219 }
220
221 // Unmarshal it into a JSON object.
222 var JSONObject map[string]interface{}
223 err = json.Unmarshal(responseBody, &JSONObject)
224 if err != nil {
225 log.Fatalln(err)
226 }
227
228 // (Optional) Print the request and its response.
229 print := "POST to " + TestNetURL +
230 "\nPayload: " + string(jsonPayload) +
231 "\nResponse status code: " + strconv.Itoa(response.StatusCode) +
232 "\nResponse: " + string(responseBody) + "\n"
233 fmt.Println(print)
234
235 // Return the response.
236 return JSONObject
237}
238
239// Create the main function to form and send signed requests.
240func main() {
241 // Log in to your Insolar Wallet, copy the XNS account address,
242 // allocate a variable for it, and paste the value (replace the Xs).
243 memberReference := "insolar:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
244
245 // In the Wallet, open Settings > Reveal private key, enter your password,
246 // copy the key, allocate a variable for it, and paste the value (replace the Hs).
247 hexPrivate := "HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH"
248
249 // Declare a new big int variable, specify the key as its value,
250 // and set its format to base 16.
251 i := new(big.Int)
252 i.SetString(hexPrivate,16)
253
254 // Create a new elliptic curve and feed the value to it
255 // to get the X and Y values of the public key.
256 privateKey := new(xecdsa.PrivateKey)
257 privateKey.PublicKey.Curve = xelliptic.P256K()
258 privateKey.D = i
259 privateKey.PublicKey.X, privateKey.PublicKey.Y = xelliptic.P256K(
260 ).ScalarBaseMult(i.Bytes())
261
262 // Convert the private key to PEM.
263 x509Encoded, err := x509.MarshalPKCS8PrivateKey(privateKey)
264 if err != nil {
265 panic(err)
266 }
267 pemPrivateKey := pem.EncodeToMemory(&pem.Block{Type:
268 "PRIVATE KEY", Bytes: x509Encoded})
269
270 // Convert the public key to PEM.
271 x509EncodedPub, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
272 if err != nil {
273 panic(err)
274 }
275 pemPublicKey := pem.EncodeToMemory(&pem.Block{Type:
276 "PUBLIC KEY", Bytes: x509EncodedPub})
277 // (Optional) Print the key pair.
278 fmt.Println(string(pemPrivateKey))
279 fmt.Println(string(pemPublicKey))
280
281 // Get a new seed to form a transfer request.
282 seed := getNewSeed()
283 // Form a request body for transfer:
284 transferReq := requestBodyWithParams{
285 JSONRPC: "2.0",
286 Method: "contract.call",
287 ID: id,
288 Params:paramsWithReference{ params:params{
289 Seed: seed,
290 CallSite: "member.transfer",
291 CallParams:transferCallParams {
292 Amount: "10000000",
293 ToMemberReference: "insolar:YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY",
294 },
295 PublicKey: string(pemPublicKey),
296 },
297 Reference: string(memberReference),
298 },
299 }
300 // Increment the id for future requests.
301 id++
302
303 // Send the signed transfer request.
304 newTransfer := sendSignedRequest(transferReq, privateKey)
305 fee := newTransfer["result"].(
306 map[string]interface{})["callResult"].(
307 map[string]interface{})["fee"].(string)
308
309 // (Optional) Print out the fee.
310 fmt.Println("Fee is " + fee)
311}