forked from bpancost/sila
-
Notifications
You must be signed in to change notification settings - Fork 0
/
client.go
437 lines (383 loc) · 14.7 KB
/
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
package sila
import (
"bytes"
"crypto/ecdsa"
"encoding/hex"
"encoding/json"
"fmt"
"math/big"
"mime/multipart"
"net/http"
"net/textproto"
"strings"
"sync"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/pkg/errors"
)
type Client interface {
// Determine if a handle is currently on use in the network
CheckHandle(userHandle string) CheckHandle
// Register an individual or business user to a handle, add basic KYC data, and an initial wallet
Register(userOrBusinessHandle string) Register
// Request KYC for an individual user
RequestKyc(userHandle string) RequestKyc
// Check the status of KYC for an individual user
CheckKyc(userHandle string) CheckKyc
// Gets an information about a user
GetEntity(userHandle string) GetEntity
// Searches for user information
GetEntities() GetEntities
// Links an individual user to a business user, indicating the individual is part of that business
LinkBusinessMember(userHandle string, businessHandle string) LinkBusinessMember
// Unlinks an individual user from a business user, indicating that they are not part of that business
UnlinkBusinessMember(userHandle string, businessHandle string) UnlinkBusinessMember
// Have a business's admin user certify that the information about a beneficial owner (who has ownership stake in the business) has correct information
CertifyBeneficialOwner(adminUserHandle string, businessHandle string) CertifyBeneficialOwner
// Have a business's admin user certify that all information about a business has been entered correctly and the business should be allowed to transact on the network
CertifyBusiness(adminUserHandle string, businessHandle string) CertifyBusiness
// Add registration data to a user after registration
AddRegistrationData(userHandle string) AddRegistrationData
// Update a user's registration data after registration
UpdateRegistrationData(userHandle string) UpdateRegistrationData
// Delete a user's registration data after registration
DeleteRegistrationData(userHandle string) DeleteRegistrationData
// Documents begins an upload documents request
Documents(userHandle string) DocumentUpload
// Link a bank account to a user, either directly or via Plaid
LinkAccount(userHandle string) LinkAccount
// Complete same day auth with Plaid
PlaidSameDayAuth(userHandle string, accountName string) PlaidSameDayAuth
// Get a user's linked accounts
GetAccounts(userHandle string) GetAccounts
// Get the balances for a user's linked accounts
GetAccountBalance(userHandle string, accountName string) GetAccountBalance
// Register a new wallet to a user
RegisterWallet(userHandle string) RegisterWallet
// Get a user's wallet
GetWallet(userHandle string) GetWallet
// Get several of a user's wallets
GetWallets(userHandle string) GetWallets
// Update information about a user's wallet
UpdateWallet(userHandle string) UpdateWallet
// Get the current Sila coin balance of a user's wallet
GetWalletBalance(walletAddress string) GetSilaBalance
// Delete a user's wallet
DeleteWallet(userHandle string) DeleteWallet
// Issue Sila coin to a wallet from a linked bank account
IssueSila(userHandle string) IssueSila
// Transfer Sila coin from one wallet to another
TransferSila(userHandle string) TransferSila
// Redeem Sila coin from a user's wallet to a linked bank account
RedeemSila(userHandle string) RedeemSila
// Get a list of transactions related to a user's wallet
GetTransactions(userHandle string) GetTransactions
// Cancel a user's transaction if still in progress
CancelTransaction(userHandle string, transactionId string) CancelTransactions
// Get a list of business types
GetBusinessTypes() GetBusinessTypes
// Get a list of business roles
GetBusinessRoles() GetBusinessRoles
// Get a list of NAICS categories and their codes
GetNaicsCategories() GetNaicsCategories
}
// The Sila client for handling calls to the Sila API
type ClientImpl struct {
privateKey *ecdsa.PrivateKey
authHandle string
version string
crypto string
environment Environment
}
// Which API environment to run in
type Environment string
const (
Sandbox Environment = "https://sandbox.silamoney.com/"
Production = "https://api.silamoney.com/"
)
var once sync.Once
var (
// A singleton instance for the client
instance *ClientImpl
)
// Generates a URL for the current environment given the API version and the path to invoke
func (env Environment) generateURL(version string, path string) string {
return string(env) + version + path
}
// Creates a new Sila client using your system's auth private key as a hex string, your system's auth handle, and the
// environment to send requests to (sandbox or production).
func NewClient(privateKeyHex string, authHandle string, environment Environment) (Client, error) {
privateKey, err := crypto.HexToECDSA(privateKeyHex)
if err != nil {
return nil, errors.Errorf("private key invalid, make sure it is hex without the 0x prefix: %v", err)
}
once.Do(func() {
instance = &ClientImpl{
privateKey: privateKey,
authHandle: authHandle,
version: "0.2",
crypto: "ETH",
environment: environment,
}
})
return instance, nil
}
// Generates a signature for a request's body using the provided private key.
func generateSignatureFromKey(requestBody []byte, privateKey *ecdsa.PrivateKey) (string, error) {
// Follows the Sila example for Golang
// Generate the message hash using the Keccak 256 algorithm.
msgHash := crypto.Keccak256(requestBody)
// Create a signature using your private key and hashed message.
sigBytes, err := crypto.Sign(msgHash, privateKey)
if err != nil {
return "", err
}
// The signature just created is off by -27 from what the API
// will expect. Correct that by converting the signature bytes
// to a big int and adding 27.
var offset int64 = 27
var bigSig = new(big.Int).SetBytes(sigBytes)
sigBytes = bigSig.Add(bigSig, big.NewInt(offset)).Bytes()
// The big library takes out any padding, but the resultant
// signature must be 130 characters (65 bytes) long. In some
// cases, you might find that sigBytes now has a length of 64 or
// less, so you can fix that in this way (this prepends the hex
// value with "0" until the requisite length is reached).
// Example: if two digits were required but the value was 1, you'd
// pass in 01.
var sigBytesLength = 65 // length of a valid signature byte array
var arr = make([]byte, sigBytesLength)
copy(arr[(sigBytesLength-len(sigBytes)):], sigBytes)
// Encode the bytes to a hex string.
return hex.EncodeToString(arr), nil
}
// Generates a signature for a message with your system's private auth key from the client creation.
func (client ClientImpl) generateAuthSignature(message []byte) (string, error) {
return generateSignatureFromKey(message, client.privateKey)
}
// Perform a call to the API at some path with the included request and a pointer to the response struct
func (client *ClientImpl) performCall(path string, requestBody interface{}, responseBody interface{}) error {
requestJson, err := json.Marshal(requestBody)
if err != nil {
return nil
}
url := instance.environment.generateURL(instance.version, path)
request, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(requestJson))
if err != nil {
return err
}
request.Header.Set("Content-type", "application/json")
authSignature, err := instance.generateAuthSignature(requestJson)
if err != nil {
return errors.Errorf("failed to generate auth signature: %v", err)
}
request.Header.Set("authsignature", authSignature)
httpClient := http.Client{}
resp, err := httpClient.Do(request)
if err != nil {
return err
}
defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(&responseBody)
if err != nil {
return err
}
return nil
}
// Perform a call to the API at some path signed by a user's wallet private key, with the included request and a pointer to the response struct
func (client *ClientImpl) performCallWithUserAuth(path string, requestBody interface{}, responseBody interface{}, userWalletPrivateKey string) error {
requestJson, err := json.Marshal(requestBody)
if err != nil {
return nil
}
url := instance.environment.generateURL(instance.version, path)
request, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(requestJson))
if err != nil {
return err
}
request.Header.Set("Content-type", "application/json")
authSignature, err := instance.generateAuthSignature(requestJson)
if err != nil {
return errors.Errorf("failed to generate auth signature: %v", err)
}
request.Header.Set("authsignature", authSignature)
userSignature, err := GenerateWalletSignature(requestJson, userWalletPrivateKey)
if err != nil {
return errors.Errorf("failed to generate user signature: %v", err)
}
request.Header.Set("usersignature", userSignature)
httpClient := http.Client{}
resp, err := httpClient.Do(request)
if err != nil {
return err
}
defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(&responseBody)
if err != nil {
return err
}
return nil
}
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
func escapeQuotes(s string) string {
return quoteEscaper.Replace(s)
}
// Perform a call to the API at some path signed by a user's wallet private key, with the included request and a pointer to the response struct
func (client *ClientImpl) performCallMultipartWithUserAuth(path string, files map[string][]byte, payload DocumentUploadData, responseBody interface{}, userWalletPrivateKey string) error {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
for name, content := range files {
h := make(textproto.MIMEHeader)
h.Set("Content-Disposition",
fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
escapeQuotes(name), escapeQuotes(payload.FileMetadata[name].Filename)))
h.Set("Content-Type", payload.FileMetadata[name].MimeType)
part, err := writer.CreatePart(h)
if err != nil {
return err
}
part.Write(content)
}
requestJson, err := json.Marshal(payload)
if err != nil {
return nil
}
payloadWriter, err := writer.CreateFormField("data")
if err != nil {
return err
}
payloadWriter.Write(requestJson)
err = writer.Close()
if err != nil {
return err
}
url := instance.environment.generateURL(instance.version, path)
request, err := http.NewRequest(http.MethodPost, url, body)
if err != nil {
return err
}
request.Header.Set("Content-Type", writer.FormDataContentType())
authSignature, err := instance.generateAuthSignature(requestJson)
if err != nil {
return errors.Errorf("failed to generate auth signature: %v", err)
}
request.Header.Set("authsignature", authSignature)
userSignature, err := GenerateWalletSignature(requestJson, userWalletPrivateKey)
if err != nil {
return errors.Errorf("failed to generate user signature: %v", err)
}
request.Header.Set("usersignature", userSignature)
httpClient := http.Client{}
resp, err := httpClient.Do(request)
if err != nil {
return err
}
defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(&responseBody)
if err != nil {
return err
}
return nil
}
// Perform a call to the API at some path signed by a user's wallet private key and a business's wallet key, with the included request and a pointer to the response struct
func (client *ClientImpl) performCallWithUserAndBusinessAuth(path string, requestBody interface{}, responseBody interface{}, userWalletPrivateKey string, businessWalletPrivateKey string) error {
requestJson, err := json.Marshal(requestBody)
if err != nil {
return nil
}
url := instance.environment.generateURL(instance.version, path)
request, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(requestJson))
if err != nil {
return err
}
request.Header.Set("Content-type", "application/json")
authSignature, err := instance.generateAuthSignature(requestJson)
if err != nil {
return errors.Errorf("failed to generate auth signature: %v", err)
}
request.Header.Set("authsignature", authSignature)
userSignature, err := GenerateWalletSignature(requestJson, userWalletPrivateKey)
if err != nil {
return errors.Errorf("failed to generate user signature: %v", err)
}
request.Header.Set("usersignature", userSignature)
businessSignature, err := GenerateWalletSignature(requestJson, businessWalletPrivateKey)
if err != nil {
return errors.Errorf("failed to generate business signature: %v", err)
}
request.Header.Set("businesssignature", businessSignature)
httpClient := http.Client{}
resp, err := httpClient.Do(request)
if err != nil {
return err
}
defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(&responseBody)
if err != nil {
return err
}
return nil
}
// Perform a public (no auth required) call to the API at some path with the included request and a pointer to the response struct
func (client *ClientImpl) performPublicCall(path string, requestBody interface{}, responseBody interface{}) error {
requestJson, err := json.Marshal(requestBody)
if err != nil {
return nil
}
url := instance.environment.generateURL(instance.version, path)
request, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(requestJson))
if err != nil {
return err
}
request.Header.Set("Content-type", "application/json")
httpClient := http.Client{}
resp, err := httpClient.Do(request)
if err != nil {
return err
}
defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(&responseBody)
if err != nil {
return err
}
return nil
}
// Gets a wallet address from a wallet's private key as a hex string and returns the wallet address
func GetWalletAddress(privateKeyHex string) (string, error) {
publicKeyECDSA, err := getPublicKeyFromPrivateHex(privateKeyHex)
if err != nil {
return "", err
}
address := crypto.PubkeyToAddress(*publicKeyECDSA).Hex()
return address, nil
}
// Gets a public key from a private key hex string
func getPublicKeyFromPrivateHex(privateKeyHex string) (*ecdsa.PublicKey, error) {
privateKey, err := crypto.HexToECDSA(privateKeyHex)
if err != nil {
return nil, err
}
publicKeyECDSA, ok := privateKey.Public().(*ecdsa.PublicKey)
if !ok {
return nil, errors.New("error casting public key to ECDSA")
}
return publicKeyECDSA, nil
}
// Generates a new private key as a hex string for a wallet
func GenerateNewPrivateKey() (string, error) {
pk, err := crypto.GenerateKey()
if err != nil {
return "", err
}
pkBytes := crypto.FromECDSA(pk)
pkHex := hexutil.Encode(pkBytes)[2:]
return pkHex, nil
}
// Generates a signature for a message with one of a user's wallet private keys (in hex) as provided.
func GenerateWalletSignature(message []byte, walletPrivateKeyHex string) (string, error) {
privateKey, err := crypto.HexToECDSA(walletPrivateKeyHex)
if err != nil {
return "", errors.Errorf("private key invalid, make sure it is hex without the 0x prefix: %v", err)
}
return generateSignatureFromKey(message, privateKey)
}