mirror of
https://github.com/AthanorLabs/atomic-swap.git
synced 2026-01-07 21:34:05 -05:00
additonal erc20 and taker decimal checks (#478)
This commit is contained in:
@@ -550,7 +550,8 @@ func runBalances(ctx *cli.Context) error {
|
||||
fmt.Printf("Token: %s\n", tokenBalance.TokenInfo.Address)
|
||||
fmt.Printf("Name: %q\n", tokenBalance.TokenInfo.Name)
|
||||
fmt.Printf("Symbol: %q\n", tokenBalance.TokenInfo.Symbol)
|
||||
fmt.Printf("Balance: %s\n", tokenBalance.AsStandard().Text('f'))
|
||||
fmt.Printf("Decimals: %d\n", tokenBalance.TokenInfo.NumDecimals)
|
||||
fmt.Printf("Balance: %s\n", tokenBalance.AsStdString())
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@ import (
|
||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/athanorlabs/atomic-swap/coins"
|
||||
"github.com/athanorlabs/atomic-swap/daemon"
|
||||
contracts "github.com/athanorlabs/atomic-swap/ethereum"
|
||||
"github.com/athanorlabs/atomic-swap/rpcclient"
|
||||
"github.com/athanorlabs/atomic-swap/tests"
|
||||
)
|
||||
@@ -19,7 +21,8 @@ import (
|
||||
type swapCLITestSuite struct {
|
||||
suite.Suite
|
||||
conf *daemon.SwapdConfig
|
||||
mockTokens map[string]ethcommon.Address
|
||||
mockTether *coins.ERC20TokenInfo
|
||||
mockDAI *coins.ERC20TokenInfo
|
||||
}
|
||||
|
||||
func TestRunSwapcliWithDaemonTests(t *testing.T) {
|
||||
@@ -28,7 +31,10 @@ func TestRunSwapcliWithDaemonTests(t *testing.T) {
|
||||
s.conf = daemon.CreateTestConf(t, tests.GetMakerTestKey(t))
|
||||
t.Setenv("SWAPD_PORT", strconv.Itoa(int(s.conf.RPCPort)))
|
||||
daemon.LaunchDaemons(t, 10*time.Minute, s.conf)
|
||||
s.mockTokens = daemon.GetMockTokens(t, s.conf.EthereumClient)
|
||||
ec := s.conf.EthereumClient.Raw()
|
||||
pk := s.conf.EthereumClient.PrivateKey()
|
||||
s.mockTether = contracts.GetMockTether(t, ec, pk)
|
||||
s.mockDAI = contracts.GetMockDAI(t, ec, pk)
|
||||
suite.Run(t, s)
|
||||
}
|
||||
|
||||
@@ -37,9 +43,9 @@ func (s *swapCLITestSuite) rpcEndpoint() *rpcclient.Client {
|
||||
}
|
||||
|
||||
func (s *swapCLITestSuite) mockDaiAddr() ethcommon.Address {
|
||||
return s.mockTokens[daemon.MockDAI]
|
||||
return s.mockDAI.Address
|
||||
}
|
||||
|
||||
func (s *swapCLITestSuite) mockTetherAddr() ethcommon.Address {
|
||||
return s.mockTokens[daemon.MockTether]
|
||||
return s.mockTether.Address
|
||||
}
|
||||
|
||||
121
coins/coins.go
121
coins/coins.go
@@ -70,7 +70,8 @@ func MoneroToPiconero(xmrAmt *apd.Decimal) *PiconeroAmount {
|
||||
// We do input validation and reject XMR values with more than 12 decimal
|
||||
// places from external sources, so no rounding will happen with those
|
||||
// values below.
|
||||
if err := roundToDecimalPlace(pnAmt, pnAmt, 0); err != nil {
|
||||
pnAmt, err := roundToDecimalPlace(pnAmt, 0)
|
||||
if err != nil {
|
||||
panic(err) // shouldn't be possible
|
||||
}
|
||||
return (*PiconeroAmount)(pnAmt)
|
||||
@@ -123,12 +124,24 @@ func FmtPiconeroAsXMR(piconeros uint64) string {
|
||||
// EthAssetAmount represents an amount of an Ethereum asset (ie. ether or an ERC20)
|
||||
type EthAssetAmount interface {
|
||||
BigInt() *big.Int
|
||||
AsStandard() *apd.Decimal
|
||||
StandardSymbol() string
|
||||
AsStd() *apd.Decimal
|
||||
AsStdString() string
|
||||
StdSymbol() string
|
||||
IsToken() bool
|
||||
NumStdDecimals() uint8
|
||||
TokenAddress() ethcommon.Address
|
||||
}
|
||||
|
||||
// NewEthAssetAmount accepts an amount, in standard units, for ETH or a token and
|
||||
// returns a type implementing EthAssetAmount. If the token is nil, we assume
|
||||
// the asset is ETH.
|
||||
func NewEthAssetAmount(amount *apd.Decimal, token *ERC20TokenInfo) EthAssetAmount {
|
||||
if token == nil {
|
||||
return EtherToWei(amount)
|
||||
}
|
||||
return NewTokenAmountFromDecimals(amount, token)
|
||||
}
|
||||
|
||||
// WeiAmount represents some amount of ETH in the smallest denomination (Wei)
|
||||
type WeiAmount apd.Decimal
|
||||
|
||||
@@ -183,7 +196,8 @@ func EtherToWei(ethAmt *apd.Decimal) *WeiAmount {
|
||||
// We do input validation on provided amounts and prevent values with
|
||||
// more than 18 decimal places, so no rounding happens with such values
|
||||
// below.
|
||||
if err := roundToDecimalPlace(weiAmt, weiAmt, 0); err != nil {
|
||||
weiAmt, err := roundToDecimalPlace(weiAmt, 0)
|
||||
if err != nil {
|
||||
panic(err) // shouldn't be possible
|
||||
}
|
||||
return ToWeiAmount(weiAmt)
|
||||
@@ -220,19 +234,24 @@ func (a *WeiAmount) AsEtherString() string {
|
||||
return a.AsEther().Text('f')
|
||||
}
|
||||
|
||||
// AsStandard is an alias for AsEther, returning the Wei amount as ETH
|
||||
func (a *WeiAmount) AsStandard() *apd.Decimal {
|
||||
// AsStd is an alias for AsEther, returning the Wei amount as ETH
|
||||
func (a *WeiAmount) AsStd() *apd.Decimal {
|
||||
return a.AsEther()
|
||||
}
|
||||
|
||||
// AsStandardString is an alias for AsEtherString, returning the Wei amount as
|
||||
// AsStdString is an alias for AsEtherString, returning the Wei amount as
|
||||
// an ETH string
|
||||
func (a *WeiAmount) AsStandardString() *apd.Decimal {
|
||||
return a.AsEther()
|
||||
func (a *WeiAmount) AsStdString() string {
|
||||
return a.AsEther().Text('f')
|
||||
}
|
||||
|
||||
// StandardSymbol returns the string "ETH"
|
||||
func (a *WeiAmount) StandardSymbol() string {
|
||||
// NumStdDecimals returns 18
|
||||
func (a *WeiAmount) NumStdDecimals() uint8 {
|
||||
return NumEtherDecimals
|
||||
}
|
||||
|
||||
// StdSymbol returns the string "ETH"
|
||||
func (a *WeiAmount) StdSymbol() string {
|
||||
return "ETH"
|
||||
}
|
||||
|
||||
@@ -283,50 +302,49 @@ func (t *ERC20TokenInfo) SanitizedSymbol() string {
|
||||
|
||||
// ERC20TokenAmount represents some amount of an ERC20 token in the smallest denomination
|
||||
type ERC20TokenAmount struct {
|
||||
Amount *apd.Decimal `json:"amount" validate:"required"` // in smallest non-divisible units of token
|
||||
Amount *apd.Decimal `json:"amount" validate:"required"` // in standard units
|
||||
TokenInfo *ERC20TokenInfo `json:"tokenInfo" validate:"required"`
|
||||
}
|
||||
|
||||
// NewERC20TokenAmountFromBigInt converts some amount in the smallest token denomination into an ERC20TokenAmount.
|
||||
func NewERC20TokenAmountFromBigInt(amount *big.Int, tokenInfo *ERC20TokenInfo) *ERC20TokenAmount {
|
||||
// NewERC20TokenAmountFromBigInt converts some amount in the smallest token denomination
|
||||
// into an ERC20TokenAmount.
|
||||
func NewERC20TokenAmountFromBigInt(amount *big.Int, token *ERC20TokenInfo) *ERC20TokenAmount {
|
||||
asDecimal := new(apd.Decimal)
|
||||
asDecimal.Coeff.SetBytes(amount.Bytes())
|
||||
decreaseExponent(asDecimal, token.NumDecimals)
|
||||
_, _ = asDecimal.Reduce(asDecimal)
|
||||
|
||||
return &ERC20TokenAmount{
|
||||
Amount: asDecimal,
|
||||
TokenInfo: tokenInfo,
|
||||
TokenInfo: token,
|
||||
}
|
||||
}
|
||||
|
||||
// NewERC20TokenAmount converts some amount in the smallest token denomination into an ERC20TokenAmount.
|
||||
func NewERC20TokenAmount(amount int64, tokenInfo *ERC20TokenInfo) *ERC20TokenAmount {
|
||||
return &ERC20TokenAmount{
|
||||
Amount: apd.New(amount, 0),
|
||||
TokenInfo: tokenInfo,
|
||||
// NewTokenAmountFromDecimals converts an amount in standard units from
|
||||
// apd.Decimal into the ERC20TokenAmount type. During the conversion, rounding
|
||||
// may occur if the input value is too precise for the token's decimals.
|
||||
func NewTokenAmountFromDecimals(amount *apd.Decimal, token *ERC20TokenInfo) *ERC20TokenAmount {
|
||||
if ExceedsDecimals(amount, token.NumDecimals) {
|
||||
log.Warnf("Converting amount=%s (digits=%d) to token amount required rounding",
|
||||
amount.Text('f'), token.NumDecimals)
|
||||
roundedAmt, err := roundToDecimalPlace(amount, token.NumDecimals)
|
||||
if err != nil {
|
||||
panic(err) // shouldn't be possible
|
||||
}
|
||||
amount = roundedAmt
|
||||
}
|
||||
}
|
||||
|
||||
// NewERC20TokenAmountFromDecimals converts some amount for a token in its
|
||||
// standard form into the smallest denomination that the token supports. For
|
||||
// example, if amount is 1.99 and decimals is 4, the resulting value stored is
|
||||
// 19900
|
||||
func NewERC20TokenAmountFromDecimals(amount *apd.Decimal, tokenInfo *ERC20TokenInfo) *ERC20TokenAmount {
|
||||
adjusted := new(apd.Decimal).Set(amount)
|
||||
increaseExponent(adjusted, tokenInfo.NumDecimals)
|
||||
// If we are rejecting token amounts that have too many decimal places on input, rounding
|
||||
// below will never occur.
|
||||
if err := roundToDecimalPlace(adjusted, adjusted, 0); err != nil {
|
||||
panic(err) // this shouldn't be possible
|
||||
}
|
||||
return &ERC20TokenAmount{
|
||||
Amount: adjusted,
|
||||
TokenInfo: tokenInfo,
|
||||
Amount: amount,
|
||||
TokenInfo: token,
|
||||
}
|
||||
}
|
||||
|
||||
// BigInt returns the ERC20TokenAmount as a *big.Int
|
||||
func (a *ERC20TokenAmount) BigInt() *big.Int {
|
||||
wholeTokenUnits := new(apd.Decimal)
|
||||
cond, err := decimalCtx.Quantize(wholeTokenUnits, a.Amount, 0)
|
||||
wholeTokenUnits := new(apd.Decimal).Set(a.Amount)
|
||||
increaseExponent(wholeTokenUnits, a.TokenInfo.NumDecimals)
|
||||
cond, err := decimalCtx.Quantize(wholeTokenUnits, wholeTokenUnits, 0)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -334,24 +352,28 @@ func (a *ERC20TokenAmount) BigInt() *big.Int {
|
||||
log.Warn("Converting ERC20TokenAmount=%s (digits=%d) to big.Int required rounding",
|
||||
a.Amount, a.TokenInfo.NumDecimals)
|
||||
}
|
||||
|
||||
return new(big.Int).SetBytes(wholeTokenUnits.Coeff.Bytes())
|
||||
}
|
||||
|
||||
// AsStandard returns the amount in standard form
|
||||
func (a *ERC20TokenAmount) AsStandard() *apd.Decimal {
|
||||
tokenAmt := new(apd.Decimal).Set(a.Amount)
|
||||
decreaseExponent(tokenAmt, a.TokenInfo.NumDecimals)
|
||||
_, _ = tokenAmt.Reduce(tokenAmt)
|
||||
return tokenAmt
|
||||
// AsStd returns the amount in standard units
|
||||
func (a *ERC20TokenAmount) AsStd() *apd.Decimal {
|
||||
return a.Amount
|
||||
}
|
||||
|
||||
// AsStandardString returns the amount as a standard (decimal adjusted) string
|
||||
func (a *ERC20TokenAmount) AsStandardString() string {
|
||||
return a.AsStandard().Text('f')
|
||||
// AsStdString returns the ERC20TokenAmount as a base10 string in standard units.
|
||||
func (a *ERC20TokenAmount) AsStdString() string {
|
||||
return a.String()
|
||||
}
|
||||
|
||||
// StandardSymbol returns the token's symbol in a format that is safe to log and display
|
||||
func (a *ERC20TokenAmount) StandardSymbol() string {
|
||||
// NumStdDecimals returns the max decimal precision of the token's standard
|
||||
// representation
|
||||
func (a *ERC20TokenAmount) NumStdDecimals() uint8 {
|
||||
return a.TokenInfo.NumDecimals
|
||||
}
|
||||
|
||||
// StdSymbol returns the token's symbol in a format that is safe to log and display
|
||||
func (a *ERC20TokenAmount) StdSymbol() string {
|
||||
return a.TokenInfo.SanitizedSymbol()
|
||||
}
|
||||
|
||||
@@ -365,8 +387,7 @@ func (a *ERC20TokenAmount) TokenAddress() ethcommon.Address {
|
||||
return a.TokenInfo.Address
|
||||
}
|
||||
|
||||
// String returns the ERC20TokenAmount as a base10 string of the token's smallest,
|
||||
// non-divisible units.
|
||||
// String returns the ERC20TokenAmount as a base10 string in standard units.
|
||||
func (a *ERC20TokenAmount) String() string {
|
||||
return a.Amount.Text('f')
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ func TestWeiAmount(t *testing.T) {
|
||||
wei := EtherToWei(amount)
|
||||
assert.Equal(t, "33300000000000000000", wei.String())
|
||||
assert.Equal(t, "33.3", wei.AsEther().String())
|
||||
assert.Equal(t, "33.3", wei.AsStandard().String()) // alias for AsEther
|
||||
assert.Equal(t, "33.3", wei.AsStdString())
|
||||
|
||||
amountUint := int64(8181)
|
||||
WeiAmount := IntToWei(amountUint)
|
||||
@@ -114,42 +114,37 @@ func TestERC20TokenAmount(t *testing.T) {
|
||||
tokenInfo := NewERC20TokenInfo(ethcommon.Address{}, numDecimals, "", "")
|
||||
|
||||
amount := StrToDecimal("33.999999999")
|
||||
wei := NewERC20TokenAmountFromDecimals(amount, tokenInfo)
|
||||
assert.Equal(t, amount.String(), wei.AsStandard().String())
|
||||
tokenAmt := NewTokenAmountFromDecimals(amount, tokenInfo)
|
||||
assert.Equal(t, amount.String(), tokenAmt.AsStdString())
|
||||
|
||||
amount = StrToDecimal("33.000000005")
|
||||
wei = NewERC20TokenAmountFromDecimals(amount, tokenInfo)
|
||||
assert.Equal(t, "33.000000005", wei.AsStandard().String())
|
||||
tokenAmt = NewTokenAmountFromDecimals(amount, tokenInfo)
|
||||
assert.Equal(t, "33.000000005", tokenAmt.AsStdString())
|
||||
|
||||
amount = StrToDecimal("33.0000000005")
|
||||
wei = NewERC20TokenAmountFromDecimals(amount, tokenInfo)
|
||||
assert.Equal(t, "33.000000001", wei.AsStandard().String())
|
||||
tokenAmt = NewTokenAmountFromDecimals(amount, tokenInfo)
|
||||
assert.Equal(t, "33.000000001", tokenAmt.AsStdString())
|
||||
|
||||
amount = StrToDecimal("999999999999999999.0000000005")
|
||||
wei = NewERC20TokenAmountFromDecimals(amount, tokenInfo)
|
||||
assert.Equal(t, "999999999999999999.000000001", wei.AsStandard().String())
|
||||
|
||||
amountUint := int64(8181)
|
||||
tokenAmt := NewERC20TokenAmount(amountUint, tokenInfo)
|
||||
assert.Equal(t, amountUint, tokenAmt.BigInt().Int64())
|
||||
tokenAmt = NewTokenAmountFromDecimals(amount, tokenInfo)
|
||||
assert.Equal(t, "999999999999999999.000000001", tokenAmt.AsStdString())
|
||||
}
|
||||
|
||||
func TestNewERC20TokenAmountFromBigInt(t *testing.T) {
|
||||
bi := big.NewInt(4321)
|
||||
token := NewERC20TokenAmountFromBigInt(bi, &ERC20TokenInfo{NumDecimals: 2})
|
||||
assert.Equal(t, "4321", token.String())
|
||||
assert.Equal(t, "43.21", token.AsStandard().String())
|
||||
tokenAmt := NewERC20TokenAmountFromBigInt(bi, &ERC20TokenInfo{NumDecimals: 2})
|
||||
assert.Equal(t, "43.21", tokenAmt.String())
|
||||
assert.Equal(t, "43.21", tokenAmt.AsStdString())
|
||||
assert.Equal(t, "4321", tokenAmt.BigInt().String())
|
||||
}
|
||||
|
||||
func TestNewERC20TokenAmountFromDecimals(t *testing.T) {
|
||||
stdAmount := StrToDecimal("0.19")
|
||||
token := NewERC20TokenAmountFromDecimals(stdAmount, &ERC20TokenInfo{NumDecimals: 1})
|
||||
token := NewTokenAmountFromDecimals(stdAmount, &ERC20TokenInfo{NumDecimals: 1})
|
||||
|
||||
// There's only one decimal place, so this is getting rounded to 2
|
||||
// under the current implementation. It's not entirely clear what
|
||||
// the ideal behavior is.
|
||||
assert.Equal(t, "2", token.String())
|
||||
assert.Equal(t, "0.2", token.AsStandard().String())
|
||||
// under the current implementation
|
||||
assert.Equal(t, "0.2", token.String())
|
||||
}
|
||||
|
||||
func TestJSONMarshal(t *testing.T) {
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
package coins
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/cockroachdb/apd/v3"
|
||||
)
|
||||
|
||||
@@ -21,7 +24,7 @@ func CalcExchangeRate(xmrPrice *apd.Decimal, ethPrice *apd.Decimal) (*ExchangeRa
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = roundToDecimalPlace(rate, rate, MaxExchangeRateDecimals); err != nil {
|
||||
if rate, err = roundToDecimalPlace(rate, MaxExchangeRateDecimals); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ToExchangeRate(rate), nil
|
||||
@@ -51,48 +54,72 @@ func (r *ExchangeRate) MarshalText() ([]byte, error) {
|
||||
return r.Decimal().MarshalText()
|
||||
}
|
||||
|
||||
// ToXMR converts an ETH amount to an XMR amount with the given exchange rate
|
||||
func (r *ExchangeRate) ToXMR(ethAmount *apd.Decimal) (*apd.Decimal, error) {
|
||||
// ToXMR converts an ETH amount to an XMR amount with the given exchange rate.
|
||||
// If the calculated value would have fractional piconeros, an error is
|
||||
// returned.
|
||||
func (r *ExchangeRate) ToXMR(ethAssetAmt EthAssetAmount) (*apd.Decimal, error) {
|
||||
xmrAmt := new(apd.Decimal)
|
||||
_, err := decimalCtx.Quo(xmrAmt, ethAmount, r.Decimal())
|
||||
_, err := decimalCtx.Quo(xmrAmt, ethAssetAmt.AsStd(), r.Decimal())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = roundToDecimalPlace(xmrAmt, xmrAmt, NumMoneroDecimals); err != nil {
|
||||
return nil, err
|
||||
|
||||
if ExceedsDecimals(xmrAmt, NumMoneroDecimals) {
|
||||
errMsg := fmt.Sprintf(
|
||||
"%s %s / %s exceeds XMR's %d decimal precision",
|
||||
ethAssetAmt.AsStdString(), ethAssetAmt.StdSymbol(), r, NumMoneroDecimals,
|
||||
)
|
||||
suggestedAltAmt := calcAltNumeratorAmount(ethAssetAmt.NumStdDecimals(), NumMoneroDecimals, r.Decimal(), xmrAmt)
|
||||
if suggestedAltAmt != nil {
|
||||
errMsg = fmt.Sprintf("%s, try %s", errMsg, suggestedAltAmt.Text('f'))
|
||||
}
|
||||
return nil, errors.New(errMsg)
|
||||
}
|
||||
|
||||
return xmrAmt, nil
|
||||
}
|
||||
|
||||
// ToETH converts an XMR amount to an ETH amount with the given exchange rate
|
||||
// ToETH converts an XMR amount to an ETH amount with the given exchange rate.
|
||||
// If the calculated result would have fractional wei, an error is returned.
|
||||
func (r *ExchangeRate) ToETH(xmrAmount *apd.Decimal) (*apd.Decimal, error) {
|
||||
ethAmt := new(apd.Decimal)
|
||||
_, err := decimalCtx.Mul(ethAmt, r.Decimal(), xmrAmount)
|
||||
_, err := decimalCtx.Mul(ethAmt, xmrAmount, r.Decimal())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Assuming the xmrAmount was capped at 12 decimal places and the exchange
|
||||
// rate was capped at 6 decimal places, you can't generate more than 18
|
||||
// decimal places below, so no rounding occurs.
|
||||
if err = roundToDecimalPlace(ethAmt, ethAmt, NumEtherDecimals); err != nil {
|
||||
// decimal places below, so the error below can't happen.
|
||||
if ExceedsDecimals(ethAmt, NumEtherDecimals) {
|
||||
err := fmt.Errorf("%s XMR * %s exceeds ETH's %d decimal precision",
|
||||
xmrAmount.Text('f'), r, NumEtherDecimals)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ethAmt, nil
|
||||
}
|
||||
|
||||
// ToERC20Amount converts an XMR amount to a token amount in standard units with
|
||||
// the given exchange rate
|
||||
// the given exchange rate. If the result requires more decimal places than the
|
||||
// token allows, an error is returned.
|
||||
func (r *ExchangeRate) ToERC20Amount(xmrAmount *apd.Decimal, token *ERC20TokenInfo) (*apd.Decimal, error) {
|
||||
erc20Amount := new(apd.Decimal)
|
||||
_, err := decimalCtx.Mul(erc20Amount, r.Decimal(), xmrAmount)
|
||||
_, err := decimalCtx.Mul(erc20Amount, xmrAmount, r.Decimal())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The token, if required, will get rounded to whole token units in
|
||||
// NewERC20TokenAmountFromDecimals.
|
||||
return NewERC20TokenAmountFromDecimals(erc20Amount, token).AsStandard(), nil
|
||||
if ExceedsDecimals(erc20Amount, token.NumDecimals) {
|
||||
// We could have a suggested value to try, like we have in ToXMR(...),
|
||||
// but since this is multiplication and not division, the end user
|
||||
// probably doesn't need the hint.
|
||||
err := fmt.Errorf("%s XMR * %s exceeds token's %d decimal precision",
|
||||
xmrAmount.Text('f'), r, token.NumDecimals)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewTokenAmountFromDecimals(erc20Amount, token).AsStd(), nil
|
||||
}
|
||||
|
||||
func (r *ExchangeRate) String() string {
|
||||
|
||||
@@ -13,41 +13,34 @@ import (
|
||||
|
||||
func TestExchangeRate_ToXMR(t *testing.T) {
|
||||
rate := StrToExchangeRate("0.25") // 4 XMR * 0.25 = 1 ETH
|
||||
ethAmount := StrToDecimal("1")
|
||||
ethAssetAmt := StrToETHAsset("1", nil)
|
||||
const expectedXMRAmount = "4"
|
||||
xmrAmount, err := rate.ToXMR(ethAmount)
|
||||
xmrAmount, err := rate.ToXMR(ethAssetAmt)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedXMRAmount, xmrAmount.String())
|
||||
}
|
||||
|
||||
func TestExchangeRate_ToXMR_roundDown(t *testing.T) {
|
||||
rate := StrToExchangeRate("0.333333")
|
||||
ethAmount := StrToDecimal("3.1")
|
||||
|
||||
func TestExchangeRate_ToXMR_exceedsXMRPrecision(t *testing.T) {
|
||||
// 3.1/0.333333 calculated to 13 decimals is 9.3000093000093 (300009 repeats indefinitely)
|
||||
// This calculator goes to 200 decimals: https://www.mathsisfun.com/calculator-precision.html
|
||||
// XMR rounds at 12 decimal places to:
|
||||
const expectedXMRAmount = "9.300009300009"
|
||||
rate := StrToExchangeRate("0.333333")
|
||||
ethAssetAmt := StrToETHAsset("3.1", nil)
|
||||
|
||||
xmrAmount, err := rate.ToXMR(ethAmount)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedXMRAmount, xmrAmount.String())
|
||||
}
|
||||
_, err := rate.ToXMR(ethAssetAmt)
|
||||
expectedErr := "3.1 ETH / 0.333333 exceeds XMR's 12 decimal precision, try 3.099999999999899997"
|
||||
require.ErrorContains(t, err, expectedErr)
|
||||
|
||||
func TestExchangeRate_ToXMR_roundUp(t *testing.T) {
|
||||
rate := StrToExchangeRate("0.666666")
|
||||
ethAmount := StrToDecimal("6.6")
|
||||
// 6.6/0.666666 to 13 decimal places is 9.9000099000099 (900009 repeats indefinitely)
|
||||
// The 9 in the 12th position goes to zero changing 11th position to 1:
|
||||
const expectedXMRAmount = "9.90000990001" // only 11 decimal places shown as 12th is 0
|
||||
xmrAmount, err := rate.ToXMR(ethAmount)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedXMRAmount, xmrAmount.String())
|
||||
rate = StrToExchangeRate("0.666666")
|
||||
ethAssetAmt = StrToETHAsset("6.6", nil)
|
||||
|
||||
_, err = rate.ToXMR(ethAssetAmt)
|
||||
expectedErr = "6.6 ETH / 0.666666 exceeds XMR's 12 decimal precision, try 6.60000000000006666"
|
||||
require.ErrorContains(t, err, expectedErr)
|
||||
}
|
||||
|
||||
func TestExchangeRate_ToXMR_fail(t *testing.T) {
|
||||
rateZero := ToExchangeRate(new(apd.Decimal)) // zero exchange rate
|
||||
_, err := rateZero.ToXMR(StrToDecimal("0.1"))
|
||||
_, err := rateZero.ToXMR(StrToETHAsset("0.1", nil))
|
||||
require.ErrorContains(t, err, "division by zero")
|
||||
}
|
||||
|
||||
@@ -72,35 +65,15 @@ func TestExchangeRate_ToERC20Amount(t *testing.T) {
|
||||
assert.Equal(t, expectedTokenStandardAmount, erc20Amt.Text('f'))
|
||||
}
|
||||
|
||||
func TestExchangeRate_ToERC20Amount_roundDown(t *testing.T) {
|
||||
// 0.333333 * 1.0000015 = 0.333333499...
|
||||
// = 0.333333 (token only supports 6 decimals)
|
||||
rate := StrToExchangeRate("0.333333")
|
||||
func TestExchangeRate_ToERC20Amount_exceedsTokenPrecision(t *testing.T) {
|
||||
const tokenDecimals = 6
|
||||
token := &ERC20TokenInfo{NumDecimals: tokenDecimals}
|
||||
|
||||
// 1.0000015 * 0.333333 = 0.3333334999995
|
||||
xmrAmount := StrToDecimal("1.0000015")
|
||||
|
||||
const tokenDecimals = 6
|
||||
const expectedTokenStandardAmount = "0.333333"
|
||||
erc20Info := &ERC20TokenInfo{NumDecimals: tokenDecimals}
|
||||
|
||||
erc20Amt, err := rate.ToERC20Amount(xmrAmount, erc20Info)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedTokenStandardAmount, erc20Amt.Text('f'))
|
||||
}
|
||||
|
||||
func TestExchangeRate_ToERC20Amount_roundUp(t *testing.T) {
|
||||
// 0.333333 * 1.000001501 = 0.333333500..
|
||||
// = 0.333334 (token only supports 6 decimals)
|
||||
|
||||
rate := StrToExchangeRate("0.333333")
|
||||
xmrAmount := StrToDecimal("1.000001501")
|
||||
|
||||
const tokenDecimals = 6
|
||||
const expectedTokenStandardAmount = "0.333334"
|
||||
erc20Info := &ERC20TokenInfo{NumDecimals: tokenDecimals}
|
||||
|
||||
erc20Amt, err := rate.ToERC20Amount(xmrAmount, erc20Info)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedTokenStandardAmount, erc20Amt.Text('f'))
|
||||
_, err := rate.ToERC20Amount(xmrAmount, token)
|
||||
require.ErrorContains(t, err, "1.0000015 XMR * 0.333333 exceeds token's 6 decimal precision")
|
||||
}
|
||||
|
||||
func TestExchangeRate_String(t *testing.T) {
|
||||
|
||||
@@ -5,18 +5,107 @@ package coins
|
||||
|
||||
import (
|
||||
"github.com/cockroachdb/apd/v3"
|
||||
"github.com/ethereum/go-ethereum/common/math"
|
||||
)
|
||||
|
||||
func roundToDecimalPlace(result *apd.Decimal, n *apd.Decimal, decimalPlace uint8) error {
|
||||
result.Set(n) // already optimizes result == n
|
||||
func roundToDecimalPlace(n *apd.Decimal, decimalPlace uint8) (*apd.Decimal, error) {
|
||||
result := new(apd.Decimal).Set(n)
|
||||
|
||||
// Adjust the exponent to the rounding place, round, then adjust the exponent back
|
||||
increaseExponent(result, decimalPlace)
|
||||
_, err := decimalCtx.RoundToIntegralValue(result, result)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
decreaseExponent(result, decimalPlace)
|
||||
_, _ = result.Reduce(result)
|
||||
return nil
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ExceedsDecimals returns `true` if the the number, written without an
|
||||
// exponent, would require more digits after the decimal place than the passed
|
||||
// value `decimals`. Otherwise, `false` is returned.
|
||||
func ExceedsDecimals(val *apd.Decimal, maxDecimals uint8) bool {
|
||||
return NumDecimals(val) > int32(maxDecimals)
|
||||
}
|
||||
|
||||
// NumDecimals returns the minimum number digits needed to represent the passed
|
||||
// value after the decimal point.
|
||||
func NumDecimals(value *apd.Decimal) int32 {
|
||||
// Transfer any rightmost digits in the coefficient to the exponent
|
||||
_, _ = value.Reduce(value)
|
||||
|
||||
if value.Exponent >= 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// The number of base-10 digits that we need to shift left by, is the number
|
||||
// of digits needed to represent the value after the decimal point.
|
||||
return -value.Exponent
|
||||
}
|
||||
|
||||
// calcAltNumeratorAmount attempts to find an alternative, close numerator
|
||||
// amount that will not exceed the result's decimal precision when divided by
|
||||
// the denominator and will also not exceed the numerator's precision. If
|
||||
// no close approximation is found, nil is returned.
|
||||
//
|
||||
// Take this example, where the calculation below has already failed due to the
|
||||
// computed XMR amount exceeding XMR's 12 decimal places.
|
||||
//
|
||||
// TOKEN_AMOUNT / EXCHANGE_RATE = XMR_AMOUNT_TOO_PRECISE
|
||||
//
|
||||
// In the above example, we want to calculate the closest possible numerator
|
||||
// (TOKEN_AMOUNT) by rounding the right-hand-side and multiplying by the
|
||||
// denominator (EXCHANGE_RATE).
|
||||
//
|
||||
// ROUND(XMR_AMOUNT_TOO_PRECISE, PRECISION) * EXCHANGE_RATE = SUGGESTED_TOKEN_AMOUNT
|
||||
// or rewritten:
|
||||
// ROUND(unroundedResult, PRECISION) * denominator = SUGGESTED_ALTERNATE_NUMERATOR
|
||||
//
|
||||
// The trick is in computing that maximum value for PRECISION that will yield a
|
||||
// valid result. Rounding too early increases the chance that we will compute a
|
||||
// value that is outside of the offer's allowed range (which we don't have
|
||||
// access to at this point in the code).
|
||||
func calcAltNumeratorAmount(
|
||||
numeratorDecimals uint8,
|
||||
resultDecimals uint8,
|
||||
denominator *apd.Decimal,
|
||||
unroundedResult *apd.Decimal,
|
||||
) *apd.Decimal {
|
||||
// Start with the max precision that the numerator will allow. Note that the
|
||||
// denominator's exponent can be positive (300 is 3E2) or negative (0.003 is
|
||||
// 3E-3). When it is positive, we can round the result at a larger precision
|
||||
// than the numerator's precision and still get a result that fits in the
|
||||
// numerator's precision.
|
||||
_, _ = denominator.Reduce(denominator)
|
||||
roundingPrecision := denominator.Exponent + int32(numeratorDecimals)
|
||||
if roundingPrecision < 0 || roundingPrecision > math.MaxUint8 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Pick the smaller precision value (precision of result or the precision
|
||||
// needed to calculate a valid numerator).
|
||||
if roundingPrecision > int32(resultDecimals) {
|
||||
roundingPrecision = int32(resultDecimals)
|
||||
}
|
||||
|
||||
roundedResult, err := roundToDecimalPlace(unroundedResult, uint8(roundingPrecision))
|
||||
if err != nil {
|
||||
return nil // not reachable
|
||||
}
|
||||
|
||||
closestAltResult := new(apd.Decimal)
|
||||
_, err = decimalCtx.Mul(closestAltResult, roundedResult, denominator)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
_, _ = closestAltResult.Reduce(closestAltResult)
|
||||
|
||||
if closestAltResult.IsZero() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return closestAltResult
|
||||
}
|
||||
|
||||
@@ -14,21 +14,147 @@ import (
|
||||
func Test_roundToDecimalPlace(t *testing.T) {
|
||||
// Round half down
|
||||
amt := StrToDecimal("33.4999999999999999999999999999999999")
|
||||
err := roundToDecimalPlace(amt, amt, 0)
|
||||
amt, err := roundToDecimalPlace(amt, 0)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "33", amt.String())
|
||||
|
||||
// Round half up
|
||||
amt = StrToDecimal("33.5")
|
||||
err = roundToDecimalPlace(amt, amt, 0)
|
||||
amt, err = roundToDecimalPlace(amt, 0)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "34", amt.String())
|
||||
|
||||
// Round at Decimal position
|
||||
amt = StrToDecimal("0.00009")
|
||||
res := new(apd.Decimal) // use a separate result variable this time
|
||||
err = roundToDecimalPlace(res, amt, 4)
|
||||
res, err := roundToDecimalPlace(amt, 4) // use a separate result variable this time
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "0.0001", res.String())
|
||||
assert.Equal(t, "0.00009", amt.String()) // input value unchanged
|
||||
}
|
||||
|
||||
func Test_exceedsMaxDigitsAfterDecimal(t *testing.T) {
|
||||
type testCase struct {
|
||||
val *apd.Decimal
|
||||
decimals uint8
|
||||
exceeds bool
|
||||
}
|
||||
|
||||
testCases := []testCase{
|
||||
{
|
||||
val: StrToDecimal("1234567890"),
|
||||
decimals: 0,
|
||||
exceeds: false,
|
||||
},
|
||||
{
|
||||
val: StrToDecimal("0.0000000000001"), // 13 decimal places
|
||||
decimals: 12,
|
||||
exceeds: true,
|
||||
},
|
||||
{
|
||||
val: StrToDecimal("123456789.999999"), // 6 decimal places
|
||||
decimals: 6,
|
||||
exceeds: false,
|
||||
},
|
||||
{
|
||||
val: StrToDecimal("123456789.999999"), // 6 decimal places
|
||||
decimals: 5,
|
||||
exceeds: true,
|
||||
},
|
||||
{
|
||||
val: StrToDecimal("123.123400000000000000"), // only 4 non-zero decimal places
|
||||
decimals: 4,
|
||||
exceeds: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
r := ExceedsDecimals(test.val, test.decimals)
|
||||
require.Equal(t, test.exceeds, r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExchangeRate_calcAltNumeratorAmount(t *testing.T) {
|
||||
type testCase struct {
|
||||
numeratorDecimals uint8
|
||||
resultDecimals uint8
|
||||
numerator *apd.Decimal
|
||||
denominator *apd.Decimal
|
||||
expectedAltValue *apd.Decimal
|
||||
}
|
||||
|
||||
testCases := []*testCase{
|
||||
{
|
||||
//
|
||||
// 30/20.5 = 1.46341[46341...] (repeats forever, so won't fit in 12 decimals)
|
||||
//
|
||||
// In order to back calculate the closest numerator that will work
|
||||
// (fit within 6 decimals), we need to round the result at 5
|
||||
// decimals (6 + -1), where the -1 was contributed by the
|
||||
// denominator.
|
||||
//
|
||||
// 1.46341 * 20.5 = 29.999905
|
||||
//
|
||||
numeratorDecimals: 6,
|
||||
resultDecimals: 12,
|
||||
numerator: StrToDecimal("30"),
|
||||
denominator: StrToDecimal("20.5"),
|
||||
expectedAltValue: StrToDecimal("29.999905"),
|
||||
},
|
||||
{
|
||||
//
|
||||
// Same as the previous example, but now the precision of the
|
||||
// numerator is 18 decimals, so we can round at the full precision
|
||||
// (12) of the result.
|
||||
//
|
||||
// 1.463414634146 * 20.5 = 29.999999999993
|
||||
numeratorDecimals: 18,
|
||||
resultDecimals: 12,
|
||||
numerator: StrToDecimal("30"),
|
||||
denominator: StrToDecimal("20.5"),
|
||||
expectedAltValue: StrToDecimal("29.999999999993"),
|
||||
},
|
||||
{
|
||||
//
|
||||
// 200/300 = 0.666666666666... (repeats 6 forever)
|
||||
//
|
||||
// This example is interesting, because the denominator, when reduced,
|
||||
// is 3E2, allowing us to round the divided result at 8 decimal (2
|
||||
// more decimal places than the precision of the numerator).
|
||||
//
|
||||
// 0.666667 * 300 = 200.0001 (naive answer, rounding at the numerator's precision)
|
||||
// 0.66666667 * 300 = 200.000001 (true closest value)
|
||||
//
|
||||
numeratorDecimals: 6,
|
||||
resultDecimals: 12,
|
||||
numerator: StrToDecimal("200"),
|
||||
denominator: StrToDecimal("300"),
|
||||
expectedAltValue: StrToDecimal("200.000001"),
|
||||
},
|
||||
{
|
||||
//
|
||||
// This is a case were we just return nil. The denominator has 6
|
||||
// decimal places (what we cap exchange rates at), but the numerator
|
||||
// is capped at 4 decimals. We can't just multiply any rounded
|
||||
// result by the denominator and get an alternate numerator that
|
||||
// fits in 4 decimals.
|
||||
//
|
||||
numeratorDecimals: 4,
|
||||
resultDecimals: 12,
|
||||
numerator: StrToDecimal("0.1"),
|
||||
denominator: StrToDecimal("0.333333"),
|
||||
expectedAltValue: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
result := new(apd.Decimal)
|
||||
_, err := decimalCtx.Quo(result, tc.numerator, tc.denominator)
|
||||
require.NoError(t, err)
|
||||
altVal := calcAltNumeratorAmount(tc.numeratorDecimals, tc.resultDecimals, tc.denominator, result)
|
||||
if tc.expectedAltValue == nil {
|
||||
require.Nil(t, altVal)
|
||||
} else {
|
||||
require.Equal(t, tc.expectedAltValue.Text('f'), altVal.Text('f'))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,12 @@ func StrToDecimal(amount string) *apd.Decimal {
|
||||
return a
|
||||
}
|
||||
|
||||
// StrToETHAsset converts a string into the EthAssetAmount. Pass nil for the
|
||||
// token if the asset is ETH.
|
||||
func StrToETHAsset(amount string, optionalToken *ERC20TokenInfo) EthAssetAmount {
|
||||
return NewEthAssetAmount(StrToDecimal(amount), nil)
|
||||
}
|
||||
|
||||
// StrToExchangeRate converts strings to ExchangeRate for tests, panicking on error.
|
||||
func StrToExchangeRate(rate string) *ExchangeRate {
|
||||
r := new(ExchangeRate)
|
||||
|
||||
@@ -39,5 +39,8 @@ type Error struct {
|
||||
|
||||
// Error ...
|
||||
func (e *Error) Error() string {
|
||||
return fmt.Sprintf("message=%s; code=%d; data=%v", e.Message, e.ErrorCode, e.Data)
|
||||
if e.ErrorCode != 0 {
|
||||
return fmt.Sprintf("message=%s; code=%d; data=%v", e.Message, e.ErrorCode, e.Data)
|
||||
}
|
||||
return e.Message
|
||||
}
|
||||
|
||||
@@ -152,14 +152,18 @@ func (o *Offer) validate() error {
|
||||
// can't be used to complete the swap if the maker does not have sufficient
|
||||
// ETH to make the claim themselves. While relayers are not used with ERC20
|
||||
// swaps, we still want a minimum swap amount, so we use the same value.
|
||||
relayerFeeAsXMR, err := o.ExchangeRate.ToXMR(coins.RelayerFeeETH)
|
||||
minAmtAsETH, err := o.ExchangeRate.ToETH(o.MinAmount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if o.MinAmount.Cmp(relayerFeeAsXMR) <= 0 {
|
||||
if minAmtAsETH.Cmp(coins.RelayerFeeETH) <= 0 {
|
||||
return fmt.Errorf(
|
||||
"min amount must be greater than %s ETH when converted (%s XMR)",
|
||||
coins.RelayerFeeETH.Text('f'), relayerFeeAsXMR.Text('f'))
|
||||
"min amount must be greater than %s ETH when converted (%s XMR * %s = %s ETH)",
|
||||
coins.RelayerFeeETH.Text('f'),
|
||||
o.MaxAmount.Text('f'),
|
||||
o.ExchangeRate,
|
||||
minAmtAsETH.Text('f'),
|
||||
)
|
||||
}
|
||||
|
||||
if o.MaxAmount.Cmp(maxOfferValue) > 0 {
|
||||
|
||||
@@ -179,7 +179,7 @@ func TestOffer_UnmarshalJSON_BadAmountsOrRate(t *testing.T) {
|
||||
{
|
||||
// 0.01 relayer fee is 0.1 XMR with exchange rate of 0.1
|
||||
jsonData: fmt.Sprintf(offerJSON, `"0.01"`, `"10"`, `"0.1"`),
|
||||
errContains: `min amount must be greater than 0.01 ETH when converted (0.1 XMR)`,
|
||||
errContains: `min amount must be greater than 0.01 ETH when converted (10 XMR * 0.1 = 0.001 ETH)`,
|
||||
},
|
||||
// Max Amount checks
|
||||
{
|
||||
|
||||
58
daemon/bad_make_take_values_test.go
Normal file
58
daemon/bad_make_take_values_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/athanorlabs/atomic-swap/coins"
|
||||
"github.com/athanorlabs/atomic-swap/monero"
|
||||
"github.com/athanorlabs/atomic-swap/rpcclient"
|
||||
"github.com/athanorlabs/atomic-swap/tests"
|
||||
)
|
||||
|
||||
// Tests end-to-end (client->swapd->client) make/take failures with currency
|
||||
// precision issues.
|
||||
func TestBadMakeTakeValues(t *testing.T) {
|
||||
bobConf := CreateTestConf(t, tests.GetMakerTestKey(t))
|
||||
monero.MineMinXMRBalance(t, bobConf.MoneroClient, coins.MoneroToPiconero(coins.StrToDecimal("10")))
|
||||
|
||||
aliceConf := CreateTestConf(t, tests.GetTakerTestKey(t))
|
||||
|
||||
timeout := 3 * time.Minute
|
||||
ctx, _ := LaunchDaemons(t, timeout, bobConf, aliceConf)
|
||||
|
||||
bc := rpcclient.NewClient(ctx, bobConf.RPCPort)
|
||||
ac := rpcclient.NewClient(ctx, aliceConf.RPCPort)
|
||||
|
||||
// Trigger a TakeOffer failure because the precision of the min/or max
|
||||
// value combined with the exchange rate would exceed the token's precision.
|
||||
// 14.979329 * 13.3 = 199.2250757 (7 digits of precision)
|
||||
minMaxXMRAmt := coins.StrToDecimal("14.979329")
|
||||
exRate := coins.StrToExchangeRate("13.3")
|
||||
mockTether := getMockTetherAsset(t, aliceConf.EthereumClient)
|
||||
expectedErr := `"net_makeOffer" failed: 14.979329 XMR * 13.3 exceeds token's 6 decimal precision`
|
||||
_, err := bc.MakeOffer(minMaxXMRAmt, minMaxXMRAmt, exRate, mockTether, false)
|
||||
require.ErrorContains(t, err, expectedErr)
|
||||
t.Log(err)
|
||||
|
||||
// Now configure the MakeOffer to succeed, so we can fail some TakeOffer calls
|
||||
minXMRAmt := coins.StrToDecimal("1")
|
||||
maxXMRAmt := coins.StrToDecimal("10")
|
||||
providesAmt := coins.StrToDecimal("5.1234567") // 7 digits, max is 6
|
||||
makeResp, err := bc.MakeOffer(minXMRAmt, maxXMRAmt, exRate, mockTether, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Fail because providesAmount has too much precision in the token's standard units
|
||||
err = ac.TakeOffer(makeResp.PeerID, makeResp.OfferID, providesAmt)
|
||||
require.ErrorContains(t, err, `"net_takeOffer" failed: "providesAmount" has too many decimal points; found=7 max=6`)
|
||||
|
||||
// Fail because the providesAmount has too much precision when converted into XMR
|
||||
// 20.123456/13.3 = 1.51304[180451127819548872] (bracketed sequence repeats forever)
|
||||
providesAmt = coins.StrToDecimal("20.123456")
|
||||
err = ac.TakeOffer(makeResp.PeerID, makeResp.OfferID, providesAmt)
|
||||
expectedErr = `"net_takeOffer" failed: 20.123456 "USDT" / 13.3 exceeds XMR's 12 decimal precision, try 20.123432`
|
||||
require.ErrorContains(t, err, expectedErr)
|
||||
t.Log(err)
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
// so she refunds the swap.
|
||||
// Bob should have aborted the swap in all cases.
|
||||
func TestXMRTakerCancelOrRefundAfterKeyExchange(t *testing.T) {
|
||||
t.Skip("Test disabled until https://github.com/AthanorLabs/atomic-swap/issues/479 is fixed")
|
||||
minXMR := coins.StrToDecimal("1")
|
||||
maxXMR := minXMR
|
||||
exRate := coins.StrToExchangeRate("300")
|
||||
|
||||
@@ -43,8 +43,7 @@ func TestAliceDoubleRestartAfterXMRLock(t *testing.T) {
|
||||
bc := rpcclient.NewClient(context.Background(), bobConf.RPCPort)
|
||||
ac := rpcclient.NewClient(context.Background(), aliceConf.RPCPort)
|
||||
|
||||
tokenAddr := GetMockTokens(t, aliceConf.EthereumClient)[MockTether]
|
||||
tokenAsset := types.EthAsset(tokenAddr)
|
||||
tokenAsset := getMockTetherAsset(t, aliceConf.EthereumClient)
|
||||
|
||||
makeResp, bobStatusCh, err := bc.MakeOfferAndSubscribe(minXMR, maxXMR, exRate, tokenAsset, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cockroachdb/apd/v3"
|
||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -29,8 +30,7 @@ func TestRunSwapDaemon_ExchangesXMRForERC20Tokens(t *testing.T) {
|
||||
|
||||
aliceConf := CreateTestConf(t, tests.GetTakerTestKey(t))
|
||||
|
||||
tokenAddr := GetMockTokens(t, aliceConf.EthereumClient)[MockTether]
|
||||
tokenAsset := types.EthAsset(tokenAddr)
|
||||
tokenAsset := getMockTetherAsset(t, aliceConf.EthereumClient)
|
||||
|
||||
timeout := 7 * time.Minute
|
||||
ctx, _ := LaunchDaemons(t, timeout, aliceConf, bobConf)
|
||||
@@ -38,6 +38,9 @@ func TestRunSwapDaemon_ExchangesXMRForERC20Tokens(t *testing.T) {
|
||||
bc := rpcclient.NewClient(ctx, bobConf.RPCPort)
|
||||
ac := rpcclient.NewClient(ctx, aliceConf.RPCPort)
|
||||
|
||||
bobStartTokenBal, err := bobConf.EthereumClient.ERC20Balance(ctx, tokenAsset.Address())
|
||||
require.NoError(t, err)
|
||||
|
||||
_, bobStatusCh, err := bc.MakeOfferAndSubscribe(minXMR, maxXMR, exRate, tokenAsset, false)
|
||||
require.NoError(t, err)
|
||||
time.Sleep(250 * time.Millisecond) // offer propagation time
|
||||
@@ -104,10 +107,12 @@ func TestRunSwapDaemon_ExchangesXMRForERC20Tokens(t *testing.T) {
|
||||
//
|
||||
// Check Bob's token balance via RPC method instead of doing it directly
|
||||
//
|
||||
balances, err := bc.Balances(&rpctypes.BalancesRequest{TokenAddrs: []ethcommon.Address{tokenAddr}})
|
||||
endBalances, err := bc.Balances(&rpctypes.BalancesRequest{TokenAddrs: []ethcommon.Address{tokenAsset.Address()}})
|
||||
require.NoError(t, err)
|
||||
t.Logf("Balances: %#v", balances)
|
||||
require.NotEmpty(t, endBalances.TokenBalances)
|
||||
|
||||
require.NotEmpty(t, balances.TokenBalances)
|
||||
require.Equal(t, providesAmt.Text('f'), balances.TokenBalances[0].AsStandardString())
|
||||
delta := new(apd.Decimal)
|
||||
_, err = coins.DecimalCtx().Sub(delta, endBalances.TokenBalances[0].Amount, bobStartTokenBal.Amount)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, providesAmt.Text('f'), delta.Text('f'))
|
||||
}
|
||||
|
||||
@@ -40,8 +40,7 @@ func TestAliceStoppedAndRestartedDuringXMRSweep(t *testing.T) {
|
||||
bc := rpcclient.NewClient(context.Background(), bobConf.RPCPort)
|
||||
ac := rpcclient.NewClient(context.Background(), aliceConf.RPCPort)
|
||||
|
||||
tokenAddr := GetMockTokens(t, aliceConf.EthereumClient)[MockTether]
|
||||
tokenAsset := types.EthAsset(tokenAddr)
|
||||
tokenAsset := getMockTetherAsset(t, aliceConf.EthereumClient)
|
||||
|
||||
makeResp, bobStatusCh, err := bc.MakeOfferAndSubscribe(minXMR, maxXMR, exRate, tokenAsset, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net"
|
||||
"path"
|
||||
"sync"
|
||||
@@ -17,11 +16,11 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/athanorlabs/atomic-swap/bootnode"
|
||||
"github.com/athanorlabs/atomic-swap/common"
|
||||
"github.com/athanorlabs/atomic-swap/common/types"
|
||||
contracts "github.com/athanorlabs/atomic-swap/ethereum"
|
||||
"github.com/athanorlabs/atomic-swap/ethereum/extethclient"
|
||||
"github.com/athanorlabs/atomic-swap/monero"
|
||||
@@ -29,12 +28,6 @@ import (
|
||||
"github.com/athanorlabs/atomic-swap/tests"
|
||||
)
|
||||
|
||||
// map indexes for our mock tokens
|
||||
const (
|
||||
MockDAI = "DAI"
|
||||
MockTether = "USDT"
|
||||
)
|
||||
|
||||
// This file is only for test support. Use the build tag "prod" to prevent
|
||||
// symbols in this file from consuming space in production binaries.
|
||||
|
||||
@@ -183,57 +176,7 @@ func WaitForSwapdStart(t *testing.T, rpcPort uint16) {
|
||||
t.Fatalf("giving up, swapd RPC port %d is not listening after %d seconds", rpcPort, maxSeconds)
|
||||
}
|
||||
|
||||
// these variables are only for use by GetMockTokens
|
||||
var _mockTokens map[string]ethcommon.Address
|
||||
var _mockTokensMu sync.Mutex
|
||||
|
||||
// GetMockTokens returns a symbol=>address map of our mock ERC20 tokens,
|
||||
// deploying them if they haven't already been deployed. Use the constants
|
||||
// defined earlier to access the map elements.
|
||||
func GetMockTokens(t *testing.T, ec extethclient.EthClient) map[string]ethcommon.Address {
|
||||
_mockTokensMu.Lock()
|
||||
defer _mockTokensMu.Unlock()
|
||||
|
||||
if _mockTokens == nil {
|
||||
_mockTokens = make(map[string]ethcommon.Address)
|
||||
}
|
||||
|
||||
calcSupply := func(numStdUnits int64, decimals int64) *big.Int {
|
||||
return new(big.Int).Mul(big.NewInt(numStdUnits), new(big.Int).Exp(big.NewInt(10), big.NewInt(decimals), nil))
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
txOpts, err := ec.TxOpts(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
mockDaiAddr, mockDaiTx, _, err := contracts.DeployTestERC20(
|
||||
txOpts,
|
||||
ec.Raw(),
|
||||
"Dai Stablecoin",
|
||||
"DAI",
|
||||
18,
|
||||
ec.Address(),
|
||||
calcSupply(1000, 18),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
tests.MineTransaction(t, ec.Raw(), mockDaiTx)
|
||||
_mockTokens[MockDAI] = mockDaiAddr
|
||||
|
||||
txOpts, err = ec.TxOpts(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
mockTetherAddr, mockTetherTx, _, err := contracts.DeployTestERC20(
|
||||
txOpts,
|
||||
ec.Raw(),
|
||||
"Tether USD",
|
||||
"USDT",
|
||||
6,
|
||||
ec.Address(),
|
||||
calcSupply(1000, 6),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
tests.MineTransaction(t, ec.Raw(), mockTetherTx)
|
||||
_mockTokens[MockTether] = mockTetherAddr
|
||||
|
||||
return _mockTokens
|
||||
func getMockTetherAsset(t *testing.T, ec extethclient.EthClient) types.EthAsset {
|
||||
token := contracts.GetMockTether(t, ec.Raw(), ec.PrivateKey())
|
||||
return types.EthAsset(token.Address)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# Joining the Stagenet/Sepolia network
|
||||
|
||||
Currently, an initial version of the swap is deployed onto the Sepolia (Ethereum testnet) and Monero Stagenet networks. To join the network and try out the swap, either as a maker or a taker, please see the following.
|
||||
Swaps can be performed on the Sepolia (Ethereum testnet) together with the
|
||||
Monero Stagenet network. This document describes how to do stagenet swaps.
|
||||
|
||||
> Note: a swap on stagenet currently takes around 10-20 minutes due to block times.
|
||||
> Note: stagenet and mainnet swaps currently take around 20 minutes due to the 2-minute block time and required 10 confirmations before funds can be spent.
|
||||
|
||||
> Note: the `swapd` process directly interacts with an unlocked Monero wallet and Ethereum private key. This is to allow for a smoother swap process that doesn't require any interaction from you once initiated. However, this effectively gives `swapd` access to all your (testnet) funds. In the future, there will be a mode that does not access your keys/wallet, but will require user interaction during a swap.
|
||||
|
||||
@@ -38,11 +39,19 @@ For Linux 64-bit, you can do:
|
||||
named `{DATA_DIR}/eth.key`. If you skip this step, a new wallet will be created for you
|
||||
that you can transfer Sepolia ether to or fund directly in the next step.
|
||||
|
||||
6. Fund your Sepolia account using a faucet:
|
||||
6a. Fund your Sepolia account using a faucet:
|
||||
- https://sepolia-faucet.pk910.de/
|
||||
- https://sepoliafaucet.com/
|
||||
- https://sepolia.dev/
|
||||
|
||||
6b. Optional: Obtain some Sepolia ERC20 tokens
|
||||
|
||||
This [unaffiliated project](https://github.com/bokkypoobah/WeenusTokenFaucet/blob/master/README.md)
|
||||
has deployed some Sepolia tokens of different decimal sizes that can be useful
|
||||
for testing. You can use MetaMask to send the contract address zero Sepolia ETH
|
||||
and the contract will grant you 1000 of its ERC20 tokens. You will pay gas fees
|
||||
that you should validate are sane before sending.
|
||||
|
||||
7. Obtain a Sepolia JSON-RPC endpoint. If you don't want to sync your own node, you can find public ones here: https://sepolia.dev/
|
||||
|
||||
8. Install go 1.20+. See [build instructions](./build.md) for more details.
|
||||
|
||||
@@ -8,13 +8,17 @@ package contracts
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"math/big"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/athanorlabs/atomic-swap/coins"
|
||||
"github.com/athanorlabs/atomic-swap/common"
|
||||
"github.com/athanorlabs/atomic-swap/ethereum/block"
|
||||
)
|
||||
|
||||
@@ -54,3 +58,128 @@ func DevDeploySwapCreator(t *testing.T, ec *ethclient.Client, pk *ecdsa.PrivateK
|
||||
|
||||
return *_swapCreatorAddr, _swapCreator
|
||||
}
|
||||
|
||||
// variables should only be accessed by GetMockTether
|
||||
var _mockTether *coins.ERC20TokenInfo
|
||||
var _mockTetherMu sync.Mutex
|
||||
|
||||
// GetMockTether returns the ERC20TokenInfo of a dev token configured with
|
||||
// similar parameters to Tether.
|
||||
func GetMockTether(t *testing.T, ec *ethclient.Client, pk *ecdsa.PrivateKey) *coins.ERC20TokenInfo {
|
||||
const (
|
||||
name = "Tether USD"
|
||||
symbol = "USDT"
|
||||
numDecimals = 6
|
||||
)
|
||||
|
||||
_mockTetherMu.Lock()
|
||||
defer _mockTetherMu.Unlock()
|
||||
|
||||
if _mockTether != nil {
|
||||
mintTokens(t, ec, pk, _mockTether)
|
||||
return _mockTether
|
||||
}
|
||||
|
||||
ownerAddress := common.EthereumPrivateKeyToAddress(pk)
|
||||
|
||||
ctx := context.Background()
|
||||
txOpts, err := newTXOpts(ctx, ec, pk)
|
||||
require.NoError(t, err)
|
||||
|
||||
supply := calcTokenUnits(1000, numDecimals)
|
||||
addr, tx, _, err := DeployTestERC20(txOpts, ec, name, symbol, numDecimals, ownerAddress, supply)
|
||||
require.NoError(t, err)
|
||||
_, err = block.WaitForReceipt(context.Background(), ec, tx.Hash())
|
||||
require.NoError(t, err)
|
||||
|
||||
_mockTether = &coins.ERC20TokenInfo{
|
||||
Address: addr,
|
||||
NumDecimals: numDecimals,
|
||||
Name: name,
|
||||
Symbol: symbol,
|
||||
}
|
||||
|
||||
return _mockTether
|
||||
}
|
||||
|
||||
// variables should only be accessed by GetMockDAI
|
||||
var _mockDAI *coins.ERC20TokenInfo
|
||||
var _mockDAIMu sync.Mutex
|
||||
|
||||
// GetMockDAI returns the ERC20TokenInfo of a dev token configured with
|
||||
// similar parameters to the DAI stablecoin.
|
||||
func GetMockDAI(t *testing.T, ec *ethclient.Client, pk *ecdsa.PrivateKey) *coins.ERC20TokenInfo {
|
||||
const (
|
||||
name = "Dai Stablecoin"
|
||||
symbol = "DAI"
|
||||
numDecimals = 18
|
||||
)
|
||||
|
||||
_mockDAIMu.Lock()
|
||||
defer _mockDAIMu.Unlock()
|
||||
|
||||
if _mockDAI != nil {
|
||||
mintTokens(t, ec, pk, _mockDAI)
|
||||
return _mockDAI
|
||||
}
|
||||
|
||||
ownerAddress := common.EthereumPrivateKeyToAddress(pk)
|
||||
|
||||
ctx := context.Background()
|
||||
txOpts, err := newTXOpts(ctx, ec, pk)
|
||||
require.NoError(t, err)
|
||||
|
||||
supply := calcTokenUnits(1000, numDecimals)
|
||||
addr, tx, _, err := DeployTestERC20(txOpts, ec, name, symbol, numDecimals, ownerAddress, supply)
|
||||
require.NoError(t, err)
|
||||
_, err = block.WaitForReceipt(context.Background(), ec, tx.Hash())
|
||||
require.NoError(t, err)
|
||||
|
||||
_mockDAI = &coins.ERC20TokenInfo{
|
||||
Address: addr,
|
||||
NumDecimals: numDecimals,
|
||||
Name: name,
|
||||
Symbol: symbol,
|
||||
}
|
||||
|
||||
return _mockDAI
|
||||
}
|
||||
|
||||
// calcTokenUnits converts the token's standard units into its internal,
|
||||
// smallest non-divisible units.
|
||||
func calcTokenUnits(numStdUnits int64, decimals uint8) *big.Int {
|
||||
powerOf10 := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(decimals)), nil)
|
||||
return new(big.Int).Mul(big.NewInt(numStdUnits), powerOf10)
|
||||
}
|
||||
|
||||
// mintTokens ensures that the account associated with `pk` has at least 1000 tokens
|
||||
func mintTokens(t *testing.T, ec *ethclient.Client, pk *ecdsa.PrivateKey, token *coins.ERC20TokenInfo) {
|
||||
ctx := context.Background()
|
||||
bindOpts := &bind.CallOpts{Context: ctx}
|
||||
|
||||
tokenContract, err := NewTestERC20(token.Address, ec)
|
||||
require.NoError(t, err)
|
||||
|
||||
decimals, err := tokenContract.Decimals(bindOpts)
|
||||
require.NoError(t, err)
|
||||
|
||||
symbol, err := tokenContract.Symbol(bindOpts)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, token.Symbol, symbol)
|
||||
|
||||
ownerAddress := common.EthereumPrivateKeyToAddress(pk)
|
||||
|
||||
desiredAmt := calcTokenUnits(1000, decimals)
|
||||
currentAmt, err := tokenContract.BalanceOf(bindOpts, ownerAddress)
|
||||
require.NoError(t, err)
|
||||
|
||||
if currentAmt.Cmp(desiredAmt) < 0 {
|
||||
txOpts, err := newTXOpts(context.Background(), ec, pk)
|
||||
require.NoError(t, err)
|
||||
mintAmt := new(big.Int).Sub(desiredAmt, currentAmt)
|
||||
tx, err := tokenContract.Mint(txOpts, ownerAddress, mintAmt)
|
||||
require.NoError(t, err)
|
||||
_, err = block.WaitForReceipt(ctx, ec, tx.Hash())
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ func (s *ExternalSender) NewSwap(
|
||||
tx := &Transaction{
|
||||
To: s.contractAddr,
|
||||
Data: input,
|
||||
Value: amount.AsStandard(),
|
||||
Value: amount.AsStd(),
|
||||
}
|
||||
|
||||
s.Lock()
|
||||
|
||||
@@ -110,7 +110,7 @@ func (s *privateKeySender) NewSwap(
|
||||
|
||||
log.Debugf("approve transaction included %s", common.ReceiptInfo(receipt))
|
||||
log.Infof("%s %s approved for use by SwapCreator's new_swap",
|
||||
amount.AsStandard().Text('f'), amount.StandardSymbol())
|
||||
amount.AsStdString(), amount.StdSymbol())
|
||||
}
|
||||
|
||||
txOpts, err := s.ethClient.TxOpts(s.ctx)
|
||||
|
||||
@@ -23,8 +23,33 @@ func (inst *Instance) MakeOffer(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if useRelayer && o.EthAsset.IsToken() {
|
||||
return nil, errRelayingWithNonEthAsset
|
||||
if o.EthAsset.IsToken() {
|
||||
if useRelayer {
|
||||
return nil, errRelayingWithNonEthAsset
|
||||
}
|
||||
|
||||
token, err := inst.backend.ETHClient().ERC20Info(inst.backend.Ctx(), o.EthAsset.Address()) //nolint:govet
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We limit exchange rates to 6 decimals and the min/max XMR amounts to
|
||||
// 12 decimals when marshalling the offer. This means we can never
|
||||
// exceed ETH's 18 decimals when multiplying min/max values by the
|
||||
// exchange rate. Tokens can have far fewer decimals though, so we need
|
||||
// additional checks. Calculating the exchange rate will give a good
|
||||
// error message if the combined precision of the exchange rate and
|
||||
// min/max values would exceed the token's precision.
|
||||
_, err = o.ExchangeRate.ToERC20Amount(o.MinAmount, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = o.ExchangeRate.ToERC20Amount(o.MaxAmount, token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extra, err := inst.offerManager.AddOffer(o, useRelayer)
|
||||
|
||||
@@ -14,12 +14,11 @@ import (
|
||||
"github.com/athanorlabs/atomic-swap/common"
|
||||
"github.com/athanorlabs/atomic-swap/common/types"
|
||||
contracts "github.com/athanorlabs/atomic-swap/ethereum"
|
||||
pcommon "github.com/athanorlabs/atomic-swap/protocol"
|
||||
)
|
||||
|
||||
// checkContract checks the contract's balance and Claim/Refund keys.
|
||||
// if the balance doesn't match what we're expecting to receive, or the public keys in the contract
|
||||
// aren't what we expect, we error and abort the swap.
|
||||
// checkContract checks the contract's balance and Claim/Refund keys. If the
|
||||
// balance doesn't match what we're expecting to receive, or the public keys in
|
||||
// the contract aren't what we expect, we error and abort the swap.
|
||||
func (s *swapState) checkContract(txHash ethcommon.Hash) error {
|
||||
tx, _, err := s.ETHClient().Raw().TransactionByHash(s.ctx, txHash)
|
||||
if err != nil {
|
||||
@@ -82,7 +81,7 @@ func (s *swapState) checkContract(txHash ethcommon.Hash) error {
|
||||
return fmt.Errorf("swap value and event value don't match: got %v, expected %v", event.Value, s.contractSwap.Value)
|
||||
}
|
||||
|
||||
expectedAmount, err := pcommon.GetEthAssetAmount(
|
||||
expectedAmount, err := getEthAssetAmount(
|
||||
s.ctx,
|
||||
s.ETHClient(),
|
||||
s.info.ExpectedAmount,
|
||||
|
||||
@@ -38,7 +38,7 @@ func (s *swapState) claimFunds() (*ethtypes.Receipt, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Infof("balance before claim: %s %s", balance.AsStandardString(), balance.StandardSymbol())
|
||||
log.Infof("balance before claim: %s %s", balance.AsStdString(), balance.StdSymbol())
|
||||
}
|
||||
|
||||
hasBalanceToClaim, err := checkForMinClaimBalance(s.ctx, s.ETHClient())
|
||||
@@ -91,7 +91,7 @@ func (s *swapState) claimFunds() (*ethtypes.Receipt, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Infof("balance after claim: %s %s", balance.AsStandardString(), balance.StandardSymbol())
|
||||
log.Infof("balance after claim: %s %s", balance.AsStdString(), balance.StdSymbol())
|
||||
}
|
||||
|
||||
return receipt, nil
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2023 The AthanorLabs/atomic-swap Authors
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
|
||||
package protocol
|
||||
package xmrmaker
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -14,21 +14,29 @@ import (
|
||||
"github.com/athanorlabs/atomic-swap/ethereum/extethclient"
|
||||
)
|
||||
|
||||
// GetEthAssetAmount converts the passed asset amt (in standard units) to
|
||||
// getEthAssetAmount converts the passed asset amt (in standard units) to
|
||||
// EthAssetAmount (ie WeiAmount or ERC20TokenAmount)
|
||||
func GetEthAssetAmount(
|
||||
func getEthAssetAmount(
|
||||
ctx context.Context,
|
||||
ec extethclient.EthClient,
|
||||
amt *apd.Decimal, // in standard units
|
||||
asset types.EthAsset,
|
||||
) (coins.EthAssetAmount, error) {
|
||||
if asset != types.EthAssetETH {
|
||||
tokenInfo, err := ec.ERC20Info(ctx, asset.Address())
|
||||
if asset.IsToken() {
|
||||
token, err := ec.ERC20Info(ctx, asset.Address())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get ERC20 info: %w", err)
|
||||
}
|
||||
|
||||
return coins.NewERC20TokenAmountFromDecimals(amt, tokenInfo), nil
|
||||
if coins.ExceedsDecimals(amt, token.NumDecimals) {
|
||||
return nil, fmt.Errorf("value can not be represented in the token's %d decimals", token.NumDecimals)
|
||||
}
|
||||
|
||||
return coins.NewTokenAmountFromDecimals(amt, token), nil
|
||||
}
|
||||
|
||||
if coins.ExceedsDecimals(amt, coins.NumEtherDecimals) {
|
||||
return nil, fmt.Errorf("value can not be represented in ETH's %d decimals", coins.NumEtherDecimals)
|
||||
}
|
||||
|
||||
return coins.EtherToWei(amt), nil
|
||||
@@ -110,44 +110,48 @@ func (inst *Instance) HandleInitiateMessage(
|
||||
return nil, errOfferIDNotSet
|
||||
}
|
||||
|
||||
// TODO: If this is not ETH, we need quick/easy access to the number
|
||||
// of token decimal places. Should it be in the OfferExtra struct?
|
||||
err := coins.ValidatePositive("providedAmount", coins.NumEtherDecimals, msg.ProvidedAmount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
offer, offerExtra, err := inst.offerManager.GetOffer(msg.OfferID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
providedAmount, err := offer.ExchangeRate.ToXMR(msg.ProvidedAmount)
|
||||
maxDecimals := uint8(coins.NumEtherDecimals)
|
||||
var token *coins.ERC20TokenInfo
|
||||
if offer.EthAsset.IsToken() {
|
||||
token, err = inst.backend.ETHClient().ERC20Info(inst.backend.Ctx(), offer.EthAsset.Address())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
maxDecimals = token.NumDecimals
|
||||
}
|
||||
|
||||
err = coins.ValidatePositive("providedAmount", maxDecimals, msg.ProvidedAmount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if providedAmount.Cmp(offer.MinAmount) < 0 {
|
||||
expectedAmount := coins.NewEthAssetAmount(msg.ProvidedAmount, token)
|
||||
|
||||
// The calculation below will return an error if the provided amount, when
|
||||
// represented in XMR, would require fractional piconeros. This can happen
|
||||
// more easily than one might expect, as ToXMR is doing a division by the
|
||||
// exchange rate. The taker also verifies that their provided amount will
|
||||
// not result in fractional piconeros, so the issue will normally be caught
|
||||
// before the taker ever contacts us.
|
||||
providedAmtAsXMR, err := offer.ExchangeRate.ToXMR(expectedAmount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if providedAmtAsXMR.Cmp(offer.MinAmount) < 0 {
|
||||
return nil, errAmountProvidedTooLow{msg.ProvidedAmount, offer.MinAmount}
|
||||
}
|
||||
|
||||
if providedAmount.Cmp(offer.MaxAmount) > 0 {
|
||||
if providedAmtAsXMR.Cmp(offer.MaxAmount) > 0 {
|
||||
return nil, errAmountProvidedTooHigh{msg.ProvidedAmount, offer.MaxAmount}
|
||||
}
|
||||
|
||||
providedPiconero := coins.MoneroToPiconero(providedAmount)
|
||||
|
||||
// check decimals if ERC20
|
||||
// note: this is our counterparty's provided amount, ie. how much we're receiving
|
||||
expectedAmount, err := pcommon.GetEthAssetAmount(
|
||||
inst.backend.Ctx(),
|
||||
inst.backend.ETHClient(),
|
||||
msg.ProvidedAmount,
|
||||
offer.EthAsset,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
providedPiconero := coins.MoneroToPiconero(providedAmtAsXMR)
|
||||
|
||||
state, err := inst.initiate(takerPeerID, offer, offerExtra, providedPiconero, expectedAmount)
|
||||
if err != nil {
|
||||
|
||||
@@ -138,7 +138,7 @@ func newSwapStateFromStart(
|
||||
offer.ID,
|
||||
coins.ProvidesXMR,
|
||||
providesAmount.AsMonero(),
|
||||
desiredAmount.AsStandard(),
|
||||
desiredAmount.AsStd(),
|
||||
offer.ExchangeRate,
|
||||
offer.EthAsset,
|
||||
stage,
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/cockroachdb/apd/v3"
|
||||
|
||||
"github.com/athanorlabs/atomic-swap/coins"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -50,26 +52,34 @@ func errContractAddrMismatch(addr string) error {
|
||||
}
|
||||
|
||||
type errAmountProvidedTooLow struct {
|
||||
providedAmtETH *apd.Decimal
|
||||
offerMinAmtETH *apd.Decimal
|
||||
providedAmtETH *apd.Decimal
|
||||
providedAmtAsXMR *apd.Decimal
|
||||
offerMinAmtXMR *apd.Decimal
|
||||
exchangeRate *coins.ExchangeRate
|
||||
}
|
||||
|
||||
func (e errAmountProvidedTooLow) Error() string {
|
||||
return fmt.Sprintf("%s ETH provided is under offer minimum of %s ETH",
|
||||
return fmt.Sprintf("provided ETH converted to XMR is under offer min of %s XMR (%s ETH / %s = %s)",
|
||||
e.offerMinAmtXMR.Text('f'),
|
||||
e.providedAmtETH.Text('f'),
|
||||
e.offerMinAmtETH.Text('f'),
|
||||
e.exchangeRate,
|
||||
e.providedAmtAsXMR.Text('f'),
|
||||
)
|
||||
}
|
||||
|
||||
type errAmountProvidedTooHigh struct {
|
||||
providedAmtETH *apd.Decimal
|
||||
offerMaxETH *apd.Decimal
|
||||
providedAmtETH *apd.Decimal
|
||||
providedAmtAsXMR *apd.Decimal
|
||||
offerMaxAmtXMR *apd.Decimal
|
||||
exchangeRate *coins.ExchangeRate
|
||||
}
|
||||
|
||||
func (e errAmountProvidedTooHigh) Error() string {
|
||||
return fmt.Sprintf("%s ETH provided is over offer maximum of %s ETH",
|
||||
return fmt.Sprintf("provided ETH converted to XMR is over offer max of %s XMR (%s ETH / %s = %s XMR)",
|
||||
e.offerMaxAmtXMR.Text('f'),
|
||||
e.providedAmtETH.Text('f'),
|
||||
e.offerMaxETH.Text('f'),
|
||||
e.exchangeRate,
|
||||
e.providedAmtAsXMR.Text('f'),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ func TestSwapState_handleEvent_EventETHClaimed(t *testing.T) {
|
||||
msg = s.SendKeysMessage().(*message.SendKeysMessage)
|
||||
msg.PrivateViewKey = s.privkeys.ViewKey()
|
||||
msg.EthAddress = s.ETHClient().Address()
|
||||
msg.ProvidedAmount = s.providedAmount.AsStandard()
|
||||
msg.ProvidedAmount = s.providedAmount.AsStd()
|
||||
|
||||
err = s.HandleProtocolMessage(msg)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -71,11 +71,11 @@ func validateMinBalForTokenSwap(
|
||||
providesAmt *apd.Decimal, // standard units
|
||||
gasPriceWei *big.Int,
|
||||
) error {
|
||||
if tokenBalance.AsStandard().Cmp(providesAmt) < 0 {
|
||||
if tokenBalance.AsStd().Cmp(providesAmt) < 0 {
|
||||
return errTokenBalanceTooLow{
|
||||
providedAmount: providesAmt,
|
||||
tokenBalance: tokenBalance.AsStandard(),
|
||||
symbol: tokenBalance.StandardSymbol(),
|
||||
tokenBalance: tokenBalance.AsStd(),
|
||||
symbol: tokenBalance.StdSymbol(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ func Test_validateMinBalForTokenSwap(t *testing.T) {
|
||||
Symbol: "TK",
|
||||
}
|
||||
balanceWei := coins.EtherToWei(coins.StrToDecimal("0.5"))
|
||||
tokenBalance := coins.NewERC20TokenAmountFromDecimals(coins.StrToDecimal("10"), tokenInfo)
|
||||
tokenBalance := coins.NewTokenAmountFromDecimals(coins.StrToDecimal("10"), tokenInfo)
|
||||
providesAmt := coins.StrToDecimal("5")
|
||||
gasPriceWei := big.NewInt(35e9) // 35 GWei
|
||||
err := validateMinBalForTokenSwap(balanceWei, tokenBalance, providesAmt, gasPriceWei)
|
||||
@@ -85,7 +85,7 @@ func Test_validateMinBalForTokenSwap_InsufficientTokenBalance(t *testing.T) {
|
||||
Symbol: "TK",
|
||||
}
|
||||
balanceWei := coins.EtherToWei(coins.StrToDecimal("0.5"))
|
||||
tokenBalance := coins.NewERC20TokenAmountFromDecimals(coins.StrToDecimal("10"), tokenInfo)
|
||||
tokenBalance := coins.NewTokenAmountFromDecimals(coins.StrToDecimal("10"), tokenInfo)
|
||||
providesAmt := coins.StrToDecimal("20")
|
||||
gasPriceWei := big.NewInt(35e9) // 35 GWei
|
||||
err := validateMinBalForTokenSwap(balanceWei, tokenBalance, providesAmt, gasPriceWei)
|
||||
@@ -100,7 +100,7 @@ func Test_validateMinBalForTokenSwap_InsufficientETHBalance(t *testing.T) {
|
||||
Symbol: "TK",
|
||||
}
|
||||
balanceWei := coins.EtherToWei(coins.StrToDecimal("0.007"))
|
||||
tokenBalance := coins.NewERC20TokenAmountFromDecimals(coins.StrToDecimal("10"), tokenInfo)
|
||||
tokenBalance := coins.NewTokenAmountFromDecimals(coins.StrToDecimal("10"), tokenInfo)
|
||||
providesAmt := coins.StrToDecimal("1")
|
||||
gasPriceWei := big.NewInt(35e9) // 35 GWei
|
||||
err := validateMinBalForTokenSwap(balanceWei, tokenBalance, providesAmt, gasPriceWei)
|
||||
|
||||
@@ -7,12 +7,11 @@ import (
|
||||
"github.com/cockroachdb/apd/v3"
|
||||
"github.com/libp2p/go-libp2p/core/peer"
|
||||
|
||||
"github.com/fatih/color"
|
||||
|
||||
"github.com/athanorlabs/atomic-swap/coins"
|
||||
"github.com/athanorlabs/atomic-swap/common"
|
||||
"github.com/athanorlabs/atomic-swap/common/types"
|
||||
pcommon "github.com/athanorlabs/atomic-swap/protocol"
|
||||
|
||||
"github.com/fatih/color"
|
||||
)
|
||||
|
||||
// Provides returns types.ProvidesETH
|
||||
@@ -20,34 +19,52 @@ func (inst *Instance) Provides() coins.ProvidesCoin {
|
||||
return coins.ProvidesETH
|
||||
}
|
||||
|
||||
// InitiateProtocol is called when an RPC call is made from the user to initiate a swap.
|
||||
// InitiateProtocol is called when an RPC call is made from the user to take a swap.
|
||||
// The input units are ether that we will provide.
|
||||
func (inst *Instance) InitiateProtocol(
|
||||
makerPeerID peer.ID,
|
||||
providesAmount *apd.Decimal,
|
||||
offer *types.Offer,
|
||||
) (common.SwapState, error) {
|
||||
err := coins.ValidatePositive("providesAmount", coins.NumEtherDecimals, providesAmount)
|
||||
maxDecimals := uint8(coins.NumEtherDecimals)
|
||||
var token *coins.ERC20TokenInfo
|
||||
if offer.EthAsset.IsToken() {
|
||||
var err error
|
||||
token, err = inst.backend.ETHClient().ERC20Info(inst.backend.Ctx(), offer.EthAsset.Address())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
maxDecimals = token.NumDecimals
|
||||
}
|
||||
|
||||
err := coins.ValidatePositive("providesAmount", maxDecimals, providesAmount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
offerMinETH, err := offer.ExchangeRate.ToETH(offer.MinAmount)
|
||||
providedAssetAmount := coins.NewEthAssetAmount(providesAmount, token)
|
||||
|
||||
providesAmtAsXMR, err := offer.ExchangeRate.ToXMR(providedAssetAmount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
offerMaxETH, err := offer.ExchangeRate.ToETH(offer.MaxAmount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if providesAmtAsXMR.Cmp(offer.MinAmount) < 0 {
|
||||
return nil, &errAmountProvidedTooLow{
|
||||
providedAmtETH: providesAmount,
|
||||
providedAmtAsXMR: providesAmtAsXMR,
|
||||
offerMinAmtXMR: offer.MinAmount,
|
||||
exchangeRate: offer.ExchangeRate,
|
||||
}
|
||||
}
|
||||
|
||||
if offerMinETH.Cmp(providesAmount) > 0 {
|
||||
return nil, errAmountProvidedTooLow{providesAmount, offerMinETH}
|
||||
}
|
||||
|
||||
if offerMaxETH.Cmp(providesAmount) < 0 {
|
||||
return nil, errAmountProvidedTooHigh{providesAmount, offerMaxETH}
|
||||
if providesAmtAsXMR.Cmp(offer.MaxAmount) > 0 {
|
||||
return nil, &errAmountProvidedTooHigh{
|
||||
providedAmtETH: providesAmount,
|
||||
providedAmtAsXMR: providesAmtAsXMR,
|
||||
offerMaxAmtXMR: offer.MaxAmount,
|
||||
exchangeRate: offer.ExchangeRate,
|
||||
}
|
||||
}
|
||||
|
||||
err = validateMinBalance(
|
||||
@@ -60,17 +77,7 @@ func (inst *Instance) InitiateProtocol(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
providedAmount, err := pcommon.GetEthAssetAmount(
|
||||
inst.backend.Ctx(),
|
||||
inst.backend.ETHClient(),
|
||||
providesAmount,
|
||||
offer.EthAsset,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
state, err := inst.initiate(makerPeerID, providedAmount, offer.ExchangeRate, offer.EthAsset, offer.ID)
|
||||
state, err := inst.initiate(makerPeerID, providedAssetAmount, offer.ExchangeRate, offer.EthAsset, offer.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@ import (
|
||||
"github.com/athanorlabs/atomic-swap/common/types"
|
||||
)
|
||||
|
||||
var (
|
||||
testExchangeRate = coins.StrToExchangeRate("0.08")
|
||||
)
|
||||
|
||||
func newTestXMRTaker(t *testing.T) *Instance {
|
||||
b := newBackend(t)
|
||||
cfg := &Config{
|
||||
@@ -37,7 +41,7 @@ func initiate(
|
||||
coins.ProvidesETH,
|
||||
minAmount,
|
||||
maxAmount,
|
||||
coins.ToExchangeRate(apd.New(1, 0)),
|
||||
testExchangeRate,
|
||||
types.EthAssetETH,
|
||||
)
|
||||
s, err := xmrtaker.InitiateProtocol(testPeerID, providesAmount, offer)
|
||||
@@ -46,31 +50,43 @@ func initiate(
|
||||
|
||||
func TestXMRTaker_InitiateProtocol(t *testing.T) {
|
||||
a := newTestXMRTaker(t)
|
||||
zero := new(apd.Decimal)
|
||||
one := apd.New(1, 0)
|
||||
min := coins.StrToDecimal("0.1")
|
||||
max := coins.StrToDecimal("1")
|
||||
|
||||
// Provided between minAmount and maxAmount
|
||||
offer, s, err := initiate(a, apd.New(1, -1), zero, one) // 0.1
|
||||
// Provided between minAmount and maxAmount (0.05 ETH / 0.08 = 0.625 XMR)
|
||||
offer, s, err := initiate(a, coins.StrToDecimal("0.05"), min, max)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, a.swapStates[offer.ID], s)
|
||||
|
||||
// Exact max is in range (0.08 ETH / 0.08 = 1 XMR)
|
||||
offer, s, err = initiate(a, coins.StrToDecimal("0.08"), min, max)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, a.swapStates[offer.ID], s)
|
||||
|
||||
// Exact min is in range (0.008 ETH / 0.08 = 0.1 XMR)
|
||||
offer, s, err = initiate(a, coins.StrToDecimal("0.008"), min, max)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, a.swapStates[offer.ID], s)
|
||||
|
||||
// Provided with too many decimals
|
||||
_, s, err = initiate(a, apd.New(1, -50), zero, one) // 10^-50
|
||||
require.Error(t, err)
|
||||
_, s, err = initiate(a, apd.New(1, -50), min, max) // 10^-50
|
||||
require.ErrorContains(t, err, `"providesAmount" has too many decimal points; found=50 max=18`)
|
||||
require.Equal(t, nil, s)
|
||||
|
||||
// Provided with a negative number
|
||||
_, s, err = initiate(a, apd.New(-1, 0), zero, one) // -1
|
||||
require.Error(t, err)
|
||||
_, s, err = initiate(a, coins.StrToDecimal("-1"), min, max)
|
||||
require.ErrorContains(t, err, `"providesAmount" cannot be negative`)
|
||||
require.Equal(t, nil, s)
|
||||
|
||||
// Provided over maxAmount
|
||||
_, s, err = initiate(a, apd.New(2, 0), one, one) // 2
|
||||
require.Error(t, err)
|
||||
// Provided over maxAmount (0.09 ETH / 0.08 = 1.125 XMR)
|
||||
_, s, err = initiate(a, coins.StrToDecimal("0.09"), min, max)
|
||||
expected := `provided ETH converted to XMR is over offer max of 1 XMR (0.09 ETH / 0.08 = 1.125 XMR)`
|
||||
require.ErrorContains(t, err, expected)
|
||||
require.Equal(t, nil, s)
|
||||
|
||||
// Provided under minAmount
|
||||
_, s, err = initiate(a, apd.New(1, -1), one, one) // 0.1
|
||||
require.Error(t, err)
|
||||
// Provided under minAmount (0.00079 ETH / 0.08 = 0.009875 XMR)
|
||||
_, s, err = initiate(a, coins.StrToDecimal("0.00079"), min, max)
|
||||
expected = `provided ETH converted to XMR is under offer min of 0.1 XMR (0.00079 ETH / 0.08 = 0.009875)`
|
||||
require.ErrorContains(t, err, expected)
|
||||
require.Equal(t, nil, s)
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ func newSwapStateFromStart(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
expectedAmount, err := exchangeRate.ToXMR(providedAmount.AsStandard())
|
||||
expectedAmount, err := exchangeRate.ToXMR(providedAmount)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -130,7 +130,7 @@ func newSwapStateFromStart(
|
||||
makerPeerID,
|
||||
offerID,
|
||||
coins.ProvidesETH,
|
||||
providedAmount.AsStandard(),
|
||||
providedAmount.AsStd(),
|
||||
expectedAmount,
|
||||
exchangeRate,
|
||||
ethAsset,
|
||||
@@ -274,7 +274,7 @@ func newSwapState(
|
||||
cancel()
|
||||
return nil, err
|
||||
}
|
||||
providedAmt = coins.NewERC20TokenAmountFromDecimals(info.ProvidedAmount, tokenInfo)
|
||||
providedAmt = coins.NewTokenAmountFromDecimals(info.ProvidedAmount, tokenInfo)
|
||||
}
|
||||
|
||||
// note: if this is recovering an ongoing swap, this will only
|
||||
@@ -602,7 +602,7 @@ func (s *swapState) lockAsset() (*ethtypes.Receipt, error) {
|
||||
|
||||
cmtXMRTaker := s.secp256k1Pub.Keccak256()
|
||||
cmtXMRMaker := s.xmrmakerSecp256k1PublicKey.Keccak256()
|
||||
log.Debugf("locking %s %s in contract", s.providedAmount.AsStandard(), s.providedAmount.StandardSymbol())
|
||||
log.Debugf("locking %s %s in contract", s.providedAmount.AsStd(), s.providedAmount.StdSymbol())
|
||||
|
||||
nonce := contracts.GenerateNewSwapNonce()
|
||||
receipt, err := s.lockAndWaitForReceipt(cmtXMRMaker, cmtXMRTaker, nonce)
|
||||
@@ -665,7 +665,7 @@ func (s *swapState) lockAsset() (*ethtypes.Receipt, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Infof("locked %s in swap contract, waiting for XMR to be locked", s.providedAmount.StandardSymbol())
|
||||
log.Infof("locked %s in swap contract, waiting for XMR to be locked", s.providedAmount.StdSymbol())
|
||||
return receipt, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -188,7 +188,7 @@ func newTestSwapStateWithERC20(t *testing.T, providesAmt *apd.Decimal) (*swapSta
|
||||
tokenInfo, err := b.ETHClient().ERC20Info(b.Ctx(), addr)
|
||||
require.NoError(t, err)
|
||||
|
||||
providesEthAssetAmt := coins.NewERC20TokenAmountFromDecimals(providesAmt, tokenInfo)
|
||||
providesEthAssetAmt := coins.NewTokenAmountFromDecimals(providesAmt, tokenInfo)
|
||||
|
||||
exchangeRate := coins.ToExchangeRate(apd.New(1, 0)) // 100%
|
||||
swapState, err := newSwapStateFromStart(b, testPeerID, types.Hash{}, false,
|
||||
|
||||
@@ -190,7 +190,7 @@ func (s *NetService) takeOffer(makerPeerID peer.ID, offerID types.Hash, provides
|
||||
|
||||
swapState, err := s.xmrtaker.InitiateProtocol(makerPeerID, providesAmount, offer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initiate protocol: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
skm := swapState.SendKeysMessage().(*message.SendKeysMessage)
|
||||
|
||||
@@ -8,6 +8,7 @@ package rpcclient
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -80,12 +81,14 @@ func (c *Client) post(method string, request any, response any) error {
|
||||
|
||||
defer func() { _ = httpResp.Body.Close() }()
|
||||
|
||||
if response == nil {
|
||||
return nil
|
||||
// Even if the response is nil, we still need to parse the outer JSON-RPC
|
||||
// shell to get any error that the server may have returned.
|
||||
err = json2.DecodeClientResponse(httpResp.Body, response)
|
||||
if response == nil && errors.Is(err, json2.ErrNullResult) {
|
||||
err = nil
|
||||
}
|
||||
|
||||
if err = json2.DecodeClientResponse(httpResp.Body, response); err != nil {
|
||||
return fmt.Errorf("failed to read %q response: %w", method, err)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%q failed: %w", method, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -70,13 +70,13 @@ func deployTestERC20(t *testing.T) ethcommon.Address {
|
||||
aliceCli := rpcclient.NewClient(ctx, defaultXMRTakerSwapdPort)
|
||||
balResp, err = aliceCli.Balances(tokenBalReq)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "1000", balResp.TokenBalances[0].AsStandardString())
|
||||
require.Equal(t, "1000", balResp.TokenBalances[0].AsStdString())
|
||||
|
||||
// verify that Charlie also has exactly 1000 tokens
|
||||
balResp, err = charlieCli.Balances(tokenBalReq)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "1000", balResp.TokenBalances[0].AsStandardString())
|
||||
require.Equal(t, "1000", balResp.TokenBalances[0].AsStdString())
|
||||
|
||||
return erc20Addr
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user