additonal erc20 and taker decimal checks (#478)

This commit is contained in:
Dmitry Holodov
2023-05-30 21:49:22 -05:00
committed by GitHub
parent 65f75c295a
commit 0f1865c33f
39 changed files with 805 additions and 339 deletions

View File

@@ -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()
}

View File

@@ -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
}

View File

@@ -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')
}

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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'))
}
}
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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
{

View 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)
}

View File

@@ -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")

View File

@@ -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)

View File

@@ -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'))
}

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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.

View File

@@ -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)
}
}

View File

@@ -132,7 +132,7 @@ func (s *ExternalSender) NewSwap(
tx := &Transaction{
To: s.contractAddr,
Data: input,
Value: amount.AsStandard(),
Value: amount.AsStd(),
}
s.Lock()

View File

@@ -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)

View File

@@ -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)

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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 {

View File

@@ -138,7 +138,7 @@ func newSwapStateFromStart(
offer.ID,
coins.ProvidesXMR,
providesAmount.AsMonero(),
desiredAmount.AsStandard(),
desiredAmount.AsStd(),
offer.ExchangeRate,
offer.EthAsset,
stage,

View File

@@ -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'),
)
}

View File

@@ -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)

View File

@@ -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(),
}
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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)

View File

@@ -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

View File

@@ -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
}