status-go/services/wallet/token/token.go

736 lines
18 KiB
Go
Raw Normal View History

2022-09-13 07:10:59 +00:00
package token
import (
"context"
"database/sql"
2022-01-14 09:21:00 +00:00
"errors"
"math/big"
"strconv"
"sync"
"time"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/log"
2022-09-09 06:58:36 +00:00
"github.com/status-im/status-go/contracts"
2022-02-02 22:50:55 +00:00
"github.com/status-im/status-go/contracts/ierc20"
2022-09-13 07:10:59 +00:00
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/rpc"
2023-02-20 09:32:45 +00:00
"github.com/status-im/status-go/rpc/chain"
2022-04-13 07:55:38 +00:00
"github.com/status-im/status-go/rpc/network"
"github.com/status-im/status-go/services/wallet/async"
)
var requestTimeout = 20 * time.Second
2022-09-09 06:58:36 +00:00
var nativeChainAddress = common.HexToAddress("0x")
type Token struct {
Address common.Address `json:"address"`
Name string `json:"name"`
Symbol string `json:"symbol"`
Color string `json:"color"`
// Decimals defines how divisible the token is. For example, 0 would be
// indivisible, whereas 18 would allow very small amounts of the token
// to be traded.
Decimals uint `json:"decimals"`
ChainID uint64 `json:"chainId"`
// PegSymbol indicates that the token is pegged to some fiat currency, using the
// ISO 4217 alphabetic code. For example, an empty string means it is not
// pegged, while "USD" means it's pegged to the United States Dollar.
PegSymbol string `json:"pegSymbol"`
}
2022-09-13 07:10:59 +00:00
func (t *Token) IsNative() bool {
return t.Address == nativeChainAddress
}
// Manager is used for accessing token store. It changes the token store based on overridden tokens
2022-10-25 14:50:32 +00:00
type Manager struct {
2022-04-13 07:55:38 +00:00
db *sql.DB
RPCClient *rpc.Client
networkManager *network.Manager
stores []store
tokenList []*Token
tokenMap storeMap
2022-04-13 07:55:38 +00:00
}
func NewTokenManager(
db *sql.DB,
RPCClient *rpc.Client,
networkManager *network.Manager,
2022-10-25 14:50:32 +00:00
) *Manager {
// Order of stores is important when merging token lists. The former prevale
tokenManager := &Manager{db, RPCClient, networkManager, []store{newUniswapStore(), newDefaultStore()}, nil, nil}
return tokenManager
}
// overrideTokensInPlace overrides tokens in the store with the ones from the networks
// BEWARE: overridden tokens will have their original address removed and replaced by the one in networks
func overrideTokensInPlace(networks []params.Network, tokens []*Token) {
for _, network := range networks {
if len(network.TokenOverrides) == 0 {
continue
}
for _, overrideToken := range network.TokenOverrides {
for _, token := range tokens {
if token.Symbol == overrideToken.Symbol {
token.Address = overrideToken.Address
}
}
}
}
}
func mergeTokenLists(sliceLists [][]*Token) []*Token {
allKeys := make(map[string]bool)
res := []*Token{}
for _, list := range sliceLists {
for _, token := range list {
key := strconv.FormatUint(token.ChainID, 10) + token.Address.String()
if _, value := allKeys[key]; !value {
allKeys[key] = true
res = append(res, token)
}
}
}
return res
}
func (tm *Manager) inStore(address common.Address, chainID uint64) bool {
if address == nativeChainAddress {
return true
}
if !tm.areTokensFetched() {
tm.fetchTokens()
}
tokensMap, ok := tm.tokenMap[chainID]
if !ok {
return false
}
_, ok = tokensMap[address]
return ok
}
func (tm *Manager) areTokensFetched() bool {
for _, store := range tm.stores {
if !store.areTokensFetched() {
return false
}
}
return true
}
func (tm *Manager) fetchTokens() {
tm.tokenList = nil
tm.tokenMap = nil
2023-03-28 13:15:34 +00:00
networks, err := tm.networkManager.Get(false)
if err != nil {
return
}
for _, store := range tm.stores {
tokens, err := store.GetTokens()
if err != nil {
log.Error("can't fetch tokens from store: %s", err)
continue
}
2023-03-28 13:15:34 +00:00
validTokens := make([]*Token, 0)
for _, token := range tokens {
for _, network := range networks {
if network.ChainID == token.ChainID {
validTokens = append(validTokens, token)
break
}
}
}
2023-03-28 13:15:34 +00:00
tm.tokenList = mergeTokenLists([][]*Token{tm.tokenList, validTokens})
}
tm.tokenMap = toTokenMap(tm.tokenList)
}
2022-10-25 14:50:32 +00:00
func (tm *Manager) FindToken(network *params.Network, tokenSymbol string) *Token {
2022-09-13 07:10:59 +00:00
if tokenSymbol == network.NativeCurrencySymbol {
2022-10-27 07:38:05 +00:00
return tm.ToToken(network)
2022-09-13 07:10:59 +00:00
}
tokens, err := tm.GetTokens(network.ChainID)
if err != nil {
return nil
}
customTokens, err := tm.GetCustomsByChainID(network.ChainID)
if err != nil {
return nil
}
allTokens := append(tokens, customTokens...)
for _, token := range allTokens {
if token.Symbol == tokenSymbol {
return token
}
}
return nil
}
2022-10-25 14:50:32 +00:00
func (tm *Manager) FindSNT(chainID uint64) *Token {
tokens, err := tm.GetTokens(chainID)
if err != nil {
2022-04-13 07:55:38 +00:00
return nil
}
for _, token := range tokens {
2022-04-13 07:55:38 +00:00
if token.Symbol == "SNT" || token.Symbol == "STT" {
return token
}
}
return nil
}
func (tm *Manager) GetAllTokensAndNativeCurrencies() ([]*Token, error) {
allTokens, err := tm.GetAllTokens()
if err != nil {
return nil, err
}
networks, err := tm.RPCClient.NetworkManager.Get(false)
if err != nil {
return nil, err
}
for _, network := range networks {
allTokens = append(allTokens, tm.ToToken(network))
}
return allTokens, nil
}
2022-10-27 07:38:05 +00:00
func (tm *Manager) GetAllTokens() ([]*Token, error) {
if !tm.areTokensFetched() {
tm.fetchTokens()
2022-10-27 07:38:05 +00:00
}
tokens, err := tm.GetCustoms()
if err != nil {
log.Error("can't fetch custom tokens: %s", err)
2022-10-27 07:38:05 +00:00
}
2023-01-13 17:12:46 +00:00
tokens = append(tm.tokenList, tokens...)
2022-10-27 07:38:05 +00:00
overrideTokensInPlace(tm.networkManager.GetConfiguredNetworks(), tokens)
return tokens, nil
2022-10-27 07:38:05 +00:00
}
2022-10-25 14:50:32 +00:00
func (tm *Manager) GetTokens(chainID uint64) ([]*Token, error) {
if !tm.areTokensFetched() {
tm.fetchTokens()
}
tokensMap, ok := tm.tokenMap[chainID]
2022-01-14 09:21:00 +00:00
if !ok {
return nil, errors.New("no tokens for this network")
}
res := make([]*Token, 0, len(tokensMap))
for _, token := range tokensMap {
res = append(res, token)
}
return res, nil
}
2022-10-25 14:50:32 +00:00
func (tm *Manager) DiscoverToken(ctx context.Context, chainID uint64, address common.Address) (*Token, error) {
backend, err := tm.RPCClient.EthClient(chainID)
if err != nil {
return nil, err
}
caller, err := ierc20.NewIERC20Caller(address, backend)
if err != nil {
return nil, err
}
name, err := caller.Name(&bind.CallOpts{
Context: ctx,
})
if err != nil {
return nil, err
}
symbol, err := caller.Symbol(&bind.CallOpts{
Context: ctx,
})
if err != nil {
return nil, err
}
decimal, err := caller.Decimals(&bind.CallOpts{
Context: ctx,
})
if err != nil {
return nil, err
}
return &Token{
Address: address,
Name: name,
Symbol: symbol,
Decimals: uint(decimal),
}, nil
}
2022-10-25 14:50:32 +00:00
func (tm *Manager) GetCustoms() ([]*Token, error) {
rows, err := tm.db.Query("SELECT address, name, symbol, decimals, color, network_id FROM tokens")
if err != nil {
return nil, err
}
defer rows.Close()
var rst []*Token
for rows.Next() {
token := &Token{}
err := rows.Scan(&token.Address, &token.Name, &token.Symbol, &token.Decimals, &token.Color, &token.ChainID)
if err != nil {
return nil, err
}
rst = append(rst, token)
}
return rst, nil
}
2022-10-25 14:50:32 +00:00
func (tm *Manager) GetCustomsByChainID(chainID uint64) ([]*Token, error) {
rows, err := tm.db.Query("SELECT address, name, symbol, decimals, color, network_id FROM tokens where network_id=?", chainID)
if err != nil {
return nil, err
}
defer rows.Close()
var rst []*Token
for rows.Next() {
token := &Token{}
err := rows.Scan(&token.Address, &token.Name, &token.Symbol, &token.Decimals, &token.Color, &token.ChainID)
if err != nil {
return nil, err
}
rst = append(rst, token)
}
return rst, nil
}
2022-10-25 14:50:32 +00:00
func (tm *Manager) IsTokenVisible(chainID uint64, address common.Address) (bool, error) {
2022-04-13 07:55:38 +00:00
rows, err := tm.db.Query("SELECT chain_id, address FROM visible_tokens WHERE chain_id = ? AND address = ?", chainID, address)
if err != nil {
return false, err
}
defer rows.Close()
return rows.Next(), nil
}
2022-10-25 14:50:32 +00:00
func (tm *Manager) Toggle(chainID uint64, address common.Address) error {
2022-09-13 07:10:59 +00:00
isVisible, err := tm.IsTokenVisible(chainID, address)
2022-04-13 07:55:38 +00:00
if err != nil {
return err
}
if isVisible {
_, err = tm.db.Exec(`DELETE FROM visible_tokens WHERE address = ? and chain_id = ?`, address, chainID)
return err
}
insert, err := tm.db.Prepare("INSERT OR REPLACE INTO visible_tokens (chain_id, address) VALUES (?, ?)")
if err != nil {
return err
}
defer insert.Close()
_, err = insert.Exec(chainID, address)
return err
}
2022-10-27 07:38:05 +00:00
func (tm *Manager) ToToken(network *params.Network) *Token {
return &Token{
Address: common.HexToAddress("0x"),
Name: network.NativeCurrencyName,
Symbol: network.NativeCurrencySymbol,
Decimals: uint(network.NativeCurrencyDecimals),
ChainID: network.ChainID,
}
}
2022-10-25 14:50:32 +00:00
func (tm *Manager) GetVisible(chainIDs []uint64) (map[uint64][]*Token, error) {
2022-09-13 07:10:59 +00:00
customTokens, err := tm.GetCustoms()
2022-04-13 07:55:38 +00:00
if err != nil {
return nil, err
}
rst := make(map[uint64][]*Token)
for _, chainID := range chainIDs {
network := tm.networkManager.Find(chainID)
if network == nil {
continue
}
rst[chainID] = make([]*Token, 0)
2022-10-27 07:38:05 +00:00
rst[chainID] = append(rst[chainID], tm.ToToken(network))
2022-04-13 07:55:38 +00:00
}
rows, err := tm.db.Query("SELECT chain_id, address FROM visible_tokens")
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
address := common.HexToAddress("0x")
chainID := uint64(0)
err := rows.Scan(&chainID, &address)
if err != nil {
return nil, err
}
found := false
tokens, err := tm.GetTokens(chainID)
if err != nil {
continue
}
for _, token := range tokens {
2022-04-13 07:55:38 +00:00
if token.Address == address {
rst[chainID] = append(rst[chainID], token)
found = true
break
}
}
if found {
continue
}
for _, token := range customTokens {
if token.Address == address {
rst[chainID] = append(rst[chainID], token)
break
}
}
}
for _, chainID := range chainIDs {
if len(rst[chainID]) == 1 {
2022-09-13 07:10:59 +00:00
token := tm.FindSNT(chainID)
2022-04-13 07:55:38 +00:00
if token != nil {
rst[chainID] = append(rst[chainID], token)
}
}
}
return rst, nil
}
2022-10-25 14:50:32 +00:00
func (tm *Manager) UpsertCustom(token Token) error {
insert, err := tm.db.Prepare("INSERT OR REPLACE INTO TOKENS (network_id, address, name, symbol, decimals, color) VALUES (?, ?, ?, ?, ?, ?)")
if err != nil {
return err
}
_, err = insert.Exec(token.ChainID, token.Address, token.Name, token.Symbol, token.Decimals, token.Color)
return err
}
2022-10-25 14:50:32 +00:00
func (tm *Manager) DeleteCustom(chainID uint64, address common.Address) error {
_, err := tm.db.Exec(`DELETE FROM TOKENS WHERE address = ? and network_id = ?`, address, chainID)
return err
}
2023-02-20 09:32:45 +00:00
func (tm *Manager) GetTokenBalance(ctx context.Context, client *chain.ClientWithFallback, account common.Address, token common.Address) (*big.Int, error) {
2022-05-10 07:48:05 +00:00
caller, err := ierc20.NewIERC20Caller(token, client)
if err != nil {
return nil, err
}
return caller.BalanceOf(&bind.CallOpts{
Context: ctx,
}, account)
}
2023-02-20 09:32:45 +00:00
func (tm *Manager) GetTokenBalanceAt(ctx context.Context, client *chain.ClientWithFallback, account common.Address, token common.Address, blockNumber *big.Int) (*big.Int, error) {
feat: retrieve balance history for tokens and cache it to DB Extends wallet module with the history package with the following components: BalanceDB (balance_db.go) - Keeps track of balance information (token count, block, block timestamp) for a token identity (chain, address, currency) - The cached data is stored in `balance_history` table. - Uniqueness constrained is enforced by the `balance_history_identify_entry` UNIQUE index. - Optimal DB fetching is ensured by the `balance_history_filter_entries` index Balance (balance.go) - Provides two stages: - Fetch of balance history using RPC calls (Balance.update function) - Retrieving of cached balance data from the DB it exists (Balance.get function) - Fetching and retrieving of data is done for specific time intervals defined by TimeInterval "enumeration" - Update process is done for a token identity by the Balance.Update function - The granularity of data points returned is defined by the constant increment step define in `timeIntervalToStride` for each time interval. - The `blocksStride` values have a common divisor to have cache hit between time intervals. Service (service.go) - Main APIs - StartBalanceHistory: Regularly updates balance history for all enabled networks, available accounts and provided tokens. - GetBalanceHistory: retrieves cached token count for a token identity (chain, address, currency) for multiple chains - UpdateVisibleTokens: will set the list of tokens to have historical balance fetched. This is a simplification to limit tokens to a small list that make sense Fetch balance history for ECR20 tokens - Add token.Manager.GetTokenBalanceAt to fetch balance of a specific block number of ECR20. - Add tokenChainClientSource concrete implementation of DataSource to fetch balance of ECR20 tokens. - Chose the correct DataSource implementation based on the token "is native" property. Tests Tests are implemented using a mock of `DataSource` interface used to intercept the RPC calls. Notes: - the timestamp used for retrieving block balance is constant Closes status-desktop: #8175, #8226, #8862
2022-11-15 12:14:41 +00:00
caller, err := ierc20.NewIERC20Caller(token, client)
if err != nil {
return nil, err
}
return caller.BalanceOf(&bind.CallOpts{
Context: ctx,
BlockNumber: blockNumber,
}, account)
}
2023-02-20 09:32:45 +00:00
func (tm *Manager) GetChainBalance(ctx context.Context, client *chain.ClientWithFallback, account common.Address) (*big.Int, error) {
2022-05-10 07:48:05 +00:00
return client.BalanceAt(ctx, account, nil)
}
2023-02-20 09:32:45 +00:00
func (tm *Manager) GetBalance(ctx context.Context, client *chain.ClientWithFallback, account common.Address, token common.Address) (*big.Int, error) {
2022-09-09 06:58:36 +00:00
if token == nativeChainAddress {
2022-09-13 07:10:59 +00:00
return tm.GetChainBalance(ctx, client, account)
}
2022-09-13 07:10:59 +00:00
return tm.GetTokenBalance(ctx, client, account, token)
}
2023-03-24 08:38:27 +00:00
func (tm *Manager) GetBalances(parent context.Context, clients map[uint64]*chain.ClientWithFallback, accounts, tokens []common.Address) (map[common.Address]map[common.Address]*hexutil.Big, error) {
var (
group = async.NewAtomicGroup(parent)
mu sync.Mutex
response = map[common.Address]map[common.Address]*hexutil.Big{}
)
2022-09-09 06:58:36 +00:00
updateBalance := func(account common.Address, token common.Address, balance *big.Int) {
mu.Lock()
if _, ok := response[account]; !ok {
response[account] = map[common.Address]*hexutil.Big{}
}
if _, ok := response[account][token]; !ok {
zeroHex := hexutil.Big(*big.NewInt(0))
response[account][token] = &zeroHex
}
sum := big.NewInt(0).Add(response[account][token].ToInt(), balance)
sumHex := hexutil.Big(*sum)
response[account][token] = &sumHex
mu.Unlock()
}
contractMaker := contracts.ContractMaker{RPCClient: tm.RPCClient}
for clientIdx := range clients {
2022-09-09 06:58:36 +00:00
client := clients[clientIdx]
2022-09-09 06:58:36 +00:00
ethScanContract, err := contractMaker.NewEthScan(client.ChainID)
if err == nil {
fetchChainBalance := false
var tokenChunks [][]common.Address
2023-03-28 12:46:46 +00:00
chunkSize := 500
2022-09-09 06:58:36 +00:00
for i := 0; i < len(tokens); i += chunkSize {
end := i + chunkSize
if end > len(tokens) {
end = len(tokens)
}
tokenChunks = append(tokenChunks, tokens[i:end])
}
for _, token := range tokens {
if token == nativeChainAddress {
fetchChainBalance = true
}
}
if fetchChainBalance {
group.Add(func(parent context.Context) error {
ctx, cancel := context.WithTimeout(parent, requestTimeout)
defer cancel()
2022-09-09 06:58:36 +00:00
res, err := ethScanContract.EtherBalances(&bind.CallOpts{
Context: ctx,
}, accounts)
if err != nil {
2022-09-09 06:58:36 +00:00
log.Error("can't fetch chain balance", err)
return nil
}
2022-09-09 06:58:36 +00:00
for idx, account := range accounts {
balance := new(big.Int)
balance.SetBytes(res[idx].Data)
updateBalance(account, common.HexToAddress("0x"), balance)
}
return nil
})
}
2022-09-09 06:58:36 +00:00
for accountIdx := range accounts {
account := accounts[accountIdx]
for idx := range tokenChunks {
chunk := tokenChunks[idx]
group.Add(func(parent context.Context) error {
ctx, cancel := context.WithTimeout(parent, requestTimeout)
defer cancel()
res, err := ethScanContract.TokensBalance(&bind.CallOpts{
Context: ctx,
}, account, chunk)
if err != nil {
log.Error("can't fetch erc20 token balance", "account", account, "error", err)
return nil
}
for idx, token := range chunk {
if !res[idx].Success {
continue
}
balance := new(big.Int)
balance.SetBytes(res[idx].Data)
updateBalance(account, token, balance)
}
return nil
})
}
}
} else {
for tokenIdx := range tokens {
for accountIdx := range accounts {
// Below, we set account, token and client from idx on purpose to avoid override
account := accounts[accountIdx]
token := tokens[tokenIdx]
client := clients[clientIdx]
if !tm.inStore(token, client.ChainID) {
continue
}
2022-09-09 06:58:36 +00:00
group.Add(func(parent context.Context) error {
ctx, cancel := context.WithTimeout(parent, requestTimeout)
defer cancel()
2022-09-13 07:10:59 +00:00
balance, err := tm.GetBalance(ctx, client, account, token)
2022-09-09 06:58:36 +00:00
if err != nil {
log.Error("can't fetch erc20 token balance", "account", account, "token", token, "error", err)
return nil
}
updateBalance(account, token, balance)
return nil
})
}
}
}
2022-09-09 06:58:36 +00:00
}
select {
case <-group.WaitAsync():
case <-parent.Done():
return nil, parent.Err()
}
return response, group.Error()
}
2022-10-27 07:38:05 +00:00
2023-03-24 08:38:27 +00:00
func (tm *Manager) GetBalancesByChain(parent context.Context, clients map[uint64]*chain.ClientWithFallback, accounts, tokens []common.Address) (map[uint64]map[common.Address]map[common.Address]*hexutil.Big, error) {
2022-10-27 07:38:05 +00:00
var (
group = async.NewAtomicGroup(parent)
mu sync.Mutex
response = map[uint64]map[common.Address]map[common.Address]*hexutil.Big{}
)
updateBalance := func(chainID uint64, account common.Address, token common.Address, balance *big.Int) {
mu.Lock()
if _, ok := response[chainID]; !ok {
response[chainID] = map[common.Address]map[common.Address]*hexutil.Big{}
}
if _, ok := response[chainID][account]; !ok {
response[chainID][account] = map[common.Address]*hexutil.Big{}
}
if _, ok := response[chainID][account][token]; !ok {
zeroHex := hexutil.Big(*big.NewInt(0))
response[chainID][account][token] = &zeroHex
}
sum := big.NewInt(0).Add(response[chainID][account][token].ToInt(), balance)
sumHex := hexutil.Big(*sum)
response[chainID][account][token] = &sumHex
mu.Unlock()
}
contractMaker := contracts.ContractMaker{RPCClient: tm.RPCClient}
for clientIdx := range clients {
client := clients[clientIdx]
ethScanContract, err := contractMaker.NewEthScan(client.ChainID)
2023-03-30 07:06:47 +00:00
if err != nil {
log.Error("error scanning contract", "err", err)
2023-03-30 07:06:47 +00:00
return nil, err
}
2022-10-27 07:38:05 +00:00
2023-03-30 07:06:47 +00:00
fetchChainBalance := false
var tokenChunks [][]common.Address
chunkSize := 500
for i := 0; i < len(tokens); i += chunkSize {
end := i + chunkSize
if end > len(tokens) {
end = len(tokens)
2022-10-27 07:38:05 +00:00
}
2023-03-30 07:06:47 +00:00
tokenChunks = append(tokenChunks, tokens[i:end])
}
for _, token := range tokens {
if token == nativeChainAddress {
fetchChainBalance = true
2022-10-27 07:38:05 +00:00
}
2023-03-30 07:06:47 +00:00
}
if fetchChainBalance {
group.Add(func(parent context.Context) error {
ctx, cancel := context.WithTimeout(parent, requestTimeout)
defer cancel()
res, err := ethScanContract.EtherBalances(&bind.CallOpts{
Context: ctx,
}, accounts)
if err != nil {
log.Error("can't fetch chain balance", err)
return nil
}
for idx, account := range accounts {
balance := new(big.Int)
balance.SetBytes(res[idx].Data)
updateBalance(client.ChainID, account, common.HexToAddress("0x"), balance)
}
return nil
})
}
for accountIdx := range accounts {
account := accounts[accountIdx]
for idx := range tokenChunks {
chunk := tokenChunks[idx]
2022-10-27 07:38:05 +00:00
group.Add(func(parent context.Context) error {
ctx, cancel := context.WithTimeout(parent, requestTimeout)
defer cancel()
2023-03-30 07:06:47 +00:00
res, err := ethScanContract.TokensBalance(&bind.CallOpts{
2022-10-27 07:38:05 +00:00
Context: ctx,
2023-03-30 07:06:47 +00:00
}, account, chunk)
2022-10-27 07:38:05 +00:00
if err != nil {
2023-03-30 07:06:47 +00:00
log.Error("can't fetch erc20 token balance", "account", account, "error", err)
2022-10-27 07:38:05 +00:00
return nil
}
2023-03-30 07:06:47 +00:00
if len(res) != len(chunk) {
log.Error("can't fetch erc20 token balance", "account", account, "error response not complete")
return nil
}
2023-03-30 07:06:47 +00:00
for idx, token := range chunk {
2023-03-30 07:06:47 +00:00
if !res[idx].Success {
continue
}
2022-10-27 07:38:05 +00:00
balance := new(big.Int)
balance.SetBytes(res[idx].Data)
2023-03-30 07:06:47 +00:00
updateBalance(client.ChainID, account, token, balance)
2022-10-27 07:38:05 +00:00
}
return nil
})
}
}
}
select {
case <-group.WaitAsync():
case <-parent.Done():
return nil, parent.Err()
}
return response, group.Error()
}