Skip to content

Commit

Permalink
Merge pull request #45 from cypherhat/master
Browse files Browse the repository at this point in the history
Add method to sign generic transactions so they can be sent independently of plugin
  • Loading branch information
cypherhat committed Oct 14, 2018
2 parents 66f3c2e + c81c7b9 commit 4575e39
Show file tree
Hide file tree
Showing 3 changed files with 406 additions and 31 deletions.
65 changes: 65 additions & 0 deletions API.md
Expand Up @@ -11,6 +11,7 @@ Vault provides a CLI that wraps the Vault REST interface. Any HTTP client (inclu
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;` │   └── <NAME> `&nbsp;&nbsp;([create](./API.md#create-account), [update](./API.md#update-account), [read](./API.md#read-account), [delete](./API.md#delete-account))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;` │      ├── debit `&nbsp;&nbsp;([update](./API.md#debit-account))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;` │      ├── sign `&nbsp;&nbsp;([update](./API.md#sign))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;` │      ├── sign-tx `&nbsp;&nbsp;([update](./API.md#sign-tx))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;` │      ├── transfer `&nbsp;&nbsp;([update](./API.md#transfer))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;` │      └── verify `&nbsp;&nbsp;([update](./API.md#verify))
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;` ├── addresses `&nbsp;&nbsp;([list](./API.md#list-addresses))
Expand Down Expand Up @@ -332,6 +333,70 @@ $ curl -s --cacert /etc/vault.d/root.crt --header "X-Vault-Token: $VAULT_TOKEN"

The example below shows the output for the successfully sending ETH from `/ethereum/accounts/test2`. The Transaction hash is returned.

```
{
"request_id": "b921207e-c0d9-a3c1-442b-ef8b1884238d",
"lease_id": "",
"lease_duration": 0,
"renewable": false,
"data": {
"amount": "100000000000000000",
"amount_in_usd": "0",
"address_from": "0x4169c9508728285e8a9f7945d08645bb6b3576e5",
"address_to": "0x8AC5e6617F65c071f6dD5d7bD400bf4a46434D41",
"gas_limit": "21000",
"gas_price": "1000000000",
"signed_transaction": "0xf86b06843b9aca00825208948ac5e6617f65c071f6dd5d7bd400bf4a46434d4188016345785d8a0000802ca0ff3fccbde1964047db6be33410436a9220c91ea4080b0e14489dc35fbdabd008a0448fe3ec216a639e1b0eb87b0e4b20aab2e5ec46dad4c38cfc81a1c54e309d21",
"starting_balance": 8460893507395267000,
"starting_balance_in_usd": "0",
"total_spend": "100000000000000000",
"transaction_hash": "0x3a103587ea6bdeee944e5f68f90ed7b1f4c7699236167d1b1d29495b0319fb26"
},
"warnings": null
}
```
### SIGN-TX

This endpoint will sign the provided transaction.

| Method | Path | Produces |
| ------------- | ------------- | ------------- |
| `POST` | `:mount-path/accounts/:name/sign-tx` | `200 application/json` |

#### Parameters

* `name` (`string: <required>`) - Specifies the name of the account to use for signing. This is specified as part of the URL.
* `address_to` (`string: <required>`) - A Hex string specifying the Ethereum address to send the ETH to.
* `amount` (`string: <required>`) - The amount of ether - in wei.
* `gas_price` (`string: <optional>`) - The price in gas for the transaction. If omitted, we will use the suggested gas price.
* `gas_limit` (`string: <optional>`) - The gas limit for the transaction. If omitted, we will estimate the gas limit.
* `nonce` (`string: <optional>`) - The nonce for the transaction. If omitted or zero, we will use the suggested nonce.
* `data` (`string: <required>`) - Transaction data to sign.

#### Sample Payload

```sh

{
"amount":"200000000000000000",
"to": "0x36D1F896E55a6577C62FDD6b84fbF74582266700",
"data": "transaction data"
}
```

#### Sample Request

```sh
$ curl -s --cacert ~/etc/vault.d/root.crt --header "X-Vault-Token: $VAULT_TOKEN" \
--request POST \
--data @payload.json \
https://localhost:8200/v1/ethereum/accounts/test2/sign-tx | jq .
```

#### Sample Response

The example below shows output for the successful signing of a transaction by the private key associated with `/ethereum/accounts/test2`.

```
{
"request_id": "b921207e-c0d9-a3c1-442b-ef8b1884238d",
Expand Down
159 changes: 159 additions & 0 deletions path_accounts.go
Expand Up @@ -147,6 +147,54 @@ Send ETH from an account.
logical.CreateOperation: b.pathDebit,
},
},
&framework.Path{
Pattern: "accounts/" + framework.GenericNameRegex("name") + "/sign-tx",
HelpSynopsis: "Sign a provided transaction. ",
HelpDescription: `
Send ETH from an account.
`,
Fields: map[string]*framework.FieldSchema{
"name": &framework.FieldSchema{Type: framework.TypeString},
"address_to": &framework.FieldSchema{
Type: framework.TypeString,
Description: "The address of the account to send ETH to.",
},
"data": &framework.FieldSchema{
Type: framework.TypeString,
Description: "The data to sign.",
},
"amount": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Amount of ETH (in wei).",
},
"nonce": &framework.FieldSchema{
Type: framework.TypeString,
Description: "The transaction nonce.",
},
"gas_limit": &framework.FieldSchema{
Type: framework.TypeString,
Description: "The gas limit for the transaction - defaults to 21000.",
Default: "21000",
},
"gas_price": &framework.FieldSchema{
Type: framework.TypeString,
Description: "The gas price for the transaction in wei.",
Default: "0",
},
"send": &framework.FieldSchema{
Type: framework.TypeBool,
Description: "Send the transaction to the network.",
Default: true,
},
},
ExistenceCheck: b.pathExistenceCheck,
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.CreateOperation: b.pathSignTx,
logical.UpdateOperation: b.pathSignTx,
},
},
&framework.Path{
Pattern: "accounts/" + framework.GenericNameRegex("name") + "/transfer",
HelpSynopsis: "Transfer ERC20 tokens.",
Expand Down Expand Up @@ -544,6 +592,117 @@ func ValidNumber(input string) *big.Int {
return amount.Abs(amount)
}

func (b *EthereumBackend) pathSignTx(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
config, err := b.configured(ctx, req)
if err != nil {
return nil, err
}

name := data.Get("name").(string)
dataToSign := data.Get("data").(string)
account, err := b.readAccount(ctx, req, name)
if err != nil {
return nil, fmt.Errorf("error reading account")
}
if account == nil {
return nil, nil
}
balance, _, exchangeValue, err := b.readAccountBalance(ctx, req, name)
if err != nil {
return nil, err
}
amount := ValidNumber(data.Get("amount").(string))
if amount == nil {
return nil, fmt.Errorf("invalid amount")
}
if amount.Cmp(balance) > 0 {
return nil, fmt.Errorf("Insufficient funds spend %v because the current account balance is %v", amount, balance)
}
if valid, err := b.validAccountConstraints(account, amount, data.Get("address_to").(string)); !valid {
return nil, err
}
chainID := ValidNumber(config.ChainID)
if chainID == nil {
return nil, fmt.Errorf("invalid chain ID")
}
gasLimitIn := ValidNumber(data.Get("gas_limit").(string))
if gasLimitIn == nil {
return nil, fmt.Errorf("invalid gas limit")
}
gasLimit := gasLimitIn.Uint64()
client, err := ethclient.Dial(config.getRPCURL())
if err != nil {
return nil, fmt.Errorf("cannot connect to " + config.getRPCURL())
}

gasPrice := ValidNumber(data.Get("gas_price").(string))
if big.NewInt(0).Cmp(gasPrice) == 0 {
gasPrice, err = client.SuggestGasPrice(context.Background())
if err != nil {
return nil, err
}
}
privateKey, err := crypto.HexToECDSA(account.PrivateKey)
if err != nil {
return nil, fmt.Errorf("error reconstructing private key")
}
defer ZeroKey(privateKey)
publicKey := privateKey.Public()
publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey)
if !ok {
return nil, fmt.Errorf("error casting public key to ECDSA")
}

fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA)
nonceIn := ValidNumber(data.Get("nonce").(string))
var nonce uint64
if nonceIn != nil && nonceIn.Cmp(big.NewInt(0)) != 0 {
nonce = nonceIn.Uint64()
} else {
nonce, err = client.PendingNonceAt(context.Background(), fromAddress)
if err != nil {
return nil, err
}
}

toAddress := common.HexToAddress(data.Get("address_to").(string))
tx := types.NewTransaction(nonce, toAddress, amount, gasLimit, gasPrice, []byte(dataToSign))
signedTx, err := types.SignTx(tx, types.NewEIP155Signer(chainID), privateKey)
if err != nil {
return nil, err
}

totalSpend, err := b.updateTotalSpend(ctx, req, fmt.Sprintf("accounts/%s", name), account, amount)
if err != nil {
return nil, err
}
amountInUSD, _ := decimal.NewFromString("0")
if config.ChainID == EthereumMainnet {
amountInUSD, err = ConvertToUSD(amount.String())
if err != nil {
return nil, err
}
}
var signedTxBuff bytes.Buffer
signedTx.EncodeRLP(&signedTxBuff)

return &logical.Response{
Data: map[string]interface{}{
"transaction_hash": signedTx.Hash().Hex(),
"signed_transaction": hexutil.Encode(signedTxBuff.Bytes()),
"address_from": account.Address,
"address_to": toAddress.String(),
"amount": amount.String(),
"amount_in_usd": amountInUSD,
"gas_price": gasPrice.String(),
"gas_limit": gasLimitIn.String(),
"total_spend": totalSpend,
"starting_balance": balance,
"starting_balance_in_usd": exchangeValue,
},
}, nil
}

func (b *EthereumBackend) pathDebit(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
config, err := b.configured(ctx, req)
if err != nil {
Expand Down

0 comments on commit 4575e39

Please sign in to comment.