diff --git a/api/spec/v1.yaml b/api/spec/v1.yaml index 978fa1151..d29dceb07 100644 --- a/api/spec/v1.yaml +++ b/api/spec/v1.yaml @@ -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. @@ -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. @@ -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] @@ -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. diff --git a/api/v1/strict_server.go b/api/v1/strict_server.go index 85a24c077..86ea9e22c 100644 --- a/api/v1/strict_server.go +++ b/api/v1/strict_server.go @@ -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 { diff --git a/storage/client/client.go b/storage/client/client.go index e65c6763f..2fa4fe342 100644 --- a/storage/client/client.go +++ b/storage/client/client.go @@ -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, @@ -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) diff --git a/storage/client/queries/queries.go b/storage/client/queries/queries.go index 73a2b3daf..49abe9790 100644 --- a/storage/client/queries/queries.go +++ b/storage/client/queries/queries.go @@ -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, @@ -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, diff --git a/storage/client/types.go b/storage/client/types.go index e8f0d0896..7ab97d46b 100644 --- a/storage/client/types.go +++ b/storage/client/types.go @@ -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