Skip to content

Commit

Permalink
Merge pull request #455 from oasisprotocol/mitjat/token-holders
Browse files Browse the repository at this point in the history
Add /evm_tokens/{address}/holders endpoint
  • Loading branch information
mitjat authored Jun 27, 2023
2 parents e9a24f1 + 792af66 commit fc689e1
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 7 deletions.
66 changes: 61 additions & 5 deletions api/spec/v1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -996,7 +996,7 @@ paths:
required: true
schema:
<<: *StakingAddressType
description: The staking address of the account to return.
description: The staking address of the token contract.
responses:
'200':
description: The requested token.
Expand All @@ -1006,6 +1006,30 @@ paths:
$ref: '#/components/schemas/EvmToken'
<<: *common_error_responses

/{runtime}/evm_tokens/{address}/holders:
get:
summary: |
Returns the list of holders of an EVM (ERC-20, ...) token.
This endpoint does not verify that `address` is actually an EVM token; if it is not, it will simply return an empty list.
parameters:
- *limit
- *offset
- *runtime
- in: path
name: address
required: true
schema:
<<: *StakingAddressType
description: The staking address of the token contract for which to return the holders.
responses:
'200':
description: The requested holders.
content:
application/json:
schema:
$ref: '#/components/schemas/TokenHolderList'
<<: *common_error_responses

/{runtime}/accounts/{address}:
get:
summary: Returns a runtime account.
Expand Down Expand Up @@ -1733,6 +1757,38 @@ components:
description: The number of decimals of precision for this token.
example: 18

TokenHolderList:
allOf:
- $ref: '#/components/schemas/List'
- type: object
required: [holders]
properties:
holders:
type: array
items:
$ref: '#/components/schemas/BareTokenHolder'
description: |
A list of token holders for a specific (implied) runtime and token.
BareTokenHolder:
description: |
Balance of an account for a specific (implied) runtime and token.
type: object
required: [holder_address, balance]
properties:
holder_address:
type: string
description: The oasis address of the account holder.
example: *staking_address_1
eth_holder_address:
type: string
description: The Ethereum address of the same account holder, if meaningfully defined.
example: *eth_address_1
balance:
<<: *BigIntType
description: Number of tokens held, in base units.


Account:
type: object
required: [address, nonce, available, escrow, debonding, allowances]
Expand Down Expand Up @@ -2394,16 +2450,16 @@ components:

EvmToken:
type: object
required: [contract_addr, evm_contract_addr, num_holders, type]
required: [contract_addr, num_holders, type]
properties:
contract_addr:
type: string
description: The Oasis address of this token's contract.
example: 'oasis1qp2hssandc7dekjdr6ygmtzt783k3gn38uupdeys'
evm_contract_addr:
eth_contract_addr:
type: string
description: The EVM address of this token's contract. Encoded as a lowercase hex string.
example: 'dc19a122e268128b5ee20366299fc7b5b199c8e3'
description: The Ethereum address of this token's contract.
example: *eth_address_1
name:
type: string
description: Name of the token, as provided by token contract's `name()` method.
Expand Down
8 changes: 8 additions & 0 deletions api/v1/strict_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,14 @@ func (srv *StrictServerImpl) GetRuntimeEvmTokensAddress(ctx context.Context, req
return apiTypes.GetRuntimeEvmTokensAddress200JSONResponse(tokens.EvmTokens[0]), nil
}

func (srv *StrictServerImpl) GetRuntimeEvmTokensAddressHolders(ctx context.Context, request apiTypes.GetRuntimeEvmTokensAddressHoldersRequestObject) (apiTypes.GetRuntimeEvmTokensAddressHoldersResponseObject, error) {
holders, err := srv.dbClient.RuntimeTokenHolders(ctx, request.Params, request.Address)
if err != nil {
return nil, err
}
return apiTypes.GetRuntimeEvmTokensAddressHolders200JSONResponse(*holders), nil
}

func (srv *StrictServerImpl) GetRuntimeTransactions(ctx context.Context, request apiTypes.GetRuntimeTransactionsRequestObject) (apiTypes.GetRuntimeTransactionsResponseObject, error) {
transactions, err := srv.dbClient.RuntimeTransactions(ctx, request.Params, nil)
if err != nil {
Expand Down
40 changes: 39 additions & 1 deletion storage/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -1463,9 +1463,10 @@ func (c *StorageClient) RuntimeTokens(ctx context.Context, p apiTypes.GetRuntime
}
for res.rows.Next() {
var t EvmToken
var addrPreimage []byte
if err2 := res.rows.Scan(
&t.ContractAddr,
&t.EvmContractAddr,
&addrPreimage,
&t.Name,
&t.Symbol,
&t.Decimals,
Expand All @@ -1476,12 +1477,49 @@ func (c *StorageClient) RuntimeTokens(ctx context.Context, p apiTypes.GetRuntime
return nil, wrapError(err2)
}

t.EthContractAddr = common.Ptr(ethCommon.BytesToAddress(addrPreimage).String())
ts.EvmTokens = append(ts.EvmTokens, t)
}

return &ts, nil
}

func (c *StorageClient) RuntimeTokenHolders(ctx context.Context, p apiTypes.GetRuntimeEvmTokensAddressHoldersParams, address staking.Address) (*TokenHolderList, error) {
res, err := c.withTotalCount(
ctx,
queries.EvmTokenHolders,
runtimeFromCtx(ctx),
address,
p.Limit,
p.Offset,
)
if err != nil {
return nil, wrapError(err)
}
defer res.rows.Close()

hs := TokenHolderList{
Holders: []BareTokenHolder{},
TotalCount: res.totalCount,
IsTotalCountClipped: res.isTotalCountClipped,
}
for res.rows.Next() {
var h BareTokenHolder
var addrPreimage []byte
if err2 := res.rows.Scan(
&h.HolderAddress,
&addrPreimage,
&h.Balance,
); err2 != nil {
return nil, wrapError(err2)
}
h.EthHolderAddress = common.Ptr(ethCommon.BytesToAddress(addrPreimage).String())
hs.Holders = append(hs.Holders, h)
}

return &hs, nil
}

// RuntimeStatus returns runtime status information.
func (c *StorageClient) RuntimeStatus(ctx context.Context) (*RuntimeStatus, error) {
runtimeName := runtimeFromCtx(ctx)
Expand Down
17 changes: 16 additions & 1 deletion storage/client/queries/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,7 @@ const (
)
SELECT
tokens.token_address AS contract_addr,
encode(preimages.address_data, 'hex') as eth_contract_addr,
preimages.address_data as eth_contract_addr,
tokens.token_name AS name,
tokens.symbol,
tokens.decimals,
Expand All @@ -471,6 +471,21 @@ const (
LIMIT $3::bigint
OFFSET $4::bigint`

//nolint:gosec // Linter suspects a hardcoded credentials token.
EvmTokenHolders = `
SELECT
balances.account_address AS holder_addr,
preimages.address_data as eth_holder_addr,
balances.balance AS balance
FROM chain.evm_token_balances AS balances
JOIN chain.address_preimages AS preimages ON (balances.account_address = preimages.address AND preimages.context_identifier = 'oasis-runtime-sdk/address: secp256k1eth' AND preimages.context_version = 0)
WHERE
(balances.runtime = $1::runtime) AND
(balances.token_address = $2::oasis_addr)
ORDER BY balance DESC
LIMIT $3::bigint
OFFSET $4::bigint`

AccountRuntimeSdkBalances = `
SELECT
balance AS balance,
Expand Down
4 changes: 4 additions & 0 deletions storage/client/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,10 @@ type EvmTokenList = api.EvmTokenList

type EvmToken = api.EvmToken

type BareTokenHolder = api.BareTokenHolder

type TokenHolderList = api.TokenHolderList

// TxVolumeList is the storage response for GetVolumes.
type TxVolumeList = api.TxVolumeList

Expand Down

0 comments on commit fc689e1

Please sign in to comment.