From 68600810d3bae97db7956a171f817ee2105829fc Mon Sep 17 00:00:00 2001 From: Brian Stafford Date: Wed, 19 Jul 2023 13:53:36 -0500 Subject: [PATCH] checkpoint. redid server. tests passing. no v1 tests on server yet. --- client/asset/eth/contractor.go | 2 +- client/asset/eth/eth.go | 64 ++--- client/asset/eth/eth_test.go | 6 +- client/asset/eth/nodeclient.go | 2 +- dex/networks/eth/params.go | 65 ++++- server/asset/eth/coiner.go | 236 +++++++++++++----- server/asset/eth/coiner_test.go | 160 +++++++------ server/asset/eth/eth.go | 106 +++------ server/asset/eth/eth_test.go | 264 +++++++++++---------- server/asset/eth/rpcclient.go | 86 ++++--- server/asset/eth/rpcclient_harness_test.go | 5 +- server/asset/eth/tokener.go | 198 +++++++++++++--- 12 files changed, 758 insertions(+), 436 deletions(-) diff --git a/client/asset/eth/contractor.go b/client/asset/eth/contractor.go index 341566a642..648e16da20 100644 --- a/client/asset/eth/contractor.go +++ b/client/asset/eth/contractor.go @@ -699,7 +699,7 @@ func (c *contractorV1) redeem(txOpts *bind.TransactOpts, redeems []*asset.Redemp // Not checking version from DecodeLocator because it was already // audited and incorrect version locator would err below anyway. - _, locator, err := dexeth.DecodeLocator(r.Spends.Contract) + _, locator, err := dexeth.DecodeContractData(r.Spends.Contract) if err != nil { return nil, fmt.Errorf("error parsing locator redeem: %w", err) } diff --git a/client/asset/eth/eth.go b/client/asset/eth/eth.go index cf3ed6c5d8..5a3152f800 100644 --- a/client/asset/eth/eth.go +++ b/client/asset/eth/eth.go @@ -97,9 +97,6 @@ const ( // TODO: Find a way to ask the host about their config set max fee and // gas values. maxTxFeeGwei = 1_000_000_000 - - contractVersionERC20 = ^uint32(0) - contractVersionUnknown = contractVersionERC20 - 1 ) var ( @@ -619,14 +616,7 @@ func privKeyFromSeed(seed []byte) (pk []byte, zero func(), err error) { // contractVersion converts a server version to a contract version. It applies // to both tokens and eth right now, but that may not always be the case. func contractVersion(serverVer uint32) uint32 { - switch serverVer { - case 0: - return 0 - case 1: - return 1 - default: - return contractVersionUnknown - } + return dexeth.ProtocolVersion(serverVer).ContractVersion() } // CreateWallet creates a new internal ETH wallet and stores the private key @@ -2149,7 +2139,7 @@ func (w *assetWallet) Redeem(form *asset.RedeemForm, feeWallet *assetWallet, non // from redemption.Spends.Contract. Even for scriptable UTXO assets, the // redeem script in this Contract field is redundant with the SecretHash // field as ExtractSwapDetails can be applied to extract the hash. - ver, locator, err := dexeth.DecodeLocator(redemption.Spends.Contract) + ver, locator, err := dexeth.DecodeContractData(redemption.Spends.Contract) if err != nil { return fail(fmt.Errorf("Redeem: invalid versioned swap contract data: %w", err)) } @@ -2304,7 +2294,7 @@ func recoverPubkey(msgHash, sig []byte) ([]byte, error) { // tokenBalance checks the token balance of the account handled by the wallet. func (w *assetWallet) tokenBalance() (bal *big.Int, err error) { // We don't care about the version. - return bal, w.withTokenContractor(w.assetID, contractVersionERC20, func(c tokenContractor) error { + return bal, w.withTokenContractor(w.assetID, dexeth.ContractVersionERC20, func(c tokenContractor) error { bal, err = c.balance(w.ctx) return err }) @@ -2313,7 +2303,7 @@ func (w *assetWallet) tokenBalance() (bal *big.Int, err error) { // tokenAllowance checks the amount of tokens that the swap contract is approved // to spend on behalf of the account handled by the wallet. func (w *assetWallet) tokenAllowance() (allowance *big.Int, err error) { - return allowance, w.withTokenContractor(w.assetID, contractVersionERC20, func(c tokenContractor) error { + return allowance, w.withTokenContractor(w.assetID, dexeth.ContractVersionERC20, func(c tokenContractor) error { allowance, err = c.allowance(w.ctx) return err }) @@ -2678,7 +2668,7 @@ func (w *assetWallet) AuditContract(coinID, contract, serializedTx dex.Bytes, re return nil, fmt.Errorf("AuditContract: coin id != txHash - coin id: %x, txHash: %s", coinID, tx.Hash()) } - version, locator, err := dexeth.DecodeLocator(contract) + version, locator, err := dexeth.DecodeContractData(contract) if err != nil { return nil, fmt.Errorf("AuditContract: failed to decode contract data: %w", err) } @@ -2720,19 +2710,8 @@ func (w *assetWallet) AuditContract(coinID, contract, serializedTx dex.Bytes, re if !ok { return nil, errors.New("AuditContract: tx does not initiate secret hash") } - // Check vector equivalence. Secret hash equivalence is implied by the - // vectors presence in the map returned from ParseInitiateData. - if vec.Value != txVec.Value { - return nil, errors.New("tx data value doesn't match reported locator data") - } - if vec.To != txVec.To { - return nil, errors.New("tx to address doesn't match reported locator data") - } - if vec.From != txVec.From { - return nil, errors.New("tx from address doesn't match reported locator data") - } - if vec.LockTime != txVec.LockTime { - return nil, errors.New("tx lock time doesn't match reported locator data") + if !dexeth.CompareVectors(vec, txVec) { + return nil, fmt.Errorf("tx vector doesn't match expectation. %+v != %+v", txVec, vec) } val = vec.Value participant = vec.To.String() @@ -2781,7 +2760,7 @@ func (w *assetWallet) LockTimeExpired(ctx context.Context, lockTime time.Time) ( // ContractLockTimeExpired returns true if the specified contract's locktime has // expired, making it possible to issue a Refund. func (w *assetWallet) ContractLockTimeExpired(ctx context.Context, contract dex.Bytes) (bool, time.Time, error) { - contractVer, locator, err := dexeth.DecodeLocator(contract) + contractVer, locator, err := dexeth.DecodeContractData(contract) if err != nil { return false, time.Time{}, err } @@ -2869,7 +2848,7 @@ func (w *assetWallet) FindRedemption(ctx context.Context, _, contract dex.Bytes) // contract, so we are basically doing the next best thing here. const coinIDTmpl = coinIDTakerFoundMakerRedemption + "%s" - contractVer, locator, err := dexeth.DecodeLocator(contract) + contractVer, locator, err := dexeth.DecodeContractData(contract) if err != nil { return nil, nil, err } @@ -2948,7 +2927,7 @@ func (w *assetWallet) findSecret(locator []byte, contractVer uint32) ([]byte, st // Refund refunds a contract. This can only be used after the time lock has // expired. func (w *assetWallet) Refund(_, contract dex.Bytes, feeRate uint64) (dex.Bytes, error) { - contractVer, locator, err := dexeth.DecodeLocator(contract) + contractVer, locator, err := dexeth.DecodeContractData(contract) if err != nil { return nil, fmt.Errorf("Refund: failed to decode contract: %w", err) } @@ -3046,7 +3025,7 @@ func (w *ETHWallet) EstimateRegistrationTxFee(feeRate uint64) uint64 { // EstimateRegistrationTxFee returns an estimate for the tx fee needed to // pay the registration fee using the provided feeRate. func (w *TokenWallet) EstimateRegistrationTxFee(feeRate uint64) uint64 { - g := w.gases(contractVersionERC20) + g := w.gases(dexeth.ContractVersionERC20) if g == nil { w.log.Errorf("no gas table") return math.MaxUint64 @@ -3121,7 +3100,7 @@ func (w *TokenWallet) canSend(value uint64, verifyBalance, isPreEstimate bool) ( return 0, nil, fmt.Errorf("error getting max fee rate: %w", err) } - g := w.gases(contractVersionERC20) + g := w.gases(dexeth.ContractVersionERC20) if g == nil { return 0, nil, fmt.Errorf("gas table not found") } @@ -3213,7 +3192,7 @@ func (w *assetWallet) RestorationInfo(seed []byte) ([]*asset.WalletRestoration, // SwapConfirmations gets the number of confirmations and the spend status // for the specified swap. func (w *assetWallet) SwapConfirmations(ctx context.Context, coinID dex.Bytes, contract dex.Bytes, _ time.Time) (confs uint32, spent bool, err error) { - contractVer, secretHash, err := dexeth.DecodeLocator(contract) + contractVer, secretHash, err := dexeth.DecodeContractData(contract) if err != nil { return 0, false, err } @@ -3392,7 +3371,7 @@ func (eth *assetWallet) DynamicRedemptionFeesPaid(ctx context.Context, coinID, c // secret hashes. func (eth *baseWallet) swapOrRedemptionFeesPaid(ctx context.Context, coinID, contractData dex.Bytes, isInit bool) (fee uint64, secretHashes [][]byte, err error) { - contractVer, locator, err := dexeth.DecodeLocator(contractData) + contractVer, locator, err := dexeth.DecodeContractData(contractData) if err != nil { return 0, nil, err } @@ -3996,7 +3975,7 @@ func (w *assetWallet) checkUnconfirmedRedemption(locator []byte, contractVer uin // entire redemption batch, a new transaction containing only the swap we are // searching for will be created. func (w *assetWallet) confirmRedemptionWithoutMonitoredTx(txHash common.Hash, redemption *asset.Redemption, feeWallet *assetWallet) (*asset.ConfirmRedemptionStatus, error) { - contractVer, locator, err := dexeth.DecodeLocator(redemption.Spends.Contract) + contractVer, locator, err := dexeth.DecodeContractData(redemption.Spends.Contract) if err != nil { return nil, fmt.Errorf("failed to decode contract data: %w", err) } @@ -4089,7 +4068,7 @@ func (w *assetWallet) confirmRedemption(coinID dex.Bytes, redemption *asset.Rede txHash = monitoredTxHash } - contractVer, locator, err := dexeth.DecodeLocator(redemption.Spends.Contract) + contractVer, locator, err := dexeth.DecodeContractData(redemption.Spends.Contract) if err != nil { return nil, fmt.Errorf("failed to decode contract data: %w", err) } @@ -4393,7 +4372,7 @@ func (w *ETHWallet) sendToAddr(addr common.Address, amt uint64, maxFeeRate *big. func (w *TokenWallet) sendToAddr(addr common.Address, amt uint64, maxFeeRate *big.Int) (tx *types.Transaction, err error) { w.baseWallet.nonceSendMtx.Lock() defer w.baseWallet.nonceSendMtx.Unlock() - g := w.gases(contractVersionERC20) + g := w.gases(dexeth.ContractVersionERC20) if g == nil { return nil, fmt.Errorf("no gas table") } @@ -4401,7 +4380,7 @@ func (w *TokenWallet) sendToAddr(addr common.Address, amt uint64, maxFeeRate *bi if err != nil { return nil, err } - return tx, w.withTokenContractor(w.assetID, contractVersionERC20, func(c tokenContractor) error { + return tx, w.withTokenContractor(w.assetID, dexeth.ContractVersionERC20, func(c tokenContractor) error { tx, err = c.transfer(txOpts, addr, w.evmify(amt)) if err != nil { c.voidUnusedNonce() @@ -4524,7 +4503,7 @@ func (w *assetWallet) loadContractors() error { // withContractor runs the provided function with the versioned contractor. func (w *assetWallet) withContractor(contractVer uint32, f func(contractor) error) error { - if contractVer == contractVersionERC20 { + if contractVer == dexeth.ContractVersionERC20 { // For ERC02 methods, use the most recent contractor version. var bestVer uint32 var bestContractor contractor @@ -4557,7 +4536,7 @@ func (w *assetWallet) withTokenContractor(assetID, ver uint32, f func(tokenContr // estimateApproveGas estimates the gas required for a transaction approving a // spender for an ERC20 contract. func (w *assetWallet) estimateApproveGas(newGas *big.Int) (gas uint64, err error) { - return gas, w.withTokenContractor(w.assetID, contractVersionERC20, func(c tokenContractor) error { + return gas, w.withTokenContractor(w.assetID, dexeth.ContractVersionERC20, func(c tokenContractor) error { gas, err = c.estimateApproveGas(w.ctx, newGas) return err }) @@ -4565,8 +4544,9 @@ func (w *assetWallet) estimateApproveGas(newGas *big.Int) (gas uint64, err error // estimateTransferGas estimates the gas needed for a token transfer call to an // ERC20 contract. +// TODO: Delete this and contractor methods. Unused. func (w *assetWallet) estimateTransferGas(val uint64) (gas uint64, err error) { - return gas, w.withTokenContractor(w.assetID, contractVersionERC20, func(c tokenContractor) error { + return gas, w.withTokenContractor(w.assetID, dexeth.ContractVersionERC20, func(c tokenContractor) error { gas, err = c.estimateTransferGas(w.ctx, w.evmify(val)) return err }) diff --git a/client/asset/eth/eth_test.go b/client/asset/eth/eth_test.go index 011bd61e2a..36e39e7b8f 100644 --- a/client/asset/eth/eth_test.go +++ b/client/asset/eth/eth_test.go @@ -2149,7 +2149,7 @@ func testSwap(t *testing.T, assetID uint32) { testName, receipt.Coin().Value(), contract.Value) } contractData := receipt.Contract() - contractVer, locator, err := dexeth.DecodeLocator(contractData) + contractVer, locator, err := dexeth.DecodeContractData(contractData) if err != nil { t.Fatalf("failed to decode contract data: %v", err) } @@ -2725,7 +2725,7 @@ func testRedeem(t *testing.T, assetID uint32) { // Check that value of output coin is as axpected var totalSwapValue uint64 for _, redemption := range test.form.Redemptions { - _, locator, err := dexeth.DecodeLocator(redemption.Spends.Contract) + _, locator, err := dexeth.DecodeContractData(redemption.Spends.Contract) if err != nil { t.Fatalf("DecodeLocator: %v", err) } @@ -3178,7 +3178,7 @@ func testAuditContract(t *testing.T, assetID uint32) { t.Fatalf(`"%v": expected contract %x != actual %x`, test.name, test.contract, auditInfo.Contract) } - _, expectedSecretHash, err := dexeth.DecodeLocator(test.contract) + _, expectedSecretHash, err := dexeth.DecodeContractData(test.contract) if err != nil { t.Fatalf(`"%v": failed to decode versioned bytes: %v`, test.name, err) } diff --git a/client/asset/eth/nodeclient.go b/client/asset/eth/nodeclient.go index 533bf350a5..6fd0a6b85e 100644 --- a/client/asset/eth/nodeclient.go +++ b/client/asset/eth/nodeclient.go @@ -429,7 +429,7 @@ func gases(parentID, assetID uint32, contractVer uint32, net dex.Network) *dexet return nil } - if contractVer != contractVersionERC20 { + if contractVer != dexeth.ContractVersionERC20 { contract, found := netToken.SwapContracts[contractVer] if !found { return nil diff --git a/dex/networks/eth/params.go b/dex/networks/eth/params.go index 6fb6a0745e..f9c1f38e0e 100644 --- a/dex/networks/eth/params.go +++ b/dex/networks/eth/params.go @@ -5,6 +5,7 @@ package eth import ( "encoding/binary" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -124,8 +125,20 @@ func EncodeContractData(contractVersion uint32, locator []byte) []byte { return b } -// DecodeLocator unpacks the contract version and secret hash. -func DecodeLocator(data []byte) (contractVersion uint32, locator []byte, err error) { +func DecodeContractDataV0(data []byte) (secretHash [32]byte, err error) { + contractVer, secretHashB, err := DecodeContractData(data) + if err != nil { + return secretHash, err + } + if contractVer != 0 { + return secretHash, errors.New("not contract version 0") + } + copy(secretHash[:], secretHashB) + return +} + +// DecodeContractData unpacks the contract version and the locator. +func DecodeContractData(data []byte) (contractVersion uint32, locator []byte, err error) { if len(data) < 4 { err = errors.New("invalid short encoding") return @@ -271,6 +284,17 @@ func (v *SwapVector) Locator() []byte { return locator } +func (v *SwapVector) String() string { + return fmt.Sprintf("{ from = %s, to = %s, value = %d, secret hash = %s, locktime = %s }", + v.From, v.To, v.Value, hex.EncodeToString(v.SecretHash[:]), time.UnixMilli(int64(v.LockTime))) +} + +func CompareVectors(v1, v2 *SwapVector) bool { + // Check vector equivalence. + return v1.Value == v2.Value && v1.To == v2.To && v1.From == v2.From && + v1.LockTime == v2.LockTime && v1.SecretHash == v2.SecretHash +} + // SwapStatus is the contract data that specifies the current contract state. type SwapStatus struct { BlockHeight uint64 @@ -390,12 +414,37 @@ func ParseV1Locator(locator []byte) (v *SwapVector, err error) { return } -func SwapVectorToAbigen(c *SwapVector) swapv1.ETHSwapVector { +func SwapVectorToAbigen(v *SwapVector) swapv1.ETHSwapVector { return swapv1.ETHSwapVector{ - SecretHash: c.SecretHash, - Initiator: c.From, - RefundTimestamp: c.LockTime, - Participant: c.To, - Value: c.Value, + SecretHash: v.SecretHash, + Initiator: v.From, + RefundTimestamp: v.LockTime, + Participant: v.To, + Value: v.Value, + } +} + +// ProtocolVersion assists in mapping the dex.Asset.Version to a contract +// version. +type ProtocolVersion uint32 + +const ( + ProtocolVersionZero ProtocolVersion = iota + ProtocolVersionV1Contracts +) + +func (v ProtocolVersion) ContractVersion() uint32 { + switch v { + case ProtocolVersionZero: + return 0 + case ProtocolVersionV1Contracts: + return 1 + default: + return ContractVersionUnknown } } + +var ( + ContractVersionERC20 = ^uint32(0) + ContractVersionUnknown = ContractVersionERC20 - 1 +) diff --git a/server/asset/eth/coiner.go b/server/asset/eth/coiner.go index 3e7ec7a0cd..12c46f9283 100644 --- a/server/asset/eth/coiner.go +++ b/server/asset/eth/coiner.go @@ -9,6 +9,7 @@ import ( "fmt" "math/big" + "decred.org/dcrdex/dex" dexeth "decred.org/dcrdex/dex/networks/eth" "decred.org/dcrdex/server/asset" "github.com/ethereum/go-ethereum" @@ -20,7 +21,7 @@ var _ asset.Coin = (*redeemCoin)(nil) type baseCoin struct { backend *AssetBackend - vector *dexeth.SwapVector + locator []byte gasFeeCap uint64 gasTipCap uint64 txHash common.Hash @@ -54,29 +55,72 @@ func (be *AssetBackend) newSwapCoin(coinID []byte, contractData []byte) (*swapCo return nil, err } - vectors, err := dexeth.ParseInitiateDataV1(bc.txData) - if err != nil { - return nil, fmt.Errorf("unable to parse initiate call data: %v", err) - } + var sum uint64 + var vector *dexeth.SwapVector + switch bc.contractVer { + case 0: + var secretHash [32]byte + copy(secretHash[:], bc.locator) - vec, ok := vectors[bc.vector.SecretHash] - if !ok { - return nil, fmt.Errorf("tx %v does not contain initiation with locator %x", bc.txHash, bc.vector.Locator()) - } + inits, err := dexeth.ParseInitiateDataV0(bc.txData) + if err != nil { + return nil, fmt.Errorf("unable to parse v0 initiate call data: %v", err) + } + + init, ok := inits[secretHash] + if !ok { + return nil, fmt.Errorf("tx %v does not contain v0 initiation with secret hash %x", bc.txHash, secretHash[:]) + } + for _, in := range inits { + sum = +be.atomize(in.Value) + } + + vector = &dexeth.SwapVector{ + // From: , + To: init.Participant, + Value: be.atomize(init.Value), + SecretHash: secretHash, + LockTime: uint64(init.LockTime.UnixMilli()), + } - if be.assetID == BipID { - var sum uint64 - for _, in := range vectors { - sum += in.Value + // if value < bc.vector.Value { + // return nil, fmt.Errorf("tx data value is too low. %d < %d", value, bc.vector.Value) + // } + // if init.Participant != bc.vector.To { + // return nil, fmt.Errorf("wrong participant in tx data. wanted %s, got %s", bc.vector.To, init.Participant) + // } + // if init.LockTime.UnixMilli() != int64(bc.vector.LockTime) { + // return nil, fmt.Errorf("wrong locktime in tx data. wanted %s, got %s", time.UnixMilli(int64(bc.vector.LockTime)), init.LockTime) + // } + case 1: + contractVector, err := dexeth.ParseV1Locator(bc.locator) + if err != nil { + return nil, fmt.Errorf("contract data vector decoding error: %w", err) + } + txVectors, err := dexeth.ParseInitiateDataV1(bc.txData) + if err != nil { + return nil, fmt.Errorf("unable to parse v1 initiate call data: %v", err) + } + txVector, ok := txVectors[contractVector.SecretHash] + if !ok { + return nil, fmt.Errorf("tx %v does not contain v1 initiation with vector %s", bc.txHash, contractVector) } - if sum != dexeth.WeiToGwei(bc.value) { - return nil, fmt.Errorf("tx %s value < sum of inits. %d < %d", bc.txHash, bc.value, sum) + if !dexeth.CompareVectors(contractVector, txVector) { + return nil, fmt.Errorf("contract and transaction vectors do not match. %+v != %+v", contractVector, txVector) } + vector = txVector + for _, v := range txVectors { + sum += v.Value + } + } + + if be.assetID == BipID && be.atomize(bc.value) < sum { + return nil, fmt.Errorf("tx %s value < sum of inits. %d < %d", bc.txHash, bc.value, sum) } return &swapCoin{ baseCoin: bc, - vector: vec, + vector: vector, }, nil } @@ -90,25 +134,26 @@ func (be *AssetBackend) newRedeemCoin(coinID []byte, contractData []byte) (*rede if err == asset.CoinNotFoundError { // If the coin is not found, check to see if the swap has been // redeemed by another transaction. - contractVer, locator, err := dexeth.DecodeLocator(contractData) + contractVer, locator, err := dexeth.DecodeContractData(contractData) if err != nil { return nil, err } - vector, err := dexeth.ParseV1Locator(locator) - if err != nil { - return nil, err + if be.contractVer != contractVer { + return nil, fmt.Errorf("wrong contract version for %s. wanted %d, got %d", dex.BipIDSymbol(be.assetID), be.contractVer, contractVer) } - be.log.Warnf("redeem coin with ID %x for locator %x was not found", coinID, locator) - status, err := be.node.status(be.ctx, be.assetID, vector) + + status, vec, err := be.node.statusAndVector(be.ctx, be.assetID, locator) if err != nil { return nil, err } + be.log.Warnf("redeem coin with ID %x for %s was not found", coinID, vec) if status.Step != dexeth.SSRedeemed { return nil, asset.CoinNotFoundError } + bc = &baseCoin{ backend: be, - vector: vector, + locator: locator, contractVer: contractVer, } return &redeemCoin{ @@ -123,18 +168,44 @@ func (be *AssetBackend) newRedeemCoin(coinID []byte, contractData []byte) (*rede return nil, fmt.Errorf("expected tx value of zero for redeem but got: %d", bc.value) } - redemptions, err := dexeth.ParseRedeemDataV1(bc.txData) - if err != nil { - return nil, fmt.Errorf("unable to parse redemption call data: %v", err) - } - redemption, ok := redemptions[bc.vector.SecretHash] - if !ok { - return nil, fmt.Errorf("tx %v does not contain redemption with locator %x", bc.txHash, bc.vector) + var secret [32]byte + switch bc.contractVer { + case 0: + var secretHash [32]byte + copy(secretHash[:], bc.locator) + redemptions, err := dexeth.ParseRedeemDataV0(bc.txData) + if err != nil { + return nil, fmt.Errorf("unable to parse v0 redemption call data: %v", err) + } + redemption, ok := redemptions[secretHash] + if !ok { + return nil, fmt.Errorf("tx %v does not contain redemption for v0 secret hash %x", bc.txHash, secretHash[:]) + } + secret = redemption.Secret + case 1: + vector, err := dexeth.ParseV1Locator(bc.locator) + if err != nil { + return nil, fmt.Errorf("error parsing vector from v1 locator '%x': %w", bc.locator, err) + } + redemptions, err := dexeth.ParseRedeemDataV1(bc.txData) + if err != nil { + return nil, fmt.Errorf("unable to parse v1 redemption call data: %v", err) + } + redemption, ok := redemptions[vector.SecretHash] + if !ok { + return nil, fmt.Errorf("tx %v does not contain redemption for v1 vector %s", bc.txHash, vector) + } + if !dexeth.CompareVectors(redemption.Contract, vector) { + return nil, fmt.Errorf("encoded vector %q doesn't match expected vector %q", redemption.Contract, vector) + } + secret = redemption.Secret + default: + return nil, fmt.Errorf("version %d redeem coin not supported", bc.contractVer) } return &redeemCoin{ baseCoin: bc, - secret: redemption.Secret, + secret: secret, }, nil } @@ -151,26 +222,20 @@ func (be *AssetBackend) baseCoin(coinID []byte, contractData []byte) (*baseCoin, } return nil, fmt.Errorf("unable to fetch transaction: %v", err) } - contractAddr := tx.To() - if *contractAddr != be.contractAddr { - return nil, fmt.Errorf("contract address is not supported: %v", contractAddr) - } serializedTx, err := tx.MarshalBinary() if err != nil { return nil, err } - contractVer, locator, err := dexeth.DecodeLocator(contractData) + contractVer, locator, err := dexeth.DecodeContractData(contractData) if err != nil { return nil, err } - if contractVer != ethContractVersion { - return nil, fmt.Errorf("contract version %d not supported, only %d", contractVer, ethContractVersion) - } - vector, err := dexeth.ParseV1Locator(locator) - if err != nil { - return nil, err + + contractAddr := tx.To() + if *contractAddr != be.contractAddr { + return nil, fmt.Errorf("contract address is not supported: %v", contractAddr) } // Gas price is not stored in the swap, and is used to determine if the @@ -186,7 +251,7 @@ func (be *AssetBackend) baseCoin(coinID []byte, contractData []byte) (*baseCoin, zero := new(big.Int) gasFeeCap := tx.GasFeeCap() if gasFeeCap == nil || gasFeeCap.Cmp(zero) <= 0 { - return nil, fmt.Errorf("Failed to parse gas fee cap from tx %s", txHash) + return nil, fmt.Errorf("failed to parse gas fee cap from tx %s", txHash) } gasFeeCapGwei, err := dexeth.WeiToGweiUint64(gasFeeCap) if err != nil { @@ -195,7 +260,7 @@ func (be *AssetBackend) baseCoin(coinID []byte, contractData []byte) (*baseCoin, gasTipCap := tx.GasTipCap() if gasTipCap == nil || gasTipCap.Cmp(zero) <= 0 { - return nil, fmt.Errorf("Failed to parse gas tip cap from tx %s", txHash) + return nil, fmt.Errorf("failed to parse gas tip cap from tx %s", txHash) } gasTipCapGwei, err := dexeth.WeiToGweiUint64(gasTipCap) if err != nil { @@ -204,7 +269,7 @@ func (be *AssetBackend) baseCoin(coinID []byte, contractData []byte) (*baseCoin, return &baseCoin{ backend: be, - vector: vector, + locator: locator, gasFeeCap: gasFeeCapGwei, gasTipCap: gasTipCapGwei, txHash: txHash, @@ -225,7 +290,7 @@ func (be *AssetBackend) baseCoin(coinID []byte, contractData []byte) (*baseCoin, // and the same account and nonce, effectively voiding the transaction we // expected to be mined. func (c *swapCoin) Confirmations(ctx context.Context) (int64, error) { - status, err := c.backend.node.status(ctx, c.backend.assetID, c.vector) + status, checkVec, err := c.backend.node.statusAndVector(ctx, c.backend.assetID, c.locator) if err != nil { return -1, err } @@ -238,39 +303,90 @@ func (c *swapCoin) Confirmations(ctx context.Context) (int64, error) { // Assume the tx still has a chance of being mined. return 0, nil } + // Any other swap state is ok. We are sure that initialization + // happened. + + // The swap initiation transaction has some number of + // confirmations, and we are sure the secret hash belongs to + // this swap. Assert that the value, receiver, and locktime are + // as expected. + switch c.contractVer { + case 0: + if checkVec.Value != c.vector.Value { + return -1, fmt.Errorf("tx data swap val (%d) does not match contract value (%d)", + c.vector.Value, checkVec.Value) + } + if checkVec.To != c.vector.To { + return -1, fmt.Errorf("tx data participant %q does not match contract value %q", + c.vector.To, checkVec.To) + } + if checkVec.LockTime != c.vector.LockTime { + return -1, fmt.Errorf("expected swap locktime (%d) does not match expected (%d)", + c.vector.LockTime, checkVec.LockTime) + } + case 1: + if err := setV1StatusBlockHeight(ctx, c.backend.node, status, c.baseCoin); err != nil { + return 0, err + } + } + bn, err := c.backend.node.blockNumber(ctx) if err != nil { return 0, fmt.Errorf("unable to fetch block number: %v", err) } - if status.Step == dexeth.SSInitiated { - return int64(bn - status.BlockHeight + 1), nil + return int64(bn - status.BlockHeight + 1), nil +} + +func setV1StatusBlockHeight(ctx context.Context, node ethFetcher, status *dexeth.SwapStatus, bc *baseCoin) error { + switch status.Step { + case dexeth.SSNone, dexeth.SSInitiated: + case dexeth.SSRedeemed, dexeth.SSRefunded: + // No block height for redeemed or refunded version 1 contracts, + // only SSInitiated. + r, err := node.transactionReceipt(ctx, bc.txHash) + if err != nil { + return err + } + status.BlockHeight = r.BlockNumber.Uint64() } - // Redeemed or refunded. Have to check the tx. - return c.backend.txConfirmations(ctx, c.txHash) + return nil } func (c *redeemCoin) Confirmations(ctx context.Context) (int64, error) { - status, err := c.backend.node.status(ctx, c.backend.assetID, c.vector) + status, err := c.backend.node.status(ctx, c.backend.assetID, c.locator) if err != nil { return -1, err } - // If swap is in None state, then the redemption can't possibly - // succeed as the swap must already be in the Initialized state - // to redeem. If the swap is in the Refunded state, then the - // redemption either failed or never happened. - if status.Step == dexeth.SSNone || status.Step == dexeth.SSRefunded { - return -1, fmt.Errorf("redemption in failed state with swap at %s state", status.Step) + // There should be no need to check the counter party, or value + // as a swap with a specific secret hash that has been redeemed + // wouldn't have been redeemed without ensuring the initiator + // is the expected address and value was also as expected. Also + // not validating the locktime, as the swap is redeemed and + // locktime no longer relevant. + if status.Step == dexeth.SSRedeemed { + if c.contractVer == 1 { + if err := setV1StatusBlockHeight(ctx, c.backend.node, status, c.baseCoin); err != nil { + return -1, err + } + } + bn, err := c.backend.node.blockNumber(ctx) + if err != nil { + return 0, fmt.Errorf("unable to fetch block number: %v", err) + } + return int64(bn - status.BlockHeight + 1), nil } - // If swap is in the Initiated state, the redemption may be // unmined. if status.Step == dexeth.SSInitiated { // Assume the tx still has a chance of being mined. return 0, nil } - - return c.backend.txConfirmations(ctx, c.txHash) + // If swap is in None state, then the redemption can't possibly + // succeed as the swap must already be in the Initialized state + // to redeem. If the swap is in the Refunded state, then the + // redemption either failed or never happened. + return -1, fmt.Errorf("redemption in failed state with swap at %s state", status.Step) } func (c *redeemCoin) Value() uint64 { return 0 } diff --git a/server/asset/eth/coiner_test.go b/server/asset/eth/coiner_test.go index b4374a6d14..799d483fe6 100644 --- a/server/asset/eth/coiner_test.go +++ b/server/asset/eth/coiner_test.go @@ -25,11 +25,11 @@ func randomAddress() *common.Address { func TestNewRedeemCoin(t *testing.T) { contractAddr := randomAddress() - var txHash [32]byte + var secret, secretHash, txHash [32]byte copy(txHash[:], encode.RandomBytes(32)) - contract := dexeth.EncodeContractData(1, vector2.Locator()) - vector3 := *vector2 - contract3 := dexeth.EncodeContractData(1, vector3.Locator()) + copy(secret[:], redeemSecretB) + copy(secretHash[:], redeemSecretHashB) + contract := dexeth.EncodeContractData(0, secretHash[:]) const gasPrice = 30 const gasTipCap = 2 const value = 5e9 @@ -45,6 +45,7 @@ func TestNewRedeemCoin(t *testing.T) { }{{ name: "ok redeem", tx: tTx(gasPrice, gasTipCap, 0, contractAddr, redeemCalldata), + swap: tSwap(97, initLocktime, 1000, secret, dexeth.SSRedeemed, &initParticipantAddr), contract: contract, }, { name: "non zero value with redeem", @@ -59,17 +60,17 @@ func TestNewRedeemCoin(t *testing.T) { }, { name: "tx coin id for redeem - contract not in tx", tx: tTx(gasPrice, gasTipCap, value, contractAddr, redeemCalldata), - contract: contract3, + contract: encode.RandomBytes(32), wantErr: true, }, { name: "tx not found, redeemed", txErr: ethereum.NotFound, - swap: tSwap(97, initLocktime, 1000, tRedeem2.Secret, dexeth.SSRedeemed, &initParticipantAddr), + swap: tSwap(97, initLocktime, 1000, secret, dexeth.SSRedeemed, &initParticipantAddr), contract: contract, }, { name: "tx not found, not redeemed", txErr: ethereum.NotFound, - swap: tSwap(97, initLocktime, 1000, tRedeem2.Secret, dexeth.SSInitiated, &initParticipantAddr), + swap: tSwap(97, initLocktime, 1000, secret, dexeth.SSInitiated, &initParticipantAddr), contract: contract, wantErr: true, }, { @@ -107,8 +108,7 @@ func TestNewRedeemCoin(t *testing.T) { t.Fatalf("unexpected error for test %q: %v", test.name, err) } - if test.txErr == nil && (rc.vector.SecretHash != tRedeem2.V.SecretHash || - rc.secret != tRedeem2.Secret || + if test.txErr == nil && (rc.secret != secret || rc.value.Uint64() != 0 || rc.gasFeeCap != wantGas || rc.gasTipCap != wantGasTipCap) { @@ -119,101 +119,102 @@ func TestNewRedeemCoin(t *testing.T) { func TestNewSwapCoin(t *testing.T) { contractAddr, randomAddr := randomAddress(), randomAddress() - var txHash [32]byte + var secret, secretHash, txHash [32]byte copy(txHash[:], encode.RandomBytes(32)) + copy(secret[:], redeemSecretB) + copy(secretHash[:], redeemSecretHashB) txCoinIDBytes := txHash[:] badCoinIDBytes := encode.RandomBytes(39) - const gasPrice = 30 - const value = 5e9 - const gasTipCap = 2 + const ( + gasPrice = 30 + txVal = 5e9 + swapVal = txVal / 2 + gasTipCap = 2 + ) wantGas, err := dexeth.WeiToGweiUint64(big.NewInt(3e10)) if err != nil { t.Fatal(err) } - wantVal, err := dexeth.WeiToGweiUint64(big.NewInt(5e18)) - if err != nil { - t.Fatal(err) - } wantGasTipCap, err := dexeth.WeiToGweiUint64(big.NewInt(2e9)) if err != nil { t.Fatal(err) } - goodContract := dexeth.EncodeContractData(1, vector2.Locator()) - vector3 := *vector2 - vector3.SecretHash = [32]byte{3} - badContract := dexeth.EncodeContractData(1, vector3.Locator()) tests := []struct { name string coinID []byte contract []byte tx *types.Transaction swpErr, txErr error + swap *dexeth.SwapState wantErr bool }{{ name: "ok init", - tx: tTx(gasPrice, gasTipCap, value, contractAddr, initCalldata), + tx: tTx(gasPrice, gasTipCap, txVal, contractAddr, initCalldata), coinID: txCoinIDBytes, - contract: goodContract, + contract: dexeth.EncodeContractData(0, secretHash[:]), + swap: tSwap(97, initLocktime, swapVal, secret, dexeth.SSRedeemed, &initParticipantAddr), }, { name: "contract incorrect length", - tx: tTx(gasPrice, gasTipCap, value, contractAddr, initCalldata), + tx: tTx(gasPrice, gasTipCap, txVal, contractAddr, initCalldata), coinID: txCoinIDBytes, - contract: goodContract[:len(goodContract)-1], + contract: initSecretHashA[:31], wantErr: true, }, { name: "tx has no data", - tx: tTx(gasPrice, gasTipCap, value, contractAddr, nil), + tx: tTx(gasPrice, gasTipCap, txVal, contractAddr, nil), coinID: txCoinIDBytes, - contract: goodContract, + contract: initSecretHashA, wantErr: true, }, { name: "unable to decode init data, must be init for init coin type", - tx: tTx(gasPrice, gasTipCap, value, contractAddr, redeemCalldata), + tx: tTx(gasPrice, gasTipCap, txVal, contractAddr, redeemCalldata), coinID: txCoinIDBytes, - contract: goodContract, + contract: initSecretHashA, wantErr: true, }, { name: "unable to decode CoinID", - tx: tTx(gasPrice, gasTipCap, value, contractAddr, initCalldata), - contract: goodContract, + tx: tTx(gasPrice, gasTipCap, txVal, contractAddr, initCalldata), + contract: initSecretHashA, wantErr: true, }, { name: "invalid coinID", - tx: tTx(gasPrice, gasTipCap, value, contractAddr, initCalldata), + tx: tTx(gasPrice, gasTipCap, txVal, contractAddr, initCalldata), coinID: badCoinIDBytes, - contract: goodContract, + contract: initSecretHashA, wantErr: true, }, { name: "transaction error", - tx: tTx(gasPrice, gasTipCap, value, contractAddr, initCalldata), + tx: tTx(gasPrice, gasTipCap, txVal, contractAddr, initCalldata), coinID: txCoinIDBytes, - contract: goodContract, + contract: initSecretHashA, txErr: errors.New(""), wantErr: true, }, { name: "transaction not found error", - tx: tTx(gasPrice, gasTipCap, value, contractAddr, initCalldata), + tx: tTx(gasPrice, gasTipCap, txVal, contractAddr, initCalldata), coinID: txCoinIDBytes, - contract: goodContract, + contract: initSecretHashA, txErr: ethereum.NotFound, wantErr: true, }, { name: "wrong contract", - tx: tTx(gasPrice, gasTipCap, value, randomAddr, initCalldata), + tx: tTx(gasPrice, gasTipCap, txVal, randomAddr, initCalldata), coinID: txCoinIDBytes, - contract: badContract, + contract: initSecretHashA, + wantErr: true, + }, { + name: "tx coin id for swap - contract not in tx", + tx: tTx(gasPrice, gasTipCap, txVal, contractAddr, initCalldata), + coinID: txCoinIDBytes, + contract: encode.RandomBytes(32), wantErr: true, - // }, { TODO: This test was doing nothing, I think. - // name: "tx coin id for swap - contract not in tx", - // tx: tTx(gasPrice, gasTipCap, value, contractAddr, initCalldata), - // coinID: txCoinIDBytes, - // contract: encode.RandomBytes(32), - // wantErr: true, }} for _, test := range tests { node := &testNode{ - tx: test.tx, - txErr: test.txErr, + tx: test.tx, + txErr: test.txErr, + swp: test.swap, + swpErr: test.swpErr, } eth := &AssetBackend{ baseBackend: &baseBackend{ @@ -235,12 +236,13 @@ func TestNewSwapCoin(t *testing.T) { t.Fatalf("unexpected error for test %q: %v", test.name, err) } - if sc.vector.To != tSwap2.Participant || - sc.vector.SecretHash != tRedeem2.V.SecretHash || - dexeth.WeiToGwei(sc.value) != wantVal || + if sc.vector.To != initParticipantAddr || + sc.vector.SecretHash != secretHash || + dexeth.WeiToGwei(sc.value) != txVal || + sc.vector.Value != swapVal || sc.gasFeeCap != wantGas || sc.gasTipCap != wantGasTipCap || - sc.vector.LockTime != tSwap2.RefundTimestamp { + sc.vector.LockTime != initLocktime*1000 { t.Fatalf("returns do not match expected for test %q / %v", test.name, sc) } } @@ -254,13 +256,15 @@ type Confirmer interface { func TestConfirmations(t *testing.T) { contractAddr, nullAddr := new(common.Address), new(common.Address) copy(contractAddr[:], encode.RandomBytes(20)) - txHash := bytesToArray(encode.RandomBytes(32)) - secret := tRedeem2.Secret + var secret, secretHash, txHash [32]byte + copy(txHash[:], encode.RandomBytes(32)) + copy(secret[:], redeemSecretB) + copy(secretHash[:], redeemSecretHashB) const gasPrice = 30 const gasTipCap = 2 - swapVal := tSwap2.Value - txVal := tSwap1.Value + tSwap2.Value - const blockNumber = 100 + const swapVal = 25e8 + const txVal = swapVal * 2 + const oneGweiMore = swapVal + 1 tests := []struct { name string swap *dexeth.SwapState @@ -284,14 +288,13 @@ func TestConfirmations(t *testing.T) { bn: 97, swap: tSwap(97, initLocktime, swapVal, secret, dexeth.SSRedeemed, &initParticipantAddr), value: 0, - wantConfs: 4, + wantConfs: 1, redeem: true, }, { - name: "ok redeem swap status initiated", - swap: tSwap(blockNumber, initLocktime, swapVal, secret, dexeth.SSInitiated, &initParticipantAddr), - value: 0, - redeem: true, - wantConfs: 0, // SSInitiated is always zero confs for redeems. + name: "ok redeem swap status initiated", + swap: tSwap(97, initLocktime, swapVal, secret, dexeth.SSInitiated, &initParticipantAddr), + value: 0, + redeem: true, }, { name: "redeem bad swap state None", swap: tSwap(0, 0, 0, secret, dexeth.SSNone, nullAddr), @@ -300,21 +303,42 @@ func TestConfirmations(t *testing.T) { redeem: true, }, { name: "error getting swap", - swapErr: errors.New(""), + swapErr: errors.New("test error"), + value: txVal, + wantErr: true, + }, { + name: "value differs from initial transaction", + swap: tSwap(99, initLocktime, oneGweiMore, secret, dexeth.SSInitiated, &initParticipantAddr), + value: txVal, + wantErr: true, + }, { + name: "participant differs from initial transaction", + swap: tSwap(99, initLocktime, swapVal, secret, dexeth.SSInitiated, nullAddr), + value: txVal, + wantErr: true, + // }, { + // name: "locktime not an int64", + // swap: tSwap(99, new(big.Int).SetUint64(^uint64(0)), value, secret, dexeth.SSInitiated, &initParticipantAddr), + // value: value, + // ct: sctInit, + // wantErr: true, + }, { + name: "locktime differs from initial transaction", + swap: tSwap(99, 0, swapVal, secret, dexeth.SSInitiated, &initParticipantAddr), value: txVal, wantErr: true, }, { name: "block number error", swap: tSwap(97, initLocktime, swapVal, secret, dexeth.SSInitiated, &initParticipantAddr), value: txVal, - bnErr: errors.New(""), + bnErr: errors.New("test error"), wantErr: true, }} for _, test := range tests { node := &testNode{ swp: test.swap, swpErr: test.swapErr, - blkNum: blockNumber, + blkNum: test.bn, blkNumErr: test.bnErr, } eth := &AssetBackend{ @@ -327,11 +351,7 @@ func TestConfirmations(t *testing.T) { atomize: dexeth.WeiToGwei, } - swapData := dexeth.EncodeContractData(1, vector2.Locator()) - - if test.swap != nil { - node.rcpt = &types.Receipt{BlockNumber: big.NewInt(int64(test.swap.BlockHeight))} - } + swapData := dexeth.EncodeContractData(0, secretHash[:]) var confirmer Confirmer var err error diff --git a/server/asset/eth/eth.go b/server/asset/eth/eth.go index 434efa87e3..d457d47267 100644 --- a/server/asset/eth/eth.go +++ b/server/asset/eth/eth.go @@ -23,7 +23,6 @@ import ( "decred.org/dcrdex/server/asset" "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa" - "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" @@ -31,8 +30,9 @@ import ( type registeredToken struct { *dexeth.Token - drv *TokenDriver - ver uint32 + drv *TokenDriver + protocolVer uint32 + contractVer uint32 } var registeredTokens = make(map[uint32]*registeredToken) @@ -46,43 +46,44 @@ func networkToken(assetID uint32, net dex.Network) (token *registeredToken, netT if !found { return nil, nil, nil, fmt.Errorf("no addresses for %s on %s", token.Name, net) } - contract, found = netToken.SwapContracts[token.ver] + contract, found = netToken.SwapContracts[token.contractVer] if !found || contract.Address == (common.Address{}) { - return nil, nil, nil, fmt.Errorf("no version %d address for %s on %s", token.ver, token.Name, net) + return nil, nil, nil, fmt.Errorf("no version %d address for %s on %s", token.contractVer, token.Name, net) } return } -func registerToken(assetID uint32, ver uint32) { +func registerToken(assetID uint32, protocolVer dexeth.ProtocolVersion) { token, exists := dexeth.Tokens[assetID] if !exists { panic(fmt.Sprintf("no token constructor for asset ID %d", assetID)) } drv := &TokenDriver{ driverBase: driverBase{ - version: ver, - unitInfo: token.UnitInfo, + protocolVer: uint32(protocolVer), + unitInfo: token.UnitInfo, }, token: token.Token, } asset.RegisterToken(assetID, drv) registeredTokens[assetID] = ®isteredToken{ - Token: token, - drv: drv, - ver: ver, + Token: token, + drv: drv, + protocolVer: uint32(protocolVer), + contractVer: protocolVer.ContractVersion(), } } func init() { asset.Register(BipID, &Driver{ driverBase: driverBase{ - version: version, - unitInfo: dexeth.UnitInfo, + protocolVer: uint32(ethProtocolVersion), + unitInfo: dexeth.UnitInfo, }, }) - registerToken(testTokenID, ethContractVersion) - registerToken(usdcID, ethContractVersion) + registerToken(testTokenID, dexeth.ProtocolVersionZero) + registerToken(usdcID, dexeth.ProtocolVersionZero) if blockPollIntervalStr != "" { blockPollInterval, _ = time.ParseDuration(blockPollIntervalStr) @@ -94,14 +95,15 @@ func init() { const ( BipID = 60 - ethContractVersion = 1 - version = 1 + ethProtocolVersion = dexeth.ProtocolVersionZero ) var ( _ asset.Driver = (*Driver)(nil) _ asset.TokenBacker = (*ETHBackend)(nil) + ethContractVersion = ethProtocolVersion.ContractVersion() + backendInfo = &asset.BackendInfo{ SupportsDynamicTxFee: true, } @@ -117,13 +119,13 @@ var ( ) type driverBase struct { - unitInfo dex.UnitInfo - version uint32 + unitInfo dex.UnitInfo + protocolVer uint32 } // Version returns the Backend implementation's version number. func (d *driverBase) Version() uint32 { - return d.version + return d.protocolVer } // DecodeCoinID creates a human-readable representation of a coin ID for @@ -175,11 +177,13 @@ type ethFetcher interface { connect(ctx context.Context) error suggestGasTipCap(ctx context.Context) (*big.Int, error) transaction(ctx context.Context, hash common.Hash) (tx *types.Transaction, isMempool bool, err error) + transactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) // token- and asset-specific methods loadToken(ctx context.Context, assetID uint32) error - status(ctx context.Context, assetID uint32, vector *dexeth.SwapVector) (*dexeth.SwapStatus, error) + status(ctx context.Context, assetID uint32, locator []byte) (*dexeth.SwapStatus, error) + vector(ctx context.Context, assetID uint32, locator []byte) (*dexeth.SwapVector, error) + statusAndVector(ctx context.Context, assetID uint32, locator []byte) (*dexeth.SwapStatus, *dexeth.SwapVector, error) accountBalance(ctx context.Context, assetID uint32, addr common.Address) (*big.Int, error) - receipt(context.Context, common.Hash) (*types.Receipt, error) } type baseBackend struct { @@ -222,6 +226,7 @@ type AssetBackend struct { redeemSize uint64 contractAddr common.Address + contractVer uint32 } // ETHBackend implements some Ethereum-specific methods. @@ -251,7 +256,7 @@ func unconnectedETH(logger dex.Logger, net dex.Network) (*ETHBackend, error) { // change to support multiple contracts. contractAddr, exists := dexeth.ContractAddresses[ethContractVersion][net] if !exists || contractAddr == (common.Address{}) { - return nil, fmt.Errorf("no eth contract for version %d, net %s", ethContractVersion, net) + return nil, fmt.Errorf("no eth contract for version 0, net %s", net) } return ÐBackend{&AssetBackend{ baseBackend: &baseBackend{ @@ -266,6 +271,7 @@ func unconnectedETH(logger dex.Logger, net dex.Network) (*ETHBackend, error) { redeemSize: dexeth.RedeemGas(1, ethContractVersion), assetID: BipID, atomize: dexeth.WeiToGwei, + contractVer: ethContractVersion, }}, nil } @@ -424,6 +430,7 @@ func (eth *ETHBackend) TokenBackend(assetID uint32, configPath string) (asset.Ba redeemSize: gases.Redeem, contractAddr: swapContract.Address, atomize: token.EVMToAtomic, + contractVer: token.contractVer, }} eth.baseBackend.tokens[assetID] = be return be, nil @@ -542,13 +549,13 @@ func (be *AssetBackend) sendBlockUpdate(u *asset.BlockUpdate) { // ValidateContract ensures that contractData encodes both the expected contract // version and a secret hash. func (eth *ETHBackend) ValidateContract(contractData []byte) error { - ver, _, err := dexeth.DecodeLocator(contractData) + contractVer, _, err := dexeth.DecodeContractData(contractData) if err != nil { // ensures secretHash is proper length return err } - if ver != ethContractVersion { - return fmt.Errorf("incorrect swap contract version %d, wanted %d", ver, ethContractVersion) + if contractVer != ethContractVersion { + return fmt.Errorf("incorrect swap contract version %d, wanted %d", contractVer, ethContractVersion) } return nil } @@ -556,7 +563,7 @@ func (eth *ETHBackend) ValidateContract(contractData []byte) error { // ValidateContract ensures that contractData encodes both the expected swap // contract version and a secret hash. func (eth *TokenBackend) ValidateContract(contractData []byte) error { - ver, _, err := dexeth.DecodeLocator(contractData) + contractVer, _, err := dexeth.DecodeContractData(contractData) if err != nil { // ensures secretHash is proper length return err } @@ -565,8 +572,8 @@ func (eth *TokenBackend) ValidateContract(contractData []byte) error { if err != nil { return fmt.Errorf("error locating token: %v", err) } - if ver != token.ver { - return fmt.Errorf("incorrect token swap contract version %d, wanted %d", ver, token.ver) + if contractVer != token.contractVer { + return fmt.Errorf("incorrect token swap contract version %d, wanted %d", contractVer, token.contractVer) } return nil @@ -595,27 +602,18 @@ func (be *AssetBackend) Contract(coinID, contractData []byte) (*asset.Contract, ContractData: contractData, SecretHash: sc.vector.SecretHash[:], TxData: sc.serializedTx, - LockTime: time.Unix(int64(sc.vector.LockTime), 0), + LockTime: time.UnixMilli(int64(sc.vector.LockTime)), }, nil } // ValidateSecret checks that the secret satisfies the secret hash. func (eth *baseBackend) ValidateSecret(secret, contractData []byte) bool { - contractVer, locator, err := dexeth.DecodeLocator(contractData) - if err != nil { - eth.baseLogger.Errorf("unable to decode contract data: %w", err) - return false - } - if contractVer != ethContractVersion { - return false - } - vec, err := dexeth.ParseV1Locator(locator) + secretHash, err := dexeth.DecodeContractDataV0(contractData) if err != nil { - eth.baseLogger.Errorf("unable to parse v1 locator: %w", err) return false } sh := sha256.Sum256(secret) - return bytes.Equal(sh[:], vec.SecretHash[:]) + return bytes.Equal(sh[:], secretHash[:]) } // Synced is true if the blockchain is ready for action. @@ -763,29 +761,3 @@ func (eth *ETHBackend) run(ctx context.Context) { } } } - -func (eth *baseBackend) txConfirmations(ctx context.Context, txHash common.Hash) (int64, error) { - r, err := eth.node.receipt(ctx, txHash) - if err != nil { - // Could be mempool. - if _, isMempool, err2 := eth.node.transaction(ctx, txHash); err2 != nil { - if errors.Is(err2, ethereum.NotFound) { - return 0, asset.CoinNotFoundError - } - return 0, fmt.Errorf("errors encountered searching for transaction: %v, %v", err, err2) - } else if isMempool { - return 0, nil - } - return 0, err - } - - if r.BlockNumber == nil || r.BlockNumber.Int64() <= 0 { - return 0, nil - } - - bn, err := eth.node.blockNumber(ctx) - if err != nil { - return 0, fmt.Errorf("unable to fetch block number: %v", err) - } - return int64(bn) - r.BlockNumber.Int64() + 1, nil -} diff --git a/server/asset/eth/eth_test.go b/server/asset/eth/eth_test.go index a85699ac8b..b8eabbdb8b 100644 --- a/server/asset/eth/eth_test.go +++ b/server/asset/eth/eth_test.go @@ -8,6 +8,7 @@ import ( "bytes" "context" "crypto/sha256" + "encoding/binary" "encoding/hex" "errors" "math/big" @@ -20,7 +21,6 @@ import ( "decred.org/dcrdex/dex/calc" "decred.org/dcrdex/dex/encode" dexeth "decred.org/dcrdex/dex/networks/eth" - swapv1 "decred.org/dcrdex/dex/networks/eth/contracts/v1" "decred.org/dcrdex/server/asset" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" @@ -30,69 +30,56 @@ import ( const initLocktime = 1632112916 var ( - _ ethFetcher = (*testNode)(nil) - tLogger = dex.StdOutLogger("ETHTEST", dex.LevelTrace) - tCtx context.Context - tSwap1 = &swapv1.ETHSwapVector{ - SecretHash: bytesToArray([]byte("1")), - Initiator: common.BytesToAddress([]byte("initiator1")), - RefundTimestamp: 1, - Participant: common.BytesToAddress([]byte("participant1")), - Value: 1, - } - tSwap2 = &swapv1.ETHSwapVector{ - SecretHash: bytesToArray([]byte("2")), - Initiator: common.BytesToAddress([]byte("initiator2")), - RefundTimestamp: 2, - Participant: common.BytesToAddress([]byte("participant2")), - Value: 2, - } - // redeemCalldata encodes [tSwap1, tSwap2] - initCalldata = mustParseHex("64a97bff00000000000000000000000000000000000000" + - "00000000000000000000000020000000000000000000000000000000000000000000000000" + - "00000000000000023100000000000000000000000000000000000000000000000000000000" + - "00000000000000000000000000000000000000000000000000696e69746961746f72310000" + - "00000000000000000000000000000000000000000000000000000000000100000000000000" + - "000000000000000000000000007061727469636970616e7431000000000000000000000000" + - "00000000000000000000000000000000000000013200000000000000000000000000000000" + - "00000000000000000000000000000000000000000000000000000000000000000000000000" + - "696e69746961746f7232000000000000000000000000000000000000000000000000000000" + - "000000000200000000000000000000000000000000000000007061727469636970616e7432" + - "0000000000000000000000000000000000000000000000000000000000000002") + _ ethFetcher = (*testNode)(nil) + tLogger = dex.StdOutLogger("ETHTEST", dex.LevelTrace) + tCtx context.Context + initCalldata = mustParseHex("a8793f94000000000000000000000000000" + + "0000000000000000000000000000000000020000000000000000000000000000000000" + + "0000000000000000000000000000002000000000000000000000000000000000000000" + + "00000000000000000614811148b3e4acc53b664f9cf6fcac0adcd328e95d62ba1f4379" + + "650ae3e1460a0f9d1a1000000000000000000000000345853e21b1d475582e71cc2691" + + "24ed5e2dd342200000000000000000000000000000000000000000000000022b1c8c12" + + "27a0000000000000000000000000000000000000000000000000000000000006148111" + + "4ebdc4c31b88d0c8f4d644591a8e00e92b607f920ad8050deb7c7469767d9c56100000" + + "0000000000000000000345853e21b1d475582e71cc269124ed5e2dd342200000000000" + + "000000000000000000000000000000000000022b1c8c1227a0000") + /* initCallData parses to: + [ETHSwapInitiation { + RefundTimestamp: 1632112916 + SecretHash: 8b3e4acc53b664f9cf6fcac0adcd328e95d62ba1f4379650ae3e1460a0f9d1a1 + Value: 5e9 gwei + Participant: 0x345853e21b1d475582e71cc269124ed5e2dd3422 + }, + ETHSwapInitiation { + RefundTimestamp: 1632112916 + SecretHash: ebdc4c31b88d0c8f4d644591a8e00e92b607f920ad8050deb7c7469767d9c561 + Value: 5e9 gwei + Participant: 0x345853e21b1d475582e71cc269124ed5e2dd3422 + }] + */ initSecretHashA = mustParseHex("8b3e4acc53b664f9cf6fcac0adcd328e95d62ba1f4379650ae3e1460a0f9d1a1") initSecretHashB = mustParseHex("ebdc4c31b88d0c8f4d644591a8e00e92b607f920ad8050deb7c7469767d9c561") initParticipantAddr = common.HexToAddress("345853e21b1d475582E71cC269124eD5e2dD3422") - - tRedeem1 = &swapv1.ETHSwapRedemption{ - V: *tSwap1, - Secret: bytesToArray([]byte("1")), - } - tRedeem2 = &swapv1.ETHSwapRedemption{ - V: *tSwap2, - Secret: bytesToArray([]byte("2")), - } - vector2 = &dexeth.SwapVector{ - From: tRedeem2.V.Initiator, - To: tRedeem2.V.Participant, - Value: tRedeem2.V.Value, - SecretHash: tRedeem2.V.SecretHash, - LockTime: tRedeem2.V.RefundTimestamp, - } - // redeemCalldata encodes [tRedeem1, tRedeem2] - redeemCalldata = mustParseHex("428b16e100000000000000000000000000000000000" + - "0000000000000000000000000002000000000000000000000000000000000000000000" + - "0000000000000000000000231000000000000000000000000000000000000000000000" + - "0000000000000000000000000000000000000000000000000000000000000696e69746" + - "961746f723100000000000000000000000000000000000000000000000000000000000" + - "0000100000000000000000000000000000000000000007061727469636970616e74310" + - "0000000000000000000000000000000000000000000000000000000000000013100000" + - "0000000000000000000000000000000000000000000000000000000003200000000000" + - "0000000000000000000000000000000000000000000000000000000000000000000000" + - "0000000000000000000000000696e69746961746f72320000000000000000000000000" + - "0000000000000000000000000000000000000020000000000000000000000000000000" + - "0000000007061727469636970616e74320000000000000000000000000000000000000" + - "0000000000000000000000000023200000000000000000000000000000000000000000" + - "000000000000000000000") + redeemCalldata = mustParseHex("f4fd17f90000000000000000000000000000000000000" + + "000000000000000000000000020000000000000000000000000000000000000000000000000" + + "00000000000000022c0a304c9321402dc11cbb5898b9f2af3029ce1c76ec6702c4cd5bb965f" + + "d3e7399d971975c09331eb00f5e0dc1eaeca9bf4ee2d086d3fe1de489f920007d654687eac0" + + "9638c0c38b4e735b79f053cb869167ee770640ac5df5c4ab030813122aebdc4c31b88d0c8f4" + + "d644591a8e00e92b607f920ad8050deb7c7469767d9c561") + /* + redeemCallData parses to: + [ETHSwapRedemption { + SecretHash: 99d971975c09331eb00f5e0dc1eaeca9bf4ee2d086d3fe1de489f920007d6546 + Secret: 2c0a304c9321402dc11cbb5898b9f2af3029ce1c76ec6702c4cd5bb965fd3e73 + } + ETHSwapRedemption { + SecretHash: ebdc4c31b88d0c8f4d644591a8e00e92b607f920ad8050deb7c7469767d9c561 + Secret: 87eac09638c0c38b4e735b79f053cb869167ee770640ac5df5c4ab030813122a + }] + */ + redeemSecretHashA = mustParseHex("99d971975c09331eb00f5e0dc1eaeca9bf4ee2d086d3fe1de489f920007d6546") + redeemSecretHashB = mustParseHex("ebdc4c31b88d0c8f4d644591a8e00e92b607f920ad8050deb7c7469767d9c561") + redeemSecretB = mustParseHex("87eac09638c0c38b4e735b79f053cb869167ee770640ac5df5c4ab030813122a") ) func mustParseHex(s string) []byte { @@ -103,11 +90,6 @@ func mustParseHex(s string) []byte { return b } -func bytesToArray(b []byte) (a [32]byte) { - copy(a[:], b) - return -} - type testNode struct { connectErr error bestHdr *types.Header @@ -125,8 +107,6 @@ type testNode struct { tx *types.Transaction txIsMempool bool txErr error - rcpt *types.Receipt - rcptErr error acctBal *big.Int acctBalErr error } @@ -161,24 +141,51 @@ func (n *testNode) suggestGasTipCap(ctx context.Context) (*big.Int, error) { return n.suggGasTipCap, n.suggGasTipCapErr } -func (n *testNode) status(ctx context.Context, assetID uint32, vector *dexeth.SwapVector) (*dexeth.SwapStatus, error) { - // return n.swp.State, n.swp.Secret, uint32(n.swp.BlockHeight), n.swpErr - if n.swpErr != nil { - return nil, n.swpErr +func (n *testNode) status(ctx context.Context, assetID uint32, locator []byte) (*dexeth.SwapStatus, error) { + if n.swp != nil { + return &dexeth.SwapStatus{ + BlockHeight: n.swp.BlockHeight, + Secret: n.swp.Secret, + Step: n.swp.State, + }, n.swpErr } - return &dexeth.SwapStatus{ - BlockHeight: n.swp.BlockHeight, - Secret: n.swp.Secret, - Step: n.swp.State, - }, nil + return nil, n.swpErr +} + +func (n *testNode) vector(ctx context.Context, assetID uint32, locator []byte) (*dexeth.SwapVector, error) { + var secretHash [32]byte + switch len(locator) { + case dexeth.LocatorV1Length: + vec, _ := dexeth.ParseV1Locator(locator) + secretHash = vec.SecretHash + default: + copy(secretHash[:], locator) + } + + if n.swp != nil { + return &dexeth.SwapVector{ + From: n.swp.Initiator, + To: n.swp.Participant, + Value: dexeth.WeiToGwei(n.swp.Value), + SecretHash: secretHash, + LockTime: uint64(n.swp.LockTime.UnixMilli()), + }, n.swpErr + } + return nil, n.swpErr +} + +func (n *testNode) statusAndVector(ctx context.Context, assetID uint32, locator []byte) (*dexeth.SwapStatus, *dexeth.SwapVector, error) { + status, _ := n.status(ctx, assetID, locator) + vec, _ := n.vector(ctx, assetID, locator) + return status, vec, n.swpErr } -func (n *testNode) transaction(ctx context.Context, hash common.Hash) (tx *types.Transaction, isMempool bool, err error) { +func (n *testNode) transaction(ctx context.Context, txHash common.Hash) (tx *types.Transaction, isMempool bool, err error) { return n.tx, n.txIsMempool, n.txErr } -func (n *testNode) receipt(context.Context, common.Hash) (*types.Receipt, error) { - return n.rcpt, n.rcptErr +func (n *testNode) transactionReceipt(ctx context.Context, txHash common.Hash) (tx *types.Receipt, err error) { + return nil, nil } func (n *testNode) accountBalance(ctx context.Context, assetID uint32, addr common.Address) (*big.Int, error) { @@ -217,8 +224,7 @@ func TestMain(m *testing.M) { tCtx, shutdown = context.WithCancel(context.Background()) doIt := func() int { defer shutdown() - dexeth.Tokens[testTokenID].NetTokens[dex.Simnet].SwapContracts[ethContractVersion].Address = common.BytesToAddress(encode.RandomBytes(20)) - dexeth.ContractAddresses[ethContractVersion][dex.Simnet] = common.BytesToAddress(encode.RandomBytes(20)) + dexeth.Tokens[testTokenID].NetTokens[dex.Simnet].SwapContracts[0].Address = common.BytesToAddress(encode.RandomBytes(20)) return m.Run() } os.Exit(doIt()) @@ -464,10 +470,11 @@ func TestContract(t *testing.T) { copy(txHash[:], encode.RandomBytes(32)) const gasPrice = 30 const gasTipCap = 2 - swapVal := tSwap2.Value - txVal := tSwap1.Value + tSwap2.Value - locator2 := vector2.Locator() - + const swapVal = 25e8 + const txVal = 5e9 + var secret, secretHash [32]byte + copy(secret[:], redeemSecretB) + copy(secretHash[:], redeemSecretHashB) tests := []struct { name string coinID []byte @@ -479,20 +486,20 @@ func TestContract(t *testing.T) { }{{ name: "ok", tx: tTx(gasPrice, gasTipCap, txVal, contractAddr, initCalldata), - contract: dexeth.EncodeContractData(1, locator2), - swap: tSwap(97, initLocktime, swapVal, tRedeem2.Secret, dexeth.SSInitiated, &initParticipantAddr), + contract: dexeth.EncodeContractData(0, secretHash[:]), + swap: tSwap(97, initLocktime, swapVal, secret, dexeth.SSInitiated, &initParticipantAddr), coinID: txHash[:], }, { name: "new coiner error, wrong tx type", tx: tTx(gasPrice, gasTipCap, txVal, contractAddr, initCalldata), - contract: dexeth.EncodeContractData(1, locator2), - swap: tSwap(97, initLocktime, swapVal, tRedeem2.Secret, dexeth.SSInitiated, &initParticipantAddr), + contract: dexeth.EncodeContractData(0, secretHash[:]), + swap: tSwap(97, initLocktime, swapVal, secret, dexeth.SSInitiated, &initParticipantAddr), coinID: txHash[1:], wantErr: true, }, { name: "confirmations error, swap error", tx: tTx(gasPrice, gasTipCap, txVal, contractAddr, initCalldata), - contract: dexeth.EncodeContractData(1, locator2), + contract: dexeth.EncodeContractData(0, secretHash[:]), coinID: txHash[:], swapErr: errors.New(""), wantErr: true, @@ -505,7 +512,7 @@ func TestContract(t *testing.T) { node.swpErr = test.swapErr eth.contractAddr = *contractAddr - contractData := dexeth.EncodeContractData(1, locator2) // matches initCalldata + contractData := dexeth.EncodeContractData(0, secretHash[:]) // matches initCalldata contract, err := eth.Contract(test.coinID, contractData) if test.wantErr { if err == nil { @@ -516,8 +523,8 @@ func TestContract(t *testing.T) { if err != nil { t.Fatalf("unexpected error for test %q: %v", test.name, err) } - if contract.SwapAddress != tRedeem2.V.Participant.String() || - contract.LockTime.Unix() != int64(tRedeem2.V.RefundTimestamp) { + if contract.SwapAddress != initParticipantAddr.String() || + contract.LockTime.Unix() != initLocktime { t.Fatalf("returns do not match expected for test %q", test.name) } } @@ -552,23 +559,20 @@ func TestValidateFeeRate(t *testing.T) { } func TestValidateSecret(t *testing.T) { - secret := bytesToArray(encode.RandomBytes(32)) + secret, blankHash := [32]byte{}, [32]byte{} + copy(secret[:], encode.RandomBytes(32)) secretHash := sha256.Sum256(secret[:]) - rightVec := &dexeth.SwapVector{SecretHash: secretHash} - wrongVec := &dexeth.SwapVector{SecretHash: bytesToArray(encode.RandomBytes(32))} - tests := []struct { name string - secret [32]byte contractData []byte want bool }{{ name: "ok", - contractData: dexeth.EncodeContractData(1, rightVec.Locator()), + contractData: dexeth.EncodeContractData(0, secretHash[:]), want: true, }, { name: "not the right hash", - contractData: dexeth.EncodeContractData(1, wrongVec.Locator()), + contractData: dexeth.EncodeContractData(0, blankHash[:]), }, { name: "bad contract data", }} @@ -585,41 +589,43 @@ func TestRedemption(t *testing.T) { receiverAddr, contractAddr := new(common.Address), new(common.Address) copy(receiverAddr[:], encode.RandomBytes(20)) copy(contractAddr[:], encode.RandomBytes(20)) - txHash := bytesToArray(encode.RandomBytes(32)) - secret := tRedeem2.Secret - locator := vector2.Locator() + var secret, secretHash, txHash [32]byte + copy(secret[:], redeemSecretB) + copy(secretHash[:], redeemSecretHashB) + copy(txHash[:], encode.RandomBytes(32)) const gasPrice = 30 const gasTipCap = 2 - goodContract := dexeth.EncodeContractData(1, locator) tests := []struct { name string coinID, contractID []byte swp *dexeth.SwapState tx *types.Transaction + txIsMempool bool + swpErr, txErr error wantErr bool }{{ name: "ok", tx: tTx(gasPrice, gasTipCap, 0, contractAddr, redeemCalldata), - contractID: goodContract, + contractID: dexeth.EncodeContractData(0, secretHash[:]), coinID: txHash[:], swp: tSwap(0, 0, 0, secret, dexeth.SSRedeemed, receiverAddr), }, { name: "new coiner error, wrong tx type", tx: tTx(gasPrice, gasTipCap, 0, contractAddr, redeemCalldata), - contractID: goodContract, + contractID: dexeth.EncodeContractData(0, secretHash[:]), coinID: txHash[1:], wantErr: true, }, { name: "confirmations error, swap wrong state", tx: tTx(gasPrice, gasTipCap, 0, contractAddr, redeemCalldata), - contractID: goodContract, + contractID: dexeth.EncodeContractData(0, secretHash[:]), swp: tSwap(0, 0, 0, secret, dexeth.SSRefunded, receiverAddr), coinID: txHash[:], wantErr: true, }, { - name: "bad contract data", + name: "validate redeem error", tx: tTx(gasPrice, gasTipCap, 0, contractAddr, redeemCalldata), - contractID: goodContract[:len(goodContract)-1], + contractID: secretHash[:31], coinID: txHash[:], swp: tSwap(0, 0, 0, secret, dexeth.SSRedeemed, receiverAddr), wantErr: true, @@ -627,8 +633,10 @@ func TestRedemption(t *testing.T) { for _, test := range tests { eth, node := tNewBackend(BipID) node.tx = test.tx - node.rcpt = &types.Receipt{BlockNumber: new(big.Int)} + node.txIsMempool = test.txIsMempool + node.txErr = test.txErr node.swp = test.swp + node.swpErr = test.swpErr eth.contractAddr = *contractAddr _, err := eth.Redemption(test.coinID, nil, test.contractID) @@ -700,26 +708,23 @@ func TestValidateContract(t *testing.T) { } func testValidateContract(t *testing.T, assetID uint32) { - locator := vector2.Locator() tests := []struct { - name string - ver uint32 - locator []byte - wantErr bool + name string + ver uint32 + secretHash []byte + wantErr bool }{{ - name: "ok", - ver: 1, - locator: locator, + name: "ok", + secretHash: make([]byte, dexeth.SecretHashSize), }, { - name: "wrong size", - ver: 1, - locator: locator[:len(locator)-1], - wantErr: true, + name: "wrong size", + secretHash: make([]byte, dexeth.SecretHashSize-1), + wantErr: true, }, { - name: "wrong version", - ver: 0, - locator: locator, - wantErr: true, + name: "wrong version", + ver: 1, + secretHash: make([]byte, dexeth.SecretHashSize), + wantErr: true, }} type contractValidator interface { @@ -735,7 +740,10 @@ func testValidateContract(t *testing.T, assetID uint32) { cv = &TokenBackend{eth} } - swapData := dexeth.EncodeContractData(test.ver, test.locator) + swapData := make([]byte, 4+len(test.secretHash)) + binary.BigEndian.PutUint32(swapData[:4], test.ver) + copy(swapData[4:], test.secretHash) + err := cv.ValidateContract(swapData) if test.wantErr { if err == nil { diff --git a/server/asset/eth/rpcclient.go b/server/asset/eth/rpcclient.go index 1efce87f62..1c2b02e05e 100644 --- a/server/asset/eth/rpcclient.go +++ b/server/asset/eth/rpcclient.go @@ -15,6 +15,7 @@ import ( "decred.org/dcrdex/dex" dexeth "decred.org/dcrdex/dex/networks/eth" + swapv0 "decred.org/dcrdex/dex/networks/eth/contracts/v0" swapv1 "decred.org/dcrdex/dex/networks/eth/contracts/v1" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" @@ -142,11 +143,25 @@ func (c *rpcclient) connectToEndpoint(ctx context.Context, endpoint endpoint) (* ec.txPoolSupported = true } - es, err := swapv1.NewETHSwap(c.ethContractAddr, ec.Client) - if err != nil { - return nil, fmt.Errorf("unable to initialize eth contract for %q: %v", endpoint, err) + var sc swapContract + switch ethContractVersion { + case 0: + es, err := swapv0.NewETHSwap(c.ethContractAddr, ec.Client) + if err != nil { + return nil, err + } + sc = &swapSourceV0{es} + case 1: + es, err := swapv1.NewETHSwap(c.ethContractAddr, ec.Client) + if err != nil { + return nil, err + } + sc = &swapSourceV1{es} + default: + return nil, fmt.Errorf("unknown eth contract version %d", ethContractVersion) } - ec.swapContract = &swapSourceV1{es} + + ec.swapContract = sc for assetID := range c.tokensLoaded { tkn, err := newTokener(ctx, assetID, c.net, ec.Client) @@ -402,7 +417,17 @@ func (c *rpcclient) withTokener(assetID uint32, f func(*tokener) error) error { } return f(tkn) }) +} +func (c *rpcclient) withSwapContract(assetID uint32, f func(swapContract) error) error { + if assetID == BipID { + return c.withClient(func(ec *ethConn) error { + return f(ec.swapContract) + }) + } + return c.withTokener(assetID, func(tkn *tokener) error { + return f(tkn) + }) } // bestHeader gets the best header at the time of calling. @@ -438,20 +463,25 @@ func (c *rpcclient) blockNumber(ctx context.Context) (bn uint64, err error) { }) } -// swap gets a swap keyed by secretHash in the contract. -func (c *rpcclient) status(ctx context.Context, assetID uint32, vector *dexeth.SwapVector) (status *dexeth.SwapStatus, err error) { - if assetID == BipID { - err = c.withClient(func(ec *ethConn) error { - status, err = ec.swapContract.Status(ctx, vector) - return err - }) - } else { - err = c.withTokener(assetID, func(tkn *tokener) error { - status, err = tkn.Status(ctx, vector) - return err - }) - } - return +func (c *rpcclient) status(ctx context.Context, assetID uint32, locator []byte) (status *dexeth.SwapStatus, err error) { + return status, c.withSwapContract(assetID, func(sc swapContract) error { + status, err = sc.status(ctx, locator) + return err + }) +} + +func (c *rpcclient) vector(ctx context.Context, assetID uint32, locator []byte) (vec *dexeth.SwapVector, err error) { + return vec, c.withSwapContract(assetID, func(sc swapContract) error { + vec, err = sc.vector(ctx, locator) + return err + }) +} + +func (c *rpcclient) statusAndVector(ctx context.Context, assetID uint32, locator []byte) (status *dexeth.SwapStatus, vec *dexeth.SwapVector, err error) { + return status, vec, c.withSwapContract(assetID, func(sc swapContract) error { + status, vec, err = sc.statusAndVector(ctx, locator) + return err + }) } // transaction gets the transaction that hashes to hash from the chain or @@ -463,6 +493,17 @@ func (c *rpcclient) transaction(ctx context.Context, hash common.Hash) (tx *type }, true) // stop on first provider with "not found", because this should be an error if tx does not exist } +func (c *rpcclient) transactionReceipt(ctx context.Context, txHash common.Hash) (r *types.Receipt, err error) { + return r, c.withClient(func(ec *ethConn) error { + r, err = ec.TransactionReceipt(ctx, txHash) + return err + }) +} + +func isNotFoundError(err error) bool { + return strings.Contains(err.Error(), "not found") +} + // dumbBalance gets the account balance, ignoring the effects of unmined // transactions. func (c *rpcclient) dumbBalance(ctx context.Context, ec *ethConn, assetID uint32, addr common.Address) (bal *big.Int, err error) { @@ -477,7 +518,6 @@ func (c *rpcclient) dumbBalance(ctx context.Context, ec *ethConn, assetID uint32 } // smartBalance gets the account balance, including the effects of known -// accountBalance gets the account balance, including the effects of known // unmined transactions. func (c *rpcclient) smartBalance(ctx context.Context, ec *ethConn, assetID uint32, addr common.Address) (bal *big.Int, err error) { tip, err := c.blockNumber(ctx) @@ -550,14 +590,6 @@ func (c *rpcclient) smartBalance(ctx context.Context, ec *ethConn, assetID uint3 return bal, nil } -// receipt fetches the transaction receipt. -func (c *rpcclient) receipt(ctx context.Context, txHash common.Hash) (r *types.Receipt, err error) { - return r, c.withClient(func(ec *ethConn) error { - r, err = ec.TransactionReceipt(ctx, txHash) - return err - }) -} - // accountBalance gets the account balance. If txPool functions are supported by the // client, it will include the effects of unmined transactions, otherwise it will not. func (c *rpcclient) accountBalance(ctx context.Context, assetID uint32, addr common.Address) (bal *big.Int, err error) { diff --git a/server/asset/eth/rpcclient_harness_test.go b/server/asset/eth/rpcclient_harness_test.go index f9c1890d12..5850cf18a2 100644 --- a/server/asset/eth/rpcclient_harness_test.go +++ b/server/asset/eth/rpcclient_harness_test.go @@ -42,7 +42,6 @@ func TestMain(m *testing.M) { monitorConnectionsInterval = 3 * time.Second // Run in function so that defers happen before os.Exit is called. - dexeth.MaybeReadSimnetAddrs() run := func() (int, error) { var cancel context.CancelFunc ctx, cancel = context.WithCancel(context.Background()) @@ -112,7 +111,9 @@ func TestSuggestGasTipCap(t *testing.T) { } func TestStatus(t *testing.T) { - _, err := ethClient.status(ctx, BipID, &dexeth.SwapVector{}) + var secretHash [32]byte + copy(secretHash[:], encode.RandomBytes(32)) + _, err := ethClient.status(ctx, BipID, secretHash[:]) if err != nil { t.Fatal(err) } diff --git a/server/asset/eth/tokener.go b/server/asset/eth/tokener.go index 5fb0f60c01..ff325b36c4 100644 --- a/server/asset/eth/tokener.go +++ b/server/asset/eth/tokener.go @@ -10,8 +10,10 @@ import ( "decred.org/dcrdex/dex" "decred.org/dcrdex/dex/networks/erc20" + erc20v0 "decred.org/dcrdex/dex/networks/erc20/contracts/v0" erc20v1 "decred.org/dcrdex/dex/networks/erc20/contracts/v1" dexeth "decred.org/dcrdex/dex/networks/eth" + swapv0 "decred.org/dcrdex/dex/networks/eth/contracts/v0" swapv1 "decred.org/dcrdex/dex/networks/eth/contracts/v1" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" @@ -19,7 +21,9 @@ import ( // swapContract is a generic source of swap contract data. type swapContract interface { - Status(context.Context, *dexeth.SwapVector) (*dexeth.SwapStatus, error) + status(ctx context.Context, locator []byte) (*dexeth.SwapStatus, error) + vector(ctx context.Context, locator []byte) (*dexeth.SwapVector, error) + statusAndVector(ctx context.Context, locator []byte) (*dexeth.SwapStatus, *dexeth.SwapVector, error) } // erc2Contract exposes methods of a token's ERC20 contract. @@ -37,18 +41,33 @@ type tokener struct { // newTokener is a constructor for a tokener. func newTokener(ctx context.Context, assetID uint32, net dex.Network, be bind.ContractBackend) (*tokener, error) { - token, netToken, swapContract, err := networkToken(assetID, net) + token, netToken, contract, err := networkToken(assetID, net) if err != nil { return nil, err } - if token.ver != version { - return nil, fmt.Errorf("wrong contract version. wanted %d, got %d", version, token.ver) + var tokenAddresser interface { + TokenAddress(opts *bind.CallOpts) (common.Address, error) } - es, err := erc20v1.NewERC20Swap(swapContract.Address, be) - if err != nil { - return nil, err + var sc swapContract + switch token.contractVer { + case 0: + es, err := erc20v0.NewERC20Swap(contract.Address, be) + if err != nil { + return nil, err + } + sc = &swapSourceV0{es} + tokenAddresser = es + case 1: + es, err := erc20v1.NewERC20Swap(contract.Address, be) + if err != nil { + return nil, err + } + sc = &swapSourceV1{es} + tokenAddresser = es + default: + return nil, fmt.Errorf("unsupported contract version %d", token.contractVer) } erc20, err := erc20.NewIERC20(netToken.Address, be) @@ -56,22 +75,22 @@ func newTokener(ctx context.Context, assetID uint32, net dex.Network, be bind.Co return nil, err } - boundAddr, err := es.TokenAddress(readOnlyCallOpts(ctx, false)) + boundAddr, err := tokenAddresser.TokenAddress(readOnlyCallOpts(ctx, false)) if err != nil { return nil, fmt.Errorf("error retrieving bound address for %s version %d contract: %w", - token.Name, token.ver, err) + token.Name, token.contractVer, err) } if boundAddr != netToken.Address { return nil, fmt.Errorf("wrong bound address for %s version %d contract. wanted %s, got %s", - token.Name, token.ver, netToken.Address, boundAddr) + token.Name, token.contractVer, netToken.Address, boundAddr) } tkn := &tokener{ registeredToken: token, - swapContract: &swapSourceV1{es}, + swapContract: sc, erc20Contract: erc20, - contractAddr: swapContract.Address, + contractAddr: contract.Address, tokenAddr: netToken.Address, } @@ -90,15 +109,15 @@ func (t *tokener) transferred(txData []byte) *big.Int { // swapped calculates the value sent to the swap contracts initiate method. func (t *tokener) swapped(txData []byte) *big.Int { - vectors, err := dexeth.ParseInitiateDataV1(txData) + inits, err := dexeth.ParseInitiateDataV0(txData) if err != nil { return nil } - var v uint64 - for _, vector := range vectors { - v += vector.Value + v := new(big.Int) + for _, init := range inits { + v.Add(v, init.Value) } - return dexeth.GweiToWei(v) + return v } // balanceOf checks the account's token balance. @@ -106,29 +125,154 @@ func (t *tokener) balanceOf(ctx context.Context, addr common.Address) (*big.Int, return t.BalanceOf(readOnlyCallOpts(ctx, false), addr) } -// swapContractV1 represents a version 0 swap contract for ETH or a token. +// swapContractV0 represents a version 0 swap contract for ETH or a token. +type swapContractV0 interface { + Swap(opts *bind.CallOpts, secretHash [32]byte) (swapv0.ETHSwapSwap, error) +} + +// swapSourceV0 wraps a swapContractV0 and translates the swap data to satisfy +// swapSource. +type swapSourceV0 struct { + contract swapContractV0 // *swapv0.ETHSwap or *erc20v0.ERCSwap +} + +// swap get the swap state for the secretHash on the version 0 contract. +func (s *swapSourceV0) swap(ctx context.Context, secretHash [32]byte) (*dexeth.SwapState, error) { + state, err := s.contract.Swap(readOnlyCallOpts(ctx, true), secretHash) + if err != nil { + return nil, fmt.Errorf("swap error: %w", err) + } + return dexeth.SwapStateFromV0(&state), nil +} + +// status fetches the SwapStatus, which specifies the current state of mutable +// swap data. +func (s *swapSourceV0) status(ctx context.Context, locator []byte) (*dexeth.SwapStatus, error) { + secretHash, err := dexeth.ParseV0Locator(locator) + if err != nil { + return nil, err + } + swap, err := s.swap(ctx, secretHash) + if err != nil { + return nil, err + } + status := &dexeth.SwapStatus{ + Step: swap.State, + Secret: swap.Secret, + BlockHeight: swap.BlockHeight, + } + return status, nil +} + +// vector generates a SwapVector, containing the immutable data that defines +// the swap. +func (s *swapSourceV0) vector(ctx context.Context, locator []byte) (*dexeth.SwapVector, error) { + secretHash, err := dexeth.ParseV0Locator(locator) + if err != nil { + return nil, err + } + swap, err := s.swap(ctx, secretHash) + if err != nil { + return nil, err + } + vector := &dexeth.SwapVector{ + From: swap.Participant, + To: swap.Initiator, + Value: dexeth.WeiToGwei(swap.Value), + SecretHash: secretHash, + LockTime: uint64(swap.LockTime.UnixMilli()), + } + return vector, nil +} + +// statusAndVector generates both the status and the vector simultaneously. For +// version 0, this is better than calling status and vector separately, since +// each makes an identical call to c.swap. +func (s *swapSourceV0) statusAndVector(ctx context.Context, locator []byte) (*dexeth.SwapStatus, *dexeth.SwapVector, error) { + secretHash, err := dexeth.ParseV0Locator(locator) + if err != nil { + return nil, nil, err + } + swap, err := s.swap(ctx, secretHash) + if err != nil { + return nil, nil, err + } + vector := &dexeth.SwapVector{ + From: swap.Participant, + To: swap.Initiator, + Value: dexeth.WeiToGwei(swap.Value), + SecretHash: secretHash, + LockTime: uint64(swap.LockTime.UnixMilli()), + } + status := &dexeth.SwapStatus{ + Step: swap.State, + Secret: swap.Secret, + BlockHeight: swap.BlockHeight, + } + return status, vector, nil +} + type swapContractV1 interface { Status(opts *bind.CallOpts, c swapv1.ETHSwapVector) (swapv1.ETHSwapStatus, error) } -// swapSourceV1 wraps a swapContractV0 and translates the swap data to satisfy -// swapSource. type swapSourceV1 struct { contract swapContractV1 // *swapv0.ETHSwap or *erc20v0.ERCSwap } -// Swap translates the version 0 swap data to the more general SwapState to -// satisfy the swapSource interface. -func (s *swapSourceV1) Status(ctx context.Context, vector *dexeth.SwapVector) (*dexeth.SwapStatus, error) { - rec, err := s.contract.Status(readOnlyCallOpts(ctx, true), dexeth.SwapVectorToAbigen(vector)) +func (s *swapSourceV1) status(ctx context.Context, locator []byte) (*dexeth.SwapStatus, error) { + v, err := dexeth.ParseV1Locator(locator) if err != nil { - return nil, fmt.Errorf("Swap error: %w", err) + return nil, err + } + rec, err := s.contract.Status(readOnlyCallOpts(ctx, true), dexeth.SwapVectorToAbigen(v)) + if err != nil { + return nil, err } return &dexeth.SwapStatus{ - BlockHeight: rec.BlockNumber.Uint64(), + Step: dexeth.SwapStep(rec.Step), Secret: rec.Secret, + BlockHeight: rec.BlockNumber.Uint64(), + }, err +} + +func (s *swapSourceV1) vector(ctx context.Context, locator []byte) (*dexeth.SwapVector, error) { + return dexeth.ParseV1Locator(locator) +} + +func (s *swapSourceV1) statusAndVector(ctx context.Context, locator []byte) (*dexeth.SwapStatus, *dexeth.SwapVector, error) { + v, err := dexeth.ParseV1Locator(locator) + if err != nil { + return nil, nil, err + } + + rec, err := s.contract.Status(readOnlyCallOpts(ctx, true), dexeth.SwapVectorToAbigen(v)) + if err != nil { + return nil, nil, err + } + return &dexeth.SwapStatus{ Step: dexeth.SwapStep(rec.Step), - }, nil + Secret: rec.Secret, + BlockHeight: rec.BlockNumber.Uint64(), + }, v, err +} + +func (s *swapSourceV1) Status(ctx context.Context, locator []byte) (*dexeth.SwapStatus, error) { + vec, err := dexeth.ParseV1Locator(locator) + if err != nil { + return nil, err + } + + status, err := s.contract.Status(readOnlyCallOpts(ctx, true), dexeth.SwapVectorToAbigen(vec)) + if err != nil { + return nil, err + } + + return &dexeth.SwapStatus{ + Step: dexeth.SwapStep(status.Step), + Secret: status.Secret, + BlockHeight: status.BlockNumber.Uint64(), + }, err } // readOnlyCallOpts is the CallOpts used for read-only contract method calls.