Merge pull request #30 from coinbase/patrick/historical-balance-lookup
[services] Add historical balance lookup
This commit is contained in:
commit
129feb7f84
19 changed files with 525 additions and 79 deletions
|
@ -1,2 +1,3 @@
|
||||||
rosetta-bitcoin
|
rosetta-bitcoin
|
||||||
bitcoin-data
|
bitcoin-data
|
||||||
|
cli-data
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -5,7 +5,7 @@ go 1.13
|
||||||
require (
|
require (
|
||||||
github.com/btcsuite/btcd v0.21.0-beta
|
github.com/btcsuite/btcd v0.21.0-beta
|
||||||
github.com/btcsuite/btcutil v1.0.2
|
github.com/btcsuite/btcutil v1.0.2
|
||||||
github.com/coinbase/rosetta-sdk-go v0.5.7
|
github.com/coinbase/rosetta-sdk-go v0.5.8-0.20201027222031-dd9e29377d5f
|
||||||
github.com/dgraph-io/badger/v2 v2.2007.2
|
github.com/dgraph-io/badger/v2 v2.2007.2
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware v1.2.2
|
github.com/grpc-ecosystem/go-grpc-middleware v1.2.2
|
||||||
github.com/stretchr/testify v1.6.1
|
github.com/stretchr/testify v1.6.1
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -61,8 +61,8 @@ github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJ
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/cloudflare/cloudflare-go v0.10.2-0.20190916151808-a80f83b9add9/go.mod h1:1MxXX1Ux4x6mqPmjkUgTP1CdXIBXKX7T+Jk9Gxrmx+U=
|
github.com/cloudflare/cloudflare-go v0.10.2-0.20190916151808-a80f83b9add9/go.mod h1:1MxXX1Ux4x6mqPmjkUgTP1CdXIBXKX7T+Jk9Gxrmx+U=
|
||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
github.com/coinbase/rosetta-sdk-go v0.5.7 h1:BaR/+O3GzrsyunVNkVQHtjDCcId8G1Fh/RqEbeyExnk=
|
github.com/coinbase/rosetta-sdk-go v0.5.8-0.20201027222031-dd9e29377d5f h1:aWkN9dKMkMMpZKX5QycpePxH176Fj2fNNC7jESfLZw0=
|
||||||
github.com/coinbase/rosetta-sdk-go v0.5.7/go.mod h1:l5aNeyeZKBkmWbVdkdLpWdToQ6hTwI7cZ1OU9cMbljY=
|
github.com/coinbase/rosetta-sdk-go v0.5.8-0.20201027222031-dd9e29377d5f/go.mod h1:l5aNeyeZKBkmWbVdkdLpWdToQ6hTwI7cZ1OU9cMbljY=
|
||||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||||
|
|
46
indexer/balance_storage_handler.go
Normal file
46
indexer/balance_storage_handler.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
// Copyright 2020 Coinbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package indexer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/parser"
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/storage"
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ storage.BalanceStorageHandler = (*BalanceStorageHandler)(nil)
|
||||||
|
|
||||||
|
// BalanceStorageHandler implements storage.BalanceStorageHandler.
|
||||||
|
type BalanceStorageHandler struct{}
|
||||||
|
|
||||||
|
// BlockAdded is called whenever a block is committed to BlockStorage.
|
||||||
|
func (h *BalanceStorageHandler) BlockAdded(
|
||||||
|
ctx context.Context,
|
||||||
|
block *types.Block,
|
||||||
|
changes []*parser.BalanceChange,
|
||||||
|
) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BlockRemoved is called whenever a block is removed from BlockStorage.
|
||||||
|
func (h *BalanceStorageHandler) BlockRemoved(
|
||||||
|
ctx context.Context,
|
||||||
|
block *types.Block,
|
||||||
|
changes []*parser.BalanceChange,
|
||||||
|
) error {
|
||||||
|
return nil
|
||||||
|
}
|
62
indexer/balance_storage_helper.go
Normal file
62
indexer/balance_storage_helper.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
// Copyright 2020 Coinbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package indexer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/asserter"
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/parser"
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/storage"
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ storage.BalanceStorageHelper = (*BalanceStorageHelper)(nil)
|
||||||
|
|
||||||
|
// BalanceStorageHelper implements storage.BalanceStorageHelper.
|
||||||
|
type BalanceStorageHelper struct {
|
||||||
|
a *asserter.Asserter
|
||||||
|
}
|
||||||
|
|
||||||
|
// AccountBalance attempts to fetch the balance
|
||||||
|
// for a missing account in storage.
|
||||||
|
func (h *BalanceStorageHelper) AccountBalance(
|
||||||
|
ctx context.Context,
|
||||||
|
account *types.AccountIdentifier,
|
||||||
|
currency *types.Currency,
|
||||||
|
block *types.BlockIdentifier,
|
||||||
|
) (*types.Amount, error) {
|
||||||
|
return &types.Amount{
|
||||||
|
Value: zeroValue,
|
||||||
|
Currency: currency,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asserter returns a *asserter.Asserter.
|
||||||
|
func (h *BalanceStorageHelper) Asserter() *asserter.Asserter {
|
||||||
|
return h.a
|
||||||
|
}
|
||||||
|
|
||||||
|
// BalanceExemptions returns a list of *types.BalanceExemption.
|
||||||
|
func (h *BalanceStorageHelper) BalanceExemptions() []*types.BalanceExemption {
|
||||||
|
return []*types.BalanceExemption{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExemptFunc returns a parser.ExemptOperation.
|
||||||
|
func (h *BalanceStorageHelper) ExemptFunc() parser.ExemptOperation {
|
||||||
|
return func(op *types.Operation) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
38
indexer/coin_storage_helper.go
Normal file
38
indexer/coin_storage_helper.go
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
// Copyright 2020 Coinbase, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package indexer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/storage"
|
||||||
|
"github.com/coinbase/rosetta-sdk-go/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ storage.CoinStorageHelper = (*CoinStorageHelper)(nil)
|
||||||
|
|
||||||
|
// CoinStorageHelper implements storage.CoinStorageHelper.
|
||||||
|
type CoinStorageHelper struct {
|
||||||
|
b *storage.BlockStorage
|
||||||
|
}
|
||||||
|
|
||||||
|
// CurrentBlockIdentifier returns the current head block identifier
|
||||||
|
// and is used to comply with the CoinStorageHelper interface.
|
||||||
|
func (h *CoinStorageHelper) CurrentBlockIdentifier(
|
||||||
|
ctx context.Context,
|
||||||
|
transaction storage.DatabaseTransaction,
|
||||||
|
) (*types.BlockIdentifier, error) {
|
||||||
|
return h.b.GetHeadBlockIdentifierTransactional(ctx, transaction)
|
||||||
|
}
|
|
@ -53,6 +53,9 @@ const (
|
||||||
// this is the estimated memory overhead for each
|
// this is the estimated memory overhead for each
|
||||||
// block fetched by the indexer.
|
// block fetched by the indexer.
|
||||||
sizeMultiplier = 15
|
sizeMultiplier = 15
|
||||||
|
|
||||||
|
// zeroValue is 0 as a string
|
||||||
|
zeroValue = "0"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -74,7 +77,6 @@ type Client interface {
|
||||||
var _ syncer.Handler = (*Indexer)(nil)
|
var _ syncer.Handler = (*Indexer)(nil)
|
||||||
var _ syncer.Helper = (*Indexer)(nil)
|
var _ syncer.Helper = (*Indexer)(nil)
|
||||||
var _ services.Indexer = (*Indexer)(nil)
|
var _ services.Indexer = (*Indexer)(nil)
|
||||||
var _ storage.CoinStorageHelper = (*Indexer)(nil)
|
|
||||||
|
|
||||||
// Indexer caches blocks and provides balance query functionality.
|
// Indexer caches blocks and provides balance query functionality.
|
||||||
type Indexer struct {
|
type Indexer struct {
|
||||||
|
@ -85,11 +87,12 @@ type Indexer struct {
|
||||||
|
|
||||||
client Client
|
client Client
|
||||||
|
|
||||||
asserter *asserter.Asserter
|
asserter *asserter.Asserter
|
||||||
database storage.Database
|
database storage.Database
|
||||||
blockStorage *storage.BlockStorage
|
blockStorage *storage.BlockStorage
|
||||||
coinStorage *storage.CoinStorage
|
balanceStorage *storage.BalanceStorage
|
||||||
workers []storage.BlockWorker
|
coinStorage *storage.CoinStorage
|
||||||
|
workers []storage.BlockWorker
|
||||||
|
|
||||||
waiter *waitTable
|
waiter *waitTable
|
||||||
}
|
}
|
||||||
|
@ -197,9 +200,21 @@ func Initialize(
|
||||||
asserter: asserter,
|
asserter: asserter,
|
||||||
}
|
}
|
||||||
|
|
||||||
coinStorage := storage.NewCoinStorage(localStore, i, asserter)
|
coinStorage := storage.NewCoinStorage(
|
||||||
|
localStore,
|
||||||
|
&CoinStorageHelper{blockStorage},
|
||||||
|
asserter,
|
||||||
|
)
|
||||||
i.coinStorage = coinStorage
|
i.coinStorage = coinStorage
|
||||||
i.workers = []storage.BlockWorker{coinStorage}
|
|
||||||
|
balanceStorage := storage.NewBalanceStorage(localStore)
|
||||||
|
balanceStorage.Initialize(
|
||||||
|
&BalanceStorageHelper{asserter},
|
||||||
|
&BalanceStorageHandler{},
|
||||||
|
)
|
||||||
|
i.balanceStorage = balanceStorage
|
||||||
|
|
||||||
|
i.workers = []storage.BlockWorker{coinStorage, balanceStorage}
|
||||||
|
|
||||||
return i, nil
|
return i, nil
|
||||||
}
|
}
|
||||||
|
@ -748,7 +763,11 @@ func (i *Indexer) GetBlockTransaction(
|
||||||
blockIdentifier *types.BlockIdentifier,
|
blockIdentifier *types.BlockIdentifier,
|
||||||
transactionIdentifier *types.TransactionIdentifier,
|
transactionIdentifier *types.TransactionIdentifier,
|
||||||
) (*types.Transaction, error) {
|
) (*types.Transaction, error) {
|
||||||
return i.blockStorage.GetBlockTransaction(ctx, blockIdentifier, transactionIdentifier)
|
return i.blockStorage.GetBlockTransaction(
|
||||||
|
ctx,
|
||||||
|
blockIdentifier,
|
||||||
|
transactionIdentifier,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCoins returns all unspent coins for a particular *types.AccountIdentifier.
|
// GetCoins returns all unspent coins for a particular *types.AccountIdentifier.
|
||||||
|
@ -759,11 +778,42 @@ func (i *Indexer) GetCoins(
|
||||||
return i.coinStorage.GetCoins(ctx, accountIdentifier)
|
return i.coinStorage.GetCoins(ctx, accountIdentifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CurrentBlockIdentifier returns the current head block identifier
|
// GetBalance returns the balance of an account
|
||||||
// and is used to comply with the CoinStorageHelper interface.
|
// at a particular *types.PartialBlockIdentifier.
|
||||||
func (i *Indexer) CurrentBlockIdentifier(
|
func (i *Indexer) GetBalance(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
transaction storage.DatabaseTransaction,
|
accountIdentifier *types.AccountIdentifier,
|
||||||
) (*types.BlockIdentifier, error) {
|
currency *types.Currency,
|
||||||
return i.blockStorage.GetHeadBlockIdentifierTransactional(ctx, transaction)
|
blockIdentifier *types.PartialBlockIdentifier,
|
||||||
|
) (*types.Amount, *types.BlockIdentifier, error) {
|
||||||
|
dbTx := i.database.NewDatabaseTransaction(ctx, false)
|
||||||
|
defer dbTx.Discard(ctx)
|
||||||
|
|
||||||
|
blockResponse, err := i.blockStorage.GetBlockLazyTransactional(
|
||||||
|
ctx,
|
||||||
|
blockIdentifier,
|
||||||
|
dbTx,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
amount, err := i.balanceStorage.GetBalanceTransactional(
|
||||||
|
ctx,
|
||||||
|
dbTx,
|
||||||
|
accountIdentifier,
|
||||||
|
currency,
|
||||||
|
blockResponse.Block.BlockIdentifier.Index,
|
||||||
|
)
|
||||||
|
if errors.Is(err, storage.ErrAccountMissing) {
|
||||||
|
return &types.Amount{
|
||||||
|
Value: zeroValue,
|
||||||
|
Currency: currency,
|
||||||
|
}, blockResponse.Block.BlockIdentifier, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return amount, blockResponse.Block.BlockIdentifier, nil
|
||||||
}
|
}
|
||||||
|
|
2
main.go
2
main.go
|
@ -154,7 +154,7 @@ func main() {
|
||||||
// requests.
|
// requests.
|
||||||
asserter, err := asserter.NewServer(
|
asserter, err := asserter.NewServer(
|
||||||
bitcoin.OperationTypes,
|
bitcoin.OperationTypes,
|
||||||
false,
|
services.HistoricalBalanceLookup,
|
||||||
[]*types.NetworkIdentifier{cfg.Network},
|
[]*types.NetworkIdentifier{cfg.Network},
|
||||||
nil,
|
nil,
|
||||||
)
|
)
|
||||||
|
|
|
@ -17,6 +17,38 @@ type Indexer struct {
|
||||||
mock.Mock
|
mock.Mock
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetBalance provides a mock function with given fields: _a0, _a1, _a2, _a3
|
||||||
|
func (_m *Indexer) GetBalance(_a0 context.Context, _a1 *types.AccountIdentifier, _a2 *types.Currency, _a3 *types.PartialBlockIdentifier) (*types.Amount, *types.BlockIdentifier, error) {
|
||||||
|
ret := _m.Called(_a0, _a1, _a2, _a3)
|
||||||
|
|
||||||
|
var r0 *types.Amount
|
||||||
|
if rf, ok := ret.Get(0).(func(context.Context, *types.AccountIdentifier, *types.Currency, *types.PartialBlockIdentifier) *types.Amount); ok {
|
||||||
|
r0 = rf(_a0, _a1, _a2, _a3)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*types.Amount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r1 *types.BlockIdentifier
|
||||||
|
if rf, ok := ret.Get(1).(func(context.Context, *types.AccountIdentifier, *types.Currency, *types.PartialBlockIdentifier) *types.BlockIdentifier); ok {
|
||||||
|
r1 = rf(_a0, _a1, _a2, _a3)
|
||||||
|
} else {
|
||||||
|
if ret.Get(1) != nil {
|
||||||
|
r1 = ret.Get(1).(*types.BlockIdentifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var r2 error
|
||||||
|
if rf, ok := ret.Get(2).(func(context.Context, *types.AccountIdentifier, *types.Currency, *types.PartialBlockIdentifier) error); ok {
|
||||||
|
r2 = rf(_a0, _a1, _a2, _a3)
|
||||||
|
} else {
|
||||||
|
r2 = ret.Error(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1, r2
|
||||||
|
}
|
||||||
|
|
||||||
// GetBlockLazy provides a mock function with given fields: _a0, _a1
|
// GetBlockLazy provides a mock function with given fields: _a0, _a1
|
||||||
func (_m *Indexer) GetBlockLazy(_a0 context.Context, _a1 *types.PartialBlockIdentifier) (*types.BlockResponse, error) {
|
func (_m *Indexer) GetBlockLazy(_a0 context.Context, _a1 *types.PartialBlockIdentifier) (*types.BlockResponse, error) {
|
||||||
ret := _m.Called(_a0, _a1)
|
ret := _m.Called(_a0, _a1)
|
||||||
|
|
|
@ -7,10 +7,12 @@
|
||||||
"http_timeout": 300,
|
"http_timeout": 300,
|
||||||
"max_retries": 5,
|
"max_retries": 5,
|
||||||
"retry_elapsed_time": 0,
|
"retry_elapsed_time": 0,
|
||||||
"max_online_connections": 0,
|
"max_online_connections": 1000,
|
||||||
"max_sync_concurrency": 0,
|
"max_sync_concurrency": 0,
|
||||||
"tip_delay": 1800,
|
"tip_delay": 1800,
|
||||||
"log_configuration": false,
|
"log_configuration": false,
|
||||||
|
"compression_disabled": true,
|
||||||
|
"memory_limit_disabled": true,
|
||||||
"data": {
|
"data": {
|
||||||
"active_reconciliation_concurrency": 0,
|
"active_reconciliation_concurrency": 0,
|
||||||
"inactive_reconciliation_concurrency": 0,
|
"inactive_reconciliation_concurrency": 0,
|
||||||
|
|
|
@ -7,10 +7,12 @@
|
||||||
"http_timeout": 300,
|
"http_timeout": 300,
|
||||||
"max_retries": 5,
|
"max_retries": 5,
|
||||||
"retry_elapsed_time": 0,
|
"retry_elapsed_time": 0,
|
||||||
"max_online_connections": 0,
|
"max_online_connections": 1000,
|
||||||
"max_sync_concurrency": 0,
|
"max_sync_concurrency": 0,
|
||||||
"tip_delay": 1800,
|
"tip_delay": 1800,
|
||||||
"log_configuration": false,
|
"log_configuration": false,
|
||||||
|
"compression_disabled": true,
|
||||||
|
"memory_limit_disabled": true,
|
||||||
"construction": {
|
"construction": {
|
||||||
"max_offline_connections": 0,
|
"max_offline_connections": 0,
|
||||||
"stale_depth": 0,
|
"stale_depth": 0,
|
||||||
|
|
|
@ -49,27 +49,51 @@ func (s *AccountAPIService) AccountBalance(
|
||||||
return nil, wrapErr(ErrUnavailableOffline, nil)
|
return nil, wrapErr(ErrUnavailableOffline, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
coins, block, err := s.i.GetCoins(ctx, request.AccountIdentifier)
|
// If we are fetching the current balance,
|
||||||
if err != nil {
|
// return all coins for an address and calculate
|
||||||
return nil, wrapErr(ErrUnableToGetCoins, err)
|
// the balance from those coins.
|
||||||
|
if request.BlockIdentifier == nil {
|
||||||
|
coins, block, err := s.i.GetCoins(ctx, request.AccountIdentifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(ErrUnableToGetCoins, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
balance := "0"
|
||||||
|
for _, coin := range coins {
|
||||||
|
balance, err = types.AddValues(balance, coin.Amount.Value)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(ErrUnableToParseIntermediateResult, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &types.AccountBalanceResponse{
|
||||||
|
BlockIdentifier: block,
|
||||||
|
Coins: coins,
|
||||||
|
Balances: []*types.Amount{
|
||||||
|
{
|
||||||
|
Value: balance,
|
||||||
|
Currency: s.config.Currency,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
balance := "0"
|
// If we are fetching a historical balance,
|
||||||
for _, coin := range coins {
|
// use balance storage and don't return coins.
|
||||||
balance, err = types.AddValues(balance, coin.Amount.Value)
|
amount, block, err := s.i.GetBalance(
|
||||||
if err != nil {
|
ctx,
|
||||||
return nil, wrapErr(ErrUnableToParseIntermediateResult, err)
|
request.AccountIdentifier,
|
||||||
}
|
s.config.Currency,
|
||||||
|
request.BlockIdentifier,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(ErrUnableToGetBalance, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &types.AccountBalanceResponse{
|
return &types.AccountBalanceResponse{
|
||||||
BlockIdentifier: block,
|
BlockIdentifier: block,
|
||||||
Coins: coins,
|
|
||||||
Balances: []*types.Amount{
|
Balances: []*types.Amount{
|
||||||
{
|
amount,
|
||||||
Value: balance,
|
|
||||||
Currency: s.config.Currency,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ func TestAccountBalance_Offline(t *testing.T) {
|
||||||
mockIndexer.AssertExpectations(t)
|
mockIndexer.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAccountBalance_Online(t *testing.T) {
|
func TestAccountBalance_Online_Current(t *testing.T) {
|
||||||
cfg := &configuration.Configuration{
|
cfg := &configuration.Configuration{
|
||||||
Mode: configuration.Online,
|
Mode: configuration.Online,
|
||||||
Currency: bitcoin.MainnetCurrency,
|
Currency: bitcoin.MainnetCurrency,
|
||||||
|
@ -104,3 +104,48 @@ func TestAccountBalance_Online(t *testing.T) {
|
||||||
|
|
||||||
mockIndexer.AssertExpectations(t)
|
mockIndexer.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAccountBalance_Online_Historical(t *testing.T) {
|
||||||
|
cfg := &configuration.Configuration{
|
||||||
|
Mode: configuration.Online,
|
||||||
|
Currency: bitcoin.MainnetCurrency,
|
||||||
|
}
|
||||||
|
mockIndexer := &mocks.Indexer{}
|
||||||
|
servicer := NewAccountAPIService(cfg, mockIndexer)
|
||||||
|
ctx := context.Background()
|
||||||
|
account := &types.AccountIdentifier{
|
||||||
|
Address: "hello",
|
||||||
|
}
|
||||||
|
block := &types.BlockIdentifier{
|
||||||
|
Index: 1000,
|
||||||
|
Hash: "block 1000",
|
||||||
|
}
|
||||||
|
partialBlock := &types.PartialBlockIdentifier{
|
||||||
|
Index: &block.Index,
|
||||||
|
}
|
||||||
|
amount := &types.Amount{
|
||||||
|
Value: "25",
|
||||||
|
Currency: bitcoin.MainnetCurrency,
|
||||||
|
}
|
||||||
|
|
||||||
|
mockIndexer.On(
|
||||||
|
"GetBalance",
|
||||||
|
ctx,
|
||||||
|
account,
|
||||||
|
bitcoin.MainnetCurrency,
|
||||||
|
partialBlock,
|
||||||
|
).Return(amount, block, nil).Once()
|
||||||
|
bal, err := servicer.AccountBalance(ctx, &types.AccountBalanceRequest{
|
||||||
|
AccountIdentifier: account,
|
||||||
|
BlockIdentifier: partialBlock,
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, &types.AccountBalanceResponse{
|
||||||
|
BlockIdentifier: block,
|
||||||
|
Balances: []*types.Amount{
|
||||||
|
amount,
|
||||||
|
},
|
||||||
|
}, bal)
|
||||||
|
|
||||||
|
mockIndexer.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
|
@ -54,6 +54,28 @@ func (s *BlockAPIService) Block(
|
||||||
return nil, wrapErr(ErrBlockNotFound, err)
|
return nil, wrapErr(ErrBlockNotFound, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Direct client to fetch transactions individually if
|
||||||
|
// more than inlineFetchLimit.
|
||||||
|
if len(blockResponse.OtherTransactions) > inlineFetchLimit {
|
||||||
|
return blockResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
txs := make([]*types.Transaction, len(blockResponse.OtherTransactions))
|
||||||
|
for i, otherTx := range blockResponse.OtherTransactions {
|
||||||
|
transaction, err := s.i.GetBlockTransaction(
|
||||||
|
ctx,
|
||||||
|
blockResponse.Block.BlockIdentifier,
|
||||||
|
otherTx,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, wrapErr(ErrTransactionNotFound, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
txs[i] = transaction
|
||||||
|
}
|
||||||
|
blockResponse.Block.Transactions = txs
|
||||||
|
|
||||||
|
blockResponse.OtherTransactions = nil
|
||||||
return blockResponse, nil
|
return blockResponse, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/coinbase/rosetta-bitcoin/configuration"
|
"github.com/coinbase/rosetta-bitcoin/configuration"
|
||||||
|
@ -46,7 +47,7 @@ func TestBlockService_Offline(t *testing.T) {
|
||||||
mockIndexer.AssertExpectations(t)
|
mockIndexer.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBlockService_Online(t *testing.T) {
|
func TestBlockService_Online_Inline(t *testing.T) {
|
||||||
cfg := &configuration.Configuration{
|
cfg := &configuration.Configuration{
|
||||||
Mode: configuration.Online,
|
Mode: configuration.Online,
|
||||||
}
|
}
|
||||||
|
@ -54,51 +55,49 @@ func TestBlockService_Online(t *testing.T) {
|
||||||
servicer := NewBlockAPIService(cfg, mockIndexer)
|
servicer := NewBlockAPIService(cfg, mockIndexer)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
block := &types.Block{
|
rawBlock := &types.Block{
|
||||||
BlockIdentifier: &types.BlockIdentifier{
|
BlockIdentifier: &types.BlockIdentifier{
|
||||||
Index: 100,
|
Index: 100,
|
||||||
Hash: "block 100",
|
Hash: "block 100",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
blockResponse := &types.BlockResponse{
|
|
||||||
Block: block,
|
|
||||||
OtherTransactions: []*types.TransactionIdentifier{
|
|
||||||
{
|
|
||||||
Hash: "tx1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
transaction := &types.Transaction{
|
transaction := &types.Transaction{
|
||||||
TransactionIdentifier: &types.TransactionIdentifier{
|
TransactionIdentifier: &types.TransactionIdentifier{
|
||||||
Hash: "tx1",
|
Hash: "tx1",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
block := &types.Block{
|
||||||
|
BlockIdentifier: &types.BlockIdentifier{
|
||||||
|
Index: 100,
|
||||||
|
Hash: "block 100",
|
||||||
|
},
|
||||||
|
Transactions: []*types.Transaction{
|
||||||
|
transaction,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
blockResponse := &types.BlockResponse{
|
||||||
|
Block: block,
|
||||||
|
}
|
||||||
|
|
||||||
t.Run("nil identifier", func(t *testing.T) {
|
t.Run("nil identifier", func(t *testing.T) {
|
||||||
mockIndexer.On(
|
mockIndexer.On(
|
||||||
"GetBlockLazy",
|
"GetBlockLazy",
|
||||||
ctx,
|
ctx,
|
||||||
(*types.PartialBlockIdentifier)(nil),
|
(*types.PartialBlockIdentifier)(nil),
|
||||||
).Return(
|
).Return(
|
||||||
blockResponse,
|
&types.BlockResponse{
|
||||||
|
Block: rawBlock,
|
||||||
|
OtherTransactions: []*types.TransactionIdentifier{
|
||||||
|
{
|
||||||
|
Hash: "tx1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
nil,
|
nil,
|
||||||
).Once()
|
).Once()
|
||||||
b, err := servicer.Block(ctx, &types.BlockRequest{})
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, blockResponse, b)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("populated identifier", func(t *testing.T) {
|
|
||||||
pbIdentifier := types.ConstructPartialBlockIdentifier(block.BlockIdentifier)
|
|
||||||
mockIndexer.On("GetBlockLazy", ctx, pbIdentifier).Return(blockResponse, nil).Once()
|
|
||||||
b, err := servicer.Block(ctx, &types.BlockRequest{
|
|
||||||
BlockIdentifier: pbIdentifier,
|
|
||||||
})
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Equal(t, blockResponse, b)
|
|
||||||
|
|
||||||
mockIndexer.On(
|
mockIndexer.On(
|
||||||
"GetBlockTransaction",
|
"GetBlockTransaction",
|
||||||
ctx,
|
ctx,
|
||||||
|
@ -108,13 +107,107 @@ func TestBlockService_Online(t *testing.T) {
|
||||||
transaction,
|
transaction,
|
||||||
nil,
|
nil,
|
||||||
).Once()
|
).Once()
|
||||||
blockTransaction, err := servicer.BlockTransaction(ctx, &types.BlockTransactionRequest{
|
b, err := servicer.Block(ctx, &types.BlockRequest{})
|
||||||
BlockIdentifier: blockResponse.Block.BlockIdentifier,
|
assert.Nil(t, err)
|
||||||
TransactionIdentifier: transaction.TransactionIdentifier,
|
assert.Equal(t, blockResponse, b)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("populated identifier", func(t *testing.T) {
|
||||||
|
pbIdentifier := types.ConstructPartialBlockIdentifier(block.BlockIdentifier)
|
||||||
|
mockIndexer.On(
|
||||||
|
"GetBlockLazy",
|
||||||
|
ctx,
|
||||||
|
pbIdentifier,
|
||||||
|
).Return(
|
||||||
|
&types.BlockResponse{
|
||||||
|
Block: rawBlock,
|
||||||
|
OtherTransactions: []*types.TransactionIdentifier{
|
||||||
|
{
|
||||||
|
Hash: "tx1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
).Once()
|
||||||
|
mockIndexer.On(
|
||||||
|
"GetBlockTransaction",
|
||||||
|
ctx,
|
||||||
|
blockResponse.Block.BlockIdentifier,
|
||||||
|
transaction.TransactionIdentifier,
|
||||||
|
).Return(
|
||||||
|
transaction,
|
||||||
|
nil,
|
||||||
|
).Once()
|
||||||
|
b, err := servicer.Block(ctx, &types.BlockRequest{
|
||||||
|
BlockIdentifier: pbIdentifier,
|
||||||
})
|
})
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, transaction, blockTransaction.Transaction)
|
assert.Equal(t, blockResponse, b)
|
||||||
})
|
})
|
||||||
|
|
||||||
mockIndexer.AssertExpectations(t)
|
mockIndexer.AssertExpectations(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBlockService_Online_External(t *testing.T) {
|
||||||
|
cfg := &configuration.Configuration{
|
||||||
|
Mode: configuration.Online,
|
||||||
|
}
|
||||||
|
mockIndexer := &mocks.Indexer{}
|
||||||
|
servicer := NewBlockAPIService(cfg, mockIndexer)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
blockResponse := &types.BlockResponse{
|
||||||
|
Block: &types.Block{
|
||||||
|
BlockIdentifier: &types.BlockIdentifier{
|
||||||
|
Index: 100,
|
||||||
|
Hash: "block 100",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
otherTxs := []*types.TransactionIdentifier{}
|
||||||
|
for i := 0; i < 200; i++ {
|
||||||
|
otherTxs = append(otherTxs, &types.TransactionIdentifier{
|
||||||
|
Hash: fmt.Sprintf("tx%d", i),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
blockResponse.OtherTransactions = otherTxs
|
||||||
|
|
||||||
|
mockIndexer.On(
|
||||||
|
"GetBlockLazy",
|
||||||
|
ctx,
|
||||||
|
(*types.PartialBlockIdentifier)(nil),
|
||||||
|
).Return(
|
||||||
|
blockResponse,
|
||||||
|
nil,
|
||||||
|
).Once()
|
||||||
|
b, err := servicer.Block(ctx, &types.BlockRequest{})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, blockResponse, b)
|
||||||
|
|
||||||
|
for _, otherTx := range b.OtherTransactions {
|
||||||
|
tx := &types.Transaction{
|
||||||
|
TransactionIdentifier: otherTx,
|
||||||
|
}
|
||||||
|
mockIndexer.On(
|
||||||
|
"GetBlockTransaction",
|
||||||
|
ctx,
|
||||||
|
blockResponse.Block.BlockIdentifier,
|
||||||
|
otherTx,
|
||||||
|
).Return(
|
||||||
|
tx,
|
||||||
|
nil,
|
||||||
|
).Once()
|
||||||
|
|
||||||
|
bTx, err := servicer.BlockTransaction(ctx, &types.BlockTransactionRequest{
|
||||||
|
BlockIdentifier: blockResponse.Block.BlockIdentifier,
|
||||||
|
TransactionIdentifier: otherTx,
|
||||||
|
})
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Equal(t, &types.BlockTransactionResponse{
|
||||||
|
Transaction: tx,
|
||||||
|
}, bTx)
|
||||||
|
}
|
||||||
|
|
||||||
|
mockIndexer.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
|
@ -40,6 +40,7 @@ var (
|
||||||
ErrUnableToGetCoins,
|
ErrUnableToGetCoins,
|
||||||
ErrTransactionNotFound,
|
ErrTransactionNotFound,
|
||||||
ErrCouldNotGetFeeRate,
|
ErrCouldNotGetFeeRate,
|
||||||
|
ErrUnableToGetBalance,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrUnimplemented is returned when an endpoint
|
// ErrUnimplemented is returned when an endpoint
|
||||||
|
@ -59,8 +60,9 @@ var (
|
||||||
// ErrNotReady is returned when bitcoind is not
|
// ErrNotReady is returned when bitcoind is not
|
||||||
// yet ready to serve queries.
|
// yet ready to serve queries.
|
||||||
ErrNotReady = &types.Error{
|
ErrNotReady = &types.Error{
|
||||||
Code: 2, //nolint
|
Code: 2, //nolint
|
||||||
Message: "Bitcoind is not ready",
|
Message: "Bitcoind is not ready",
|
||||||
|
Retriable: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrBitcoind is returned when bitcoind
|
// ErrBitcoind is returned when bitcoind
|
||||||
|
@ -173,6 +175,14 @@ var (
|
||||||
Code: 17, // nolint
|
Code: 17, // nolint
|
||||||
Message: "Could not get suggested fee rate",
|
Message: "Could not get suggested fee rate",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrUnableToGetBalance is returned by the indexer
|
||||||
|
// when it is not possible to get the balance
|
||||||
|
// of a *types.AccountIdentifier.
|
||||||
|
ErrUnableToGetBalance = &types.Error{
|
||||||
|
Code: 18, //nolint
|
||||||
|
Message: "Unable to get balance",
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// wrapErr adds details to the types.Error provided. We use a function
|
// wrapErr adds details to the types.Error provided. We use a function
|
||||||
|
@ -180,8 +190,8 @@ var (
|
||||||
// errors.
|
// errors.
|
||||||
func wrapErr(rErr *types.Error, err error) *types.Error {
|
func wrapErr(rErr *types.Error, err error) *types.Error {
|
||||||
newErr := &types.Error{
|
newErr := &types.Error{
|
||||||
Code: rErr.Code,
|
Code: rErr.Code,
|
||||||
Message: rErr.Message,
|
Message: rErr.Message,
|
||||||
Retriable: rErr.Retriable,
|
Retriable: rErr.Retriable,
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -95,9 +95,10 @@ func (s *NetworkAPIService) NetworkOptions(
|
||||||
MiddlewareVersion: &MiddlewareVersion,
|
MiddlewareVersion: &MiddlewareVersion,
|
||||||
},
|
},
|
||||||
Allow: &types.Allow{
|
Allow: &types.Allow{
|
||||||
OperationStatuses: bitcoin.OperationStatuses,
|
OperationStatuses: bitcoin.OperationStatuses,
|
||||||
OperationTypes: bitcoin.OperationTypes,
|
OperationTypes: bitcoin.OperationTypes,
|
||||||
Errors: Errors,
|
Errors: Errors,
|
||||||
|
HistoricalBalanceLookup: HistoricalBalanceLookup,
|
||||||
},
|
},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
middlewareVersion = "0.0.4"
|
middlewareVersion = "0.0.5"
|
||||||
defaultNetworkOptions = &types.NetworkOptionsResponse{
|
defaultNetworkOptions = &types.NetworkOptionsResponse{
|
||||||
Version: &types.Version{
|
Version: &types.Version{
|
||||||
RosettaVersion: types.RosettaAPIVersion,
|
RosettaVersion: types.RosettaAPIVersion,
|
||||||
|
@ -35,9 +35,10 @@ var (
|
||||||
MiddlewareVersion: &middlewareVersion,
|
MiddlewareVersion: &middlewareVersion,
|
||||||
},
|
},
|
||||||
Allow: &types.Allow{
|
Allow: &types.Allow{
|
||||||
OperationStatuses: bitcoin.OperationStatuses,
|
OperationStatuses: bitcoin.OperationStatuses,
|
||||||
OperationTypes: bitcoin.OperationTypes,
|
OperationTypes: bitcoin.OperationTypes,
|
||||||
Errors: Errors,
|
Errors: Errors,
|
||||||
|
HistoricalBalanceLookup: HistoricalBalanceLookup,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,14 @@ const (
|
||||||
// NodeVersion is the version of
|
// NodeVersion is the version of
|
||||||
// bitcoin core we are using.
|
// bitcoin core we are using.
|
||||||
NodeVersion = "0.20.1"
|
NodeVersion = "0.20.1"
|
||||||
|
|
||||||
|
// HistoricalBalanceLookup indicates
|
||||||
|
// that historical balance lookup is supported.
|
||||||
|
HistoricalBalanceLookup = true
|
||||||
|
|
||||||
|
// inlineFetchLimit is the maximum number
|
||||||
|
// of transactions to fetch inline.
|
||||||
|
inlineFetchLimit = 100
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -34,7 +42,7 @@ var (
|
||||||
// variable instead of a constant because
|
// variable instead of a constant because
|
||||||
// we typically need the pointer of this
|
// we typically need the pointer of this
|
||||||
// value.
|
// value.
|
||||||
MiddlewareVersion = "0.0.4"
|
MiddlewareVersion = "0.0.5"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client is used by the servicers to get Peer information
|
// Client is used by the servicers to get Peer information
|
||||||
|
@ -48,7 +56,10 @@ type Client interface {
|
||||||
|
|
||||||
// Indexer is used by the servicers to get block and account data.
|
// Indexer is used by the servicers to get block and account data.
|
||||||
type Indexer interface {
|
type Indexer interface {
|
||||||
GetBlockLazy(context.Context, *types.PartialBlockIdentifier) (*types.BlockResponse, error)
|
GetBlockLazy(
|
||||||
|
context.Context,
|
||||||
|
*types.PartialBlockIdentifier,
|
||||||
|
) (*types.BlockResponse, error)
|
||||||
GetBlockTransaction(
|
GetBlockTransaction(
|
||||||
context.Context,
|
context.Context,
|
||||||
*types.BlockIdentifier,
|
*types.BlockIdentifier,
|
||||||
|
@ -62,6 +73,12 @@ type Indexer interface {
|
||||||
context.Context,
|
context.Context,
|
||||||
[]*types.Coin,
|
[]*types.Coin,
|
||||||
) ([]*bitcoin.ScriptPubKey, error)
|
) ([]*bitcoin.ScriptPubKey, error)
|
||||||
|
GetBalance(
|
||||||
|
context.Context,
|
||||||
|
*types.AccountIdentifier,
|
||||||
|
*types.Currency,
|
||||||
|
*types.PartialBlockIdentifier,
|
||||||
|
) (*types.Amount, *types.BlockIdentifier, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type unsignedTransaction struct {
|
type unsignedTransaction struct {
|
||||||
|
|
Loading…
Reference in a new issue