From 7069b49d859fc508d0a57fb4e3e0b086c31ca4db Mon Sep 17 00:00:00 2001 From: Mitja T Date: Wed, 21 Jun 2023 11:23:49 -0700 Subject: [PATCH 1/3] Add /evm_tokens/{address}/holders endpoint --- api/spec/v1.yaml | 58 ++++++++++++++++++++++++++++++- api/v1/strict_server.go | 8 +++++ storage/client/client.go | 35 +++++++++++++++++++ storage/client/queries/queries.go | 15 ++++++++ storage/client/types.go | 4 +++ 5 files changed, 119 insertions(+), 1 deletion(-) diff --git a/api/spec/v1.yaml b/api/spec/v1.yaml index 978fa1151..a10b74999 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] 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..5a3f0beec 100644 --- a/storage/client/client.go +++ b/storage/client/client.go @@ -1482,6 +1482,41 @@ func (c *StorageClient) RuntimeTokens(ctx context.Context, p apiTypes.GetRuntime 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 + if err2 := res.rows.Scan( + &h.HolderAddress, + &h.EthHolderAddress, + &h.Balance, + ); err2 != nil { + return nil, wrapError(err2) + } + + 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..3d75fdabc 100644 --- a/storage/client/queries/queries.go +++ b/storage/client/queries/queries.go @@ -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, + encode(preimages.address_data, 'hex') 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 From 40341e01689bb7b62f8830c94561da11c18a405a Mon Sep 17 00:00:00 2001 From: Mitja T Date: Wed, 21 Jun 2023 11:45:36 -0700 Subject: [PATCH 2/3] api: Checksum-format eth addresses --- api/spec/v1.yaml | 4 ++-- storage/client/client.go | 9 ++++++--- storage/client/queries/queries.go | 4 ++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/api/spec/v1.yaml b/api/spec/v1.yaml index a10b74999..752e51e36 100644 --- a/api/spec/v1.yaml +++ b/api/spec/v1.yaml @@ -2458,8 +2458,8 @@ components: example: 'oasis1qp2hssandc7dekjdr6ygmtzt783k3gn38uupdeys' evm_contract_addr: type: string - description: The EVM address of this token's contract. Encoded as a lowercase hex string. - example: 'dc19a122e268128b5ee20366299fc7b5b199c8e3' + description: The EVM 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/storage/client/client.go b/storage/client/client.go index 5a3f0beec..3e98f3b76 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,6 +1477,7 @@ func (c *StorageClient) RuntimeTokens(ctx context.Context, p apiTypes.GetRuntime return nil, wrapError(err2) } + t.EvmContractAddr = ethCommon.BytesToAddress(addrPreimage).String() ts.EvmTokens = append(ts.EvmTokens, t) } @@ -1503,14 +1505,15 @@ func (c *StorageClient) RuntimeTokenHolders(ctx context.Context, p apiTypes.GetR } for res.rows.Next() { var h BareTokenHolder + var addrPreimage []byte if err2 := res.rows.Scan( &h.HolderAddress, - &h.EthHolderAddress, + &addrPreimage, &h.Balance, ); err2 != nil { return nil, wrapError(err2) } - + h.EthHolderAddress = common.Ptr(ethCommon.BytesToAddress(addrPreimage).String()) hs.Holders = append(hs.Holders, h) } diff --git a/storage/client/queries/queries.go b/storage/client/queries/queries.go index 3d75fdabc..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, @@ -475,7 +475,7 @@ const ( EvmTokenHolders = ` SELECT balances.account_address AS holder_addr, - encode(preimages.address_data, 'hex') as eth_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) From 792af66671ab1893f233d1b0f29f77c2fc2e1356 Mon Sep 17 00:00:00 2001 From: Mitja T Date: Wed, 21 Jun 2023 11:59:43 -0700 Subject: [PATCH 3/3] Rename evm_contract_addr -> eth_contract_addr --- api/spec/v1.yaml | 6 +++--- storage/client/client.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/spec/v1.yaml b/api/spec/v1.yaml index 752e51e36..d29dceb07 100644 --- a/api/spec/v1.yaml +++ b/api/spec/v1.yaml @@ -2450,15 +2450,15 @@ 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. + description: The Ethereum address of this token's contract. example: *eth_address_1 name: type: string diff --git a/storage/client/client.go b/storage/client/client.go index 3e98f3b76..2fa4fe342 100644 --- a/storage/client/client.go +++ b/storage/client/client.go @@ -1477,7 +1477,7 @@ func (c *StorageClient) RuntimeTokens(ctx context.Context, p apiTypes.GetRuntime return nil, wrapError(err2) } - t.EvmContractAddr = ethCommon.BytesToAddress(addrPreimage).String() + t.EthContractAddr = common.Ptr(ethCommon.BytesToAddress(addrPreimage).String()) ts.EvmTokens = append(ts.EvmTokens, t) }