From cd4866c930accf92d9eed312b87afba99c01ae20 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 | 11 +- client/asset/eth/eth.go | 69 ++--- client/asset/eth/eth_test.go | 41 +-- client/asset/eth/nodeclient.go | 6 +- client/asset/eth/nodeclient_harness_test.go | 152 +++++------ dex/networks/eth/contracts/ETHSwapV1.sol | 4 + dex/networks/eth/params.go | 65 ++++- dex/networks/eth/tokens.go | 8 - dex/networks/polygon/params.go | 14 +- server/asset/eth/coiner.go | 236 ++++++++++++----- server/asset/eth/coiner_test.go | 157 +++++++----- server/asset/eth/eth.go | 124 ++++----- server/asset/eth/eth_test.go | 268 ++++++++++---------- server/asset/eth/rpcclient.go | 93 ++++--- server/asset/eth/rpcclient_harness_test.go | 5 +- server/asset/eth/tokener.go | 199 +++++++++++++-- server/asset/polygon/polygon.go | 39 +-- 17 files changed, 904 insertions(+), 587 deletions(-) diff --git a/client/asset/eth/contractor.go b/client/asset/eth/contractor.go index e8f6a1b9fd..d7ea333580 100644 --- a/client/asset/eth/contractor.go +++ b/client/asset/eth/contractor.go @@ -585,8 +585,7 @@ func (c *tokenContractorV0) tokenAddress() common.Address { type contractorV1 struct { contractV1 - abi *abi.ABI - // net dex.Network + abi *abi.ABI contractAddr common.Address acctAddr common.Address cb bind.ContractBackend @@ -598,15 +597,13 @@ type contractorV1 struct { var _ contractor = (*contractorV1)(nil) func newV1Contractor(net dex.Network, contractAddr, acctAddr common.Address, cb bind.ContractBackend) (contractor, error) { - c, err := swapv1.NewETHSwap(contractAddr, cb) if err != nil { return nil, err } return &contractorV1{ - contractV1: c, - abi: dexeth.ABIs[1], - // net: net, + contractV1: c, + abi: dexeth.ABIs[1], contractAddr: contractAddr, acctAddr: acctAddr, cb: cb, @@ -698,7 +695,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 011c21ff75..c19f235988 100644 --- a/client/asset/eth/eth.go +++ b/client/asset/eth/eth.go @@ -96,9 +96,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 ( @@ -549,14 +546,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() } func CreateEVMWallet(chainID int64, createWalletParams *asset.CreateWalletParams, compat *CompatibilityData, skipConnect bool) error { @@ -1326,9 +1316,10 @@ func (w *baseWallet) MaxFundingFees(_ uint32, _ uint64, _ map[string]string) uin // SingleLotSwapRefundFees returns the fees for a swap transaction for a single lot. func (w *assetWallet) SingleLotSwapRefundFees(serverVer uint32, feeSuggestion uint64, _ bool) (swapFees uint64, refundFees uint64, err error) { - g := w.gases(contractVersion(serverVer)) + contractVer := contractVersion(serverVer) + g := w.gases(contractVer) if g == nil { - return 0, 0, fmt.Errorf("no gases known for %d version %d", w.assetID, serverVer) + return 0, 0, fmt.Errorf("no gases known for %d, contract version %d", w.assetID, contractVer) } return g.Swap * feeSuggestion, g.Refund * feeSuggestion, nil } @@ -2155,7 +2146,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)) } @@ -2310,7 +2301,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 }) @@ -2319,7 +2310,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 }) @@ -2685,7 +2676,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) } @@ -2727,19 +2718,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() @@ -2788,7 +2768,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 } @@ -2855,7 +2835,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 } @@ -2934,7 +2914,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) } @@ -3032,7 +3012,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 @@ -3107,7 +3087,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") } @@ -3199,7 +3179,7 @@ func (w *ETHWallet) RestorationInfo(seed []byte) ([]*asset.WalletRestoration, er // 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 } @@ -3384,7 +3364,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 } @@ -4001,7 +3981,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) } @@ -4094,7 +4074,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) } @@ -4495,7 +4475,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") } @@ -4504,7 +4484,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() @@ -4627,7 +4607,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 @@ -4660,7 +4640,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 }) @@ -4668,8 +4648,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 706146697e..186e603dbf 100644 --- a/client/asset/eth/eth_test.go +++ b/client/asset/eth/eth_test.go @@ -2471,7 +2471,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) } @@ -3047,7 +3047,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) } @@ -3302,15 +3302,6 @@ func TestMaxOrder(t *testing.T) { } } -func overMaxWei() *big.Int { - maxInt := ^uint64(0) - maxWei := new(big.Int).SetUint64(maxInt) - gweiFactorBig := big.NewInt(dexeth.GweiFactor) - maxWei.Mul(maxWei, gweiFactorBig) - overMaxWei := new(big.Int).Set(maxWei) - return overMaxWei.Add(overMaxWei, gweiFactorBig) -} - func packInitiateDataV0(initiations []*dexeth.Initiation) ([]byte, error) { abiInitiations := make([]swapv0.ETHSwapInitiation, 0, len(initiations)) for _, init := range initiations { @@ -3500,7 +3491,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) } @@ -4087,24 +4078,14 @@ func testRefundReserves(t *testing.T, assetID uint32) { node.swapMap = map[[32]byte]*dexeth.SwapState{secretHash: {}} feeWallet := eth - gasesV0 := dexeth.VersionedGases[0] - gasesV1 := &dexeth.Gases{Refund: 1e6} + gasesV0 := eth.versionedGases[0] + gasesV1 := eth.versionedGases[1] assetV0 := *tETHV0 assetV1 := *tETHV0 - if assetID == BipID { - eth.versionedGases[1] = gasesV1 - } else { + if assetID != BipID { feeWallet = node.tokenParent assetV0 = *tTokenV0 assetV1 = *tTokenV0 - tokenContracts := dexeth.Tokens[simnetTokenID].NetTokens[dex.Simnet].SwapContracts - gasesV0 = &tokenGasesV0 - tc := *tokenContracts[0] - tc.Gas = *gasesV1 - tokenContracts[1] = &tc - defer delete(tokenContracts, 1) - eth.versionedGases[0] = gasesV0 - eth.versionedGases[1] = gasesV1 node.tokenContractor.bal = dexeth.GweiToWei(1e9) } @@ -4185,9 +4166,9 @@ func testRedemptionReserves(t *testing.T, assetID uint32) { var secretHash [32]byte node.tContractor.swapMap[secretHash] = &dexeth.SwapState{} - gasesV1 := &dexeth.Gases{Redeem: 1e6, RedeemAdd: 85e5} + gasesV0 := eth.versionedGases[0] + gasesV1 := eth.versionedGases[1] eth.versionedGases[1] = gasesV1 - gasesV0 := dexeth.VersionedGases[0] assetV0 := *tETHV0 assetV1 := *tETHV0 feeWallet := eth @@ -4196,12 +4177,6 @@ func testRedemptionReserves(t *testing.T, assetID uint32) { feeWallet = node.tokenParent assetV0 = *tTokenV0 assetV1 = *tTokenV0 - tokenContracts := dexeth.Tokens[simnetTokenID].NetTokens[dex.Simnet].SwapContracts - gasesV0 = &tokenGasesV0 - tc := *tokenContracts[0] - tc.Gas = *gasesV1 - tokenContracts[1] = &tc - defer delete(tokenContracts, 1) } assetV0.MaxFeeRate = 45 diff --git a/client/asset/eth/nodeclient.go b/client/asset/eth/nodeclient.go index c30aee8d92..ccf7b92cac 100644 --- a/client/asset/eth/nodeclient.go +++ b/client/asset/eth/nodeclient.go @@ -30,8 +30,7 @@ import ( ) const ( - contractVersionNewest = ^uint32(0) - approveGas = 4e5 + approveGas = 4e5 ) var ( @@ -410,9 +409,10 @@ func newTxOpts(ctx context.Context, from common.Address, val, maxGas uint64, max } func gases(contractVer uint32, versionedGases map[uint32]*dexeth.Gases) *dexeth.Gases { - if contractVer != contractVersionNewest { + if contractVer != dexeth.ContractVersionERC20 { return versionedGases[contractVer] } + var bestVer uint32 var bestGases *dexeth.Gases for ver, gases := range versionedGases { diff --git a/client/asset/eth/nodeclient_harness_test.go b/client/asset/eth/nodeclient_harness_test.go index 5eda092fa7..314c074b3c 100644 --- a/client/asset/eth/nodeclient_harness_test.go +++ b/client/asset/eth/nodeclient_harness_test.go @@ -129,10 +129,8 @@ var ( testTokenID uint32 masterToken *dexeth.Token - contractAddr common.Address - - v1 bool - ver uint32 + v1 bool + contractVer uint32 ) func newContract(stamp uint64, secretHash [32]byte, val uint64) *asset.Contract { @@ -149,7 +147,7 @@ func acLocator(c *asset.Contract) []byte { } func makeLocator(secretHash [32]byte, valg, lockTime uint64) []byte { - if ver == 1 { + if contractVer == 1 { return (&dexeth.SwapVector{ From: ethClient.address(), To: participantEthClient.address(), @@ -171,7 +169,7 @@ func newRedeem(secret, secretHash [32]byte, valg, lockTime uint64) *asset.Redemp // id: txHash, value: valg, }, - Contract: dexeth.EncodeContractData(ver, makeLocator(secretHash, valg, lockTime)), + Contract: dexeth.EncodeContractData(contractVer, makeLocator(secretHash, valg, lockTime)), }, Secret: secret[:], } @@ -385,15 +383,15 @@ func runSimnet(m *testing.M) (int, error) { return 1, fmt.Errorf("error creating participant wallet dir: %v", err) } - tokenGases = &dexeth.Tokens[testTokenID].NetTokens[dex.Simnet].SwapContracts[ver].Gas + tokenGases = &dexeth.Tokens[testTokenID].NetTokens[dex.Simnet].SwapContracts[contractVer].Gas // ETH swap contract. token := dexeth.Tokens[testTokenID].NetTokens[dex.Simnet] - fmt.Printf("ETH swap contract address is %v\n", dexeth.ContractAddresses[ver][dex.Simnet]) - fmt.Printf("Token swap contract addr is %v\n", token.SwapContracts[ver].Address) + fmt.Printf("ETH swap contract address is %v\n", dexeth.ContractAddresses[contractVer][dex.Simnet]) + fmt.Printf("Token swap contract addr is %v\n", token.SwapContracts[contractVer].Address) fmt.Printf("Test token contract addr is %v\n", token.Address) - contractAddr = dexeth.ContractAddresses[ver][dex.Simnet] + contractAddr := dexeth.ContractAddresses[contractVer][dex.Simnet] initiatorProviders, participantProviders := rpcEndpoints(dex.Simnet) @@ -428,9 +426,9 @@ func runSimnet(m *testing.M) (int, error) { simnetAddr = simnetAcct.Address participantAddr = participantAcct.Address - contractAddr, exists := dexeth.ContractAddresses[ver][dex.Simnet] + contractAddr, exists := dexeth.ContractAddresses[contractVer][dex.Simnet] if !exists || contractAddr == (common.Address{}) { - return 1, fmt.Errorf("no contract address for version %d", ver) + return 1, fmt.Errorf("no contract address for contract version %d", contractVer) } if v1 { @@ -495,7 +493,7 @@ func runSimnet(m *testing.M) (int, error) { func runTestnet(m *testing.M) (int, error) { testTokenID = usdcID masterToken = dexeth.Tokens[testTokenID] - tokenGases = &masterToken.NetTokens[dex.Testnet].SwapContracts[ver].Gas + tokenGases = &masterToken.NetTokens[dex.Testnet].SwapContracts[contractVer].Gas if testnetWalletSeed == "" || testnetParticipantWalletSeed == "" { return 1, errors.New("testnet seeds not set") } @@ -510,8 +508,8 @@ func runTestnet(m *testing.M) (int, error) { return 1, fmt.Errorf("error creating testnet participant wallet dir: %v", err) } secPerBlock = testnetSecPerBlock - contractAddr = dexeth.ContractAddresses[ver][dex.Testnet] - fmt.Printf("ETH swap contract address is %v\n", contractAddr) + contractAddr := dexeth.ContractAddresses[contractVer][dex.Testnet] + fmt.Printf("Swap contract address is %v\n", contractAddr) initiatorRPC, participantRPC := rpcEndpoints(dex.Testnet) @@ -560,13 +558,13 @@ func runTestnet(m *testing.M) (int, error) { simnetAddr = simnetAcct.Address participantAddr = participantAcct.Address - contractAddr, exists := dexeth.ContractAddresses[ver][dex.Testnet] + contractAddr, exists := dexeth.ContractAddresses[contractVer][dex.Testnet] if !exists || contractAddr == (common.Address{}) { - return 1, fmt.Errorf("no contract address for version %d", ver) + return 1, fmt.Errorf("no contract address for version %d", contractVer) } ctor, tokenCtor := newV0Contractor, newV0TokenContractor - if ver == 1 { + if contractVer == 1 { ctor, tokenCtor = newV1Contractor, newV1TokenContractor } @@ -622,6 +620,8 @@ func prepareV1SimnetContractors() (err error) { } func prepareSimnetContractors(c contractorConstructor, tc tokenContractorConstructor) (err error) { + contractAddr := dexeth.ContractAddresses[contractVer][dex.Simnet] + if simnetContractor, err = c(dex.Simnet, contractAddr, simnetAddr, ethClient.contractBackend()); err != nil { return fmt.Errorf("new contractor error: %w", err) } @@ -629,7 +629,9 @@ func prepareSimnetContractors(c contractorConstructor, tc tokenContractorConstru return fmt.Errorf("participant new contractor error: %w", err) } - if simnetTokenContractor, err = tc(dex.Simnet, masterToken, simnetAddr, ethClient.contractBackend()); err != nil { + token := dexeth.Tokens[testTokenID] + + if simnetTokenContractor, err = tc(dex.Simnet, token, simnetAddr, ethClient.contractBackend()); err != nil { return fmt.Errorf("new token contractor error: %w", err) } @@ -638,7 +640,7 @@ func prepareSimnetContractors(c contractorConstructor, tc tokenContractorConstru // (*BoundContract).Call while calling (*ERC20Swap).TokenAddress. time.Sleep(time.Second) - if participantTokenContractor, err = tc(dex.Simnet, masterToken, participantAddr, participantEthClient.contractBackend()); err != nil { + if participantTokenContractor, err = tc(dex.Simnet, token, participantAddr, participantEthClient.contractBackend()); err != nil { return fmt.Errorf("participant new token contractor error: %w", err) } return @@ -674,14 +676,12 @@ func TestMain(m *testing.M) { flag.Parse() if v1 { - ver = 1 + contractVer = 1 } - ethGases = dexeth.VersionedGases[ver] - contractAddr = dexeth.ContractAddresses[BipID][dex.Simnet] + ethGases = dexeth.VersionedGases[contractVer] if isTestnet { - contractAddr = dexeth.ContractAddresses[BipID][dex.Testnet] tmpDir, err := os.MkdirTemp("", "") if err != nil { fmt.Fprintf(os.Stderr, "error creating temporary directory: %v", err) @@ -839,7 +839,7 @@ func TestBasicRetrieval(t *testing.T) { func TestPeering(t *testing.T) { t.Run("testAddPeer", testAddPeer) t.Run("testSyncProgress", testSyncProgress) - t.Run("testGetCodeAt", testGetCodeAt) + // t.Run("testGetCodeAt", testGetCodeAt) } func TestAccount(t *testing.T) { @@ -1167,39 +1167,41 @@ func testSyncProgress(t *testing.T) { spew.Dump(p) } -// func testInitiateGas(t *testing.T, assetID uint32) { -// if assetID != BipID { -// prepareTokenClients(t) -// } +func testInitiateGas(t *testing.T, assetID uint32) { + gases := ethGases + c := simnetContractor + if assetID != BipID { + c = simnetTokenContractor + prepareTokenClients(t) + gases = tokenGases + } -// gases := gases(ver, dexeth.VersionedGases) - -// var previousGas uint64 -// maxSwaps := 50 -// for i := 1; i <= maxSwaps; i++ { -// gas, err := ethClient.estimateInitGas(ctx, i) -// if err != nil { -// t.Fatalf("unexpected error from estimateInitGas(%d): %v", i, err) -// } - -// var expectedGas uint64 -// var actualGas uint64 -// if i == 1 { -// expectedGas = gases.Swap -// actualGas = gas -// } else { -// expectedGas = gases.SwapAdd -// actualGas = gas - previousGas -// } -// if actualGas > expectedGas || actualGas < expectedGas/2 { -// t.Fatalf("Expected incremental gas for %d initiations to be close to %d but got %d", -// i, expectedGas, actualGas) -// } - -// fmt.Printf("Gas used for batch initiating %v swaps: %v. %v more than previous \n", i, gas, gas-previousGas) -// previousGas = gas -// } -// } + var previousGas uint64 + maxSwaps := 50 + for i := 1; i <= maxSwaps; i++ { + gas, err := c.estimateInitGas(ctx, i) + if err != nil { + t.Fatalf("unexpected error from estimateInitGas(%d): %v", i, err) + } + + var expectedGas uint64 + var actualGas uint64 + if i == 1 { + expectedGas = gases.Swap + actualGas = gas + } else { + expectedGas = gases.SwapAdd + actualGas = gas - previousGas + } + if actualGas > expectedGas || actualGas < expectedGas/2 { + t.Fatalf("Expected incremental gas for %d initiations to be close to %d but got %d", + i, expectedGas, actualGas) + } + + fmt.Printf("Gas used for batch initiating %v swaps: %v. %v more than previous \n", i, gas, gas-previousGas) + previousGas = gas + } +} // feesAtBlk calculates the gas fee at blkNum. This adds the base fee at blkNum // to a minimum gas tip cap. @@ -2303,23 +2305,23 @@ func testApproveGas(t *testing.T) { fmt.Printf("replacement tx hash: %s\n", tx.Hash()) }*/ -func testGetCodeAt(t *testing.T) { - cl, is := ethClient.(*nodeClient) - if !is { - t.Skip("getCode tests only run for nodeClient") - } - byteCode, err := cl.getCodeAt(ctx, contractAddr) - if err != nil { - t.Fatalf("Failed to get bytecode: %v", err) - } - c, err := hex.DecodeString(swapv0.ETHSwapRuntimeBin) - if err != nil { - t.Fatalf("Error decoding") - } - if !bytes.Equal(byteCode, c) { - t.Fatal("Contract on chain does not match one in code") - } -} +// func testGetCodeAt(t *testing.T) { +// cl, is := ethClient.(*nodeClient) +// if !is { +// t.Skip("getCode tests only run for nodeClient") +// } +// byteCode, err := cl.getCodeAt(ctx, contractAddr) +// if err != nil { +// t.Fatalf("Failed to get bytecode: %v", err) +// } +// c, err := hex.DecodeString(swapv0.ETHSwapRuntimeBin) +// if err != nil { +// t.Fatalf("Error decoding") +// } +// if !bytes.Equal(byteCode, c) { +// t.Fatal("Contract on chain does not match one in code") +// } +// } func testSignMessage(t *testing.T) { msg := []byte("test message") @@ -2347,7 +2349,7 @@ func TestTokenGasEstimates(t *testing.T) { runSimnetMiner(ctx, "eth", tLogger) prepareTokenClients(t) tLogger.SetLevel(dex.LevelInfo) - if err := getGasEstimates(ctx, ethClient, participantEthClient, simnetTokenContractor, participantTokenContractor, 5, ver, tokenGases, tLogger); err != nil { + if err := getGasEstimates(ctx, ethClient, participantEthClient, simnetTokenContractor, participantTokenContractor, 5, contractVer, tokenGases, tLogger); err != nil { t.Fatalf("getGasEstimates error: %v", err) } } diff --git a/dex/networks/eth/contracts/ETHSwapV1.sol b/dex/networks/eth/contracts/ETHSwapV1.sol index d1e115b67f..3c1173f8f8 100644 --- a/dex/networks/eth/contracts/ETHSwapV1.sol +++ b/dex/networks/eth/contracts/ETHSwapV1.sol @@ -187,6 +187,10 @@ contract ETHSwap { require(secretValidates(r.secret, r.v.secretHash), "invalid secret"); swaps[k] = r.secret; + + + // DRAFT TODO: NOOOOOO! This doesn't account for decimals. This is + // WRONG for e.g. USDC. amountToRedeem += r.v.value * 1 gwei; } diff --git a/dex/networks/eth/params.go b/dex/networks/eth/params.go index b671a51860..b013662b7c 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" @@ -129,8 +130,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 @@ -276,6 +289,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 @@ -395,12 +419,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/dex/networks/eth/tokens.go b/dex/networks/eth/tokens.go index 4e4d983318..ae143b97d8 100644 --- a/dex/networks/eth/tokens.go +++ b/dex/networks/eth/tokens.go @@ -296,14 +296,6 @@ func MaybeReadSimnetAddrsDir( return } - fmt.Println("------------") - for ver, nets := range contractAddrs { - fmt.Println("--MaybeReadSimnetAddrsDir.0", ver, len(nets)) - for net, addr := range nets { - fmt.Println("--MaybeReadSimnetAddrsDir.1", net, addr) - } - } - ethSwapContractAddrFileV0 := filepath.Join(harnessDir, "eth_swap_contract_address.txt") tokenSwapContractAddrFileV0 := filepath.Join(harnessDir, "erc20_swap_contract_address.txt") ethSwapContractAddrFileV1 := filepath.Join(harnessDir, "eth_swap_contract_address_v1.txt") diff --git a/dex/networks/polygon/params.go b/dex/networks/polygon/params.go index 93ffb8d816..c78a695795 100644 --- a/dex/networks/polygon/params.go +++ b/dex/networks/polygon/params.go @@ -111,7 +111,19 @@ var ( Transfer: 64_539, }, }, - 1: {}, + 1: { + // DRAFT TODO + Address: common.Address{}, // Set in MaybeReadSimnetAddrs + Gas: dexeth.Gases{ + Swap: 223_163, + SwapAdd: 146_399, + Redeem: 82_121, + RedeemAdd: 41_113, + Refund: 62_527, + Approve: 58_180, + Transfer: 64_539, + }, + }, }, }, }, diff --git a/server/asset/eth/coiner.go b/server/asset/eth/coiner.go index 5a4f26ca83..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 == be.baseChainID { - 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 5d09103174..7992b32245 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,13 +119,18 @@ 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 - value := tSwap1.Value + tSwap2.Value - 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) @@ -134,82 +139,83 @@ func TestNewSwapCoin(t *testing.T) { 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: initSecretHashA, + wantErr: true, + }, { + name: "tx coin id for swap - contract not in tx", + tx: tTx(gasPrice, gasTipCap, txVal, contractAddr, initCalldata), coinID: txCoinIDBytes, - contract: badContract, + 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{ @@ -231,12 +237,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) != value || + 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) } @@ -251,13 +258,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 @@ -281,14 +290,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), @@ -297,21 +305,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{ @@ -324,11 +353,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 e1e6d0a14a..515ea30a47 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,51 +30,69 @@ import ( type VersionedToken struct { *dexeth.Token - Ver uint32 + ContractVersion uint32 } var registeredTokens = make(map[uint32]*VersionedToken) -func registerToken(assetID uint32, ver uint32) { +// func networkToken(assetID uint32, net dex.Network) (token *VersionedToken, netToken *dexeth.NetToken, contract *dexeth.SwapContract, err error) { +// token, found := registeredTokens[assetID] +// if !found { +// return nil, nil, nil, fmt.Errorf("no token for asset ID %d", assetID) +// } +// netToken, found = token.NetTokens[net] +// if !found { +// return nil, nil, nil, fmt.Errorf("no addresses for %s on %s", token.Name, net) +// } +// contract, found = netToken.SwapContracts[token.ContractVersion] +// if !found || contract.Address == (common.Address{}) { +// return nil, nil, nil, fmt.Errorf("no version %d address for %s on %s", token.ContractVersion, token.Name, net) +// } +// return +// } + +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)) } - asset.RegisterToken(assetID, &TokenDriver{ + drv := &TokenDriver{ DriverBase: DriverBase{ - Ver: ver, - UI: token.UnitInfo, + ProtocolVersion: protocolVer, + UI: token.UnitInfo, }, Token: token.Token, - }) + } + asset.RegisterToken(assetID, drv) registeredTokens[assetID] = &VersionedToken{ - Token: token, - Ver: ver, + Token: token, + ContractVersion: protocolVer.ContractVersion(), } } func init() { asset.Register(BipID, &Driver{ DriverBase: DriverBase{ - Ver: version, - UI: dexeth.UnitInfo, + ProtocolVersion: ethProtocolVersion, + UI: dexeth.UnitInfo, }, }) - registerToken(testTokenID, ethContractVersion) - registerToken(usdcID, ethContractVersion) + registerToken(testTokenID, dexeth.ProtocolVersionZero) + registerToken(usdcID, dexeth.ProtocolVersionZero) } 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, } @@ -90,21 +107,21 @@ func networkToken(vToken *VersionedToken, net dex.Network) (netToken *dexeth.Net return nil, nil, fmt.Errorf("no addresses for %s on %s", vToken.Name, net) } - contract, found = netToken.SwapContracts[vToken.Ver] + contract, found = netToken.SwapContracts[vToken.ContractVersion] if !found || contract.Address == (common.Address{}) { - return nil, nil, fmt.Errorf("no version %d address for %s on %s", vToken.Ver, vToken.Name, net) + return nil, nil, fmt.Errorf("no version %d address for %s on %s", vToken.ContractVersion, vToken.Name, net) } return } type DriverBase struct { - UI dex.UnitInfo - Ver uint32 + UI dex.UnitInfo + ProtocolVersion dexeth.ProtocolVersion } // Version returns the Backend implementation's version number. func (d *DriverBase) Version() uint32 { - return d.Ver + return uint32(d.ProtocolVersion) } // DecodeCoinID creates a human-readable representation of a coin ID for @@ -156,11 +173,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, vToken *VersionedToken) 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 { @@ -207,6 +226,7 @@ type AssetBackend struct { redeemSize uint64 contractAddr common.Address + contractVer uint32 } // ETHBackend implements some Ethereum-specific methods. @@ -237,7 +257,7 @@ func unconnectedETH(bipID uint32, contractAddr common.Address, vTokens map[uint3 // 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{ @@ -255,6 +275,7 @@ func unconnectedETH(bipID uint32, contractAddr common.Address, vTokens map[uint3 redeemSize: dexeth.RedeemGas(1, ethContractVersion), assetID: bipID, atomize: dexeth.WeiToGwei, + contractVer: ethContractVersion, }}, nil } @@ -567,13 +588,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 } @@ -581,19 +602,17 @@ 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 } - - if ver != eth.VersionedToken.Ver { - return fmt.Errorf("incorrect token swap contract version %d, wanted %d", ver, eth.VersionedToken.Ver) - } - _, _, err = networkToken(eth.VersionedToken, eth.net) if err != nil { return fmt.Errorf("error locating token: %v", err) } + if contractVer != eth.VersionedToken.ContractVersion { + return fmt.Errorf("incorrect token swap contract version %d, wanted %d", contractVer, eth.VersionedToken.ContractVersion) + } return nil } @@ -621,27 +640,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. @@ -789,29 +799,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 66a5e6309f..181ff325c6 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" @@ -21,7 +22,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" @@ -31,69 +31,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: 2.5e9 gwei + Participant: 0x345853e21b1d475582e71cc269124ed5e2dd3422 + }, + ETHSwapInitiation { + RefundTimestamp: 1632112916 + SecretHash: ebdc4c31b88d0c8f4d644591a8e00e92b607f920ad8050deb7c7469767d9c561 + Value: 2.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 { @@ -104,11 +91,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 @@ -126,8 +108,6 @@ type testNode struct { tx *types.Transaction txIsMempool bool txErr error - rcpt *types.Receipt - rcptErr error acctBal *big.Int acctBalErr error } @@ -162,24 +142,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) { @@ -218,8 +225,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()) @@ -465,10 +471,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 @@ -480,20 +487,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, @@ -506,7 +513,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 { @@ -517,8 +524,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) } } @@ -553,23 +560,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", }} @@ -586,41 +590,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, @@ -628,8 +634,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) @@ -701,26 +709,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 { @@ -745,13 +750,16 @@ func testValidateContract(t *testing.T, assetID uint32) { cv = &TokenBackend{ AssetBackend: eth, VersionedToken: &VersionedToken{ - Token: dexeth.Tokens[testTokenID], - Ver: test.ver, + Token: dexeth.Tokens[testTokenID], + ContractVersion: test.ver, }, } } - 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 7759a27f99..14b07e240f 100644 --- a/server/asset/eth/rpcclient.go +++ b/server/asset/eth/rpcclient.go @@ -17,6 +17,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" @@ -233,14 +234,31 @@ 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 %v contract for %q: %v", c.baseChainName, 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: + + // Duh. Nope. + + 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, vToken := range c.tokensLoaded { - tkn, err := newTokener(ctx, vToken, c.net, ec.Client) + tkn, err := newTokener(ctx, assetID, vToken, c.net, ec.Client) if err != nil { return nil, fmt.Errorf("error constructing ERC20Swap: %w", err) } @@ -481,7 +499,7 @@ func (c *rpcclient) loadToken(ctx context.Context, assetID uint32, vToken *Versi c.tokensLoaded[assetID] = vToken for _, cl := range c.clientsCopy() { - tkn, err := newTokener(ctx, vToken, c.net, cl.Client) + tkn, err := newTokener(ctx, assetID, vToken, c.net, cl.Client) if err != nil { return fmt.Errorf("error constructing ERC20Swap: %w", err) } @@ -498,7 +516,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 == c.baseChainID { + 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. @@ -537,20 +565,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 == c.baseChainID { - 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 @@ -562,6 +595,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) { @@ -576,7 +620,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) @@ -649,14 +692,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 fe7261154a..347ba04b5b 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 f939de0a13..0a50274ab2 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. @@ -38,19 +42,39 @@ type tokener struct { // newTokener is a constructor for a tokener. func newTokener( ctx context.Context, + assetID uint32, vToken *VersionedToken, net dex.Network, be bind.ContractBackend, ) (*tokener, error) { - netToken, swapContract, err := networkToken(vToken, net) + netToken, contract, err := networkToken(vToken, net) if err != nil { return nil, err } - es, err := erc20v1.NewERC20Swap(swapContract.Address, be) - if err != nil { - return nil, err + var tokenAddresser interface { + TokenAddress(opts *bind.CallOpts) (common.Address, error) + } + + var sc swapContract + switch vToken.ContractVersion { + 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", vToken.ContractVersion) } erc20, err := erc20.NewIERC20(netToken.Address, be) @@ -58,22 +82,22 @@ func newTokener( 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", - vToken.Name, vToken.Ver, err) + vToken.Name, vToken.ContractVersion, err) } if boundAddr != netToken.Address { return nil, fmt.Errorf("wrong bound address for %s version %d contract. wanted %s, got %s", - vToken.Name, vToken.Ver, netToken.Address, boundAddr) + vToken.Name, vToken.ContractVersion, netToken.Address, boundAddr) } tkn := &tokener{ VersionedToken: vToken, - swapContract: &swapSourceV1{es}, + swapContract: sc, erc20Contract: erc20, - contractAddr: swapContract.Address, + contractAddr: contract.Address, tokenAddr: netToken.Address, } @@ -92,15 +116,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. @@ -108,29 +132,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. diff --git a/server/asset/polygon/polygon.go b/server/asset/polygon/polygon.go index 9f8e3faf88..7d2a95b763 100644 --- a/server/asset/polygon/polygon.go +++ b/server/asset/polygon/polygon.go @@ -5,9 +5,9 @@ package polygon import ( "fmt" - "time" "decred.org/dcrdex/dex" + dexeth "decred.org/dcrdex/dex/networks/eth" dexpolygon "decred.org/dcrdex/dex/networks/polygon" "decred.org/dcrdex/server/asset" "decred.org/dcrdex/server/asset/eth" @@ -15,49 +15,42 @@ import ( var registeredTokens = make(map[uint32]*eth.VersionedToken) -func registerToken(assetID uint32, ver uint32) { +func registerToken(assetID uint32, protocolVersion dexeth.ProtocolVersion) { token, exists := dexpolygon.Tokens[assetID] if !exists { panic(fmt.Sprintf("no token constructor for asset ID %d", assetID)) } asset.RegisterToken(assetID, ð.TokenDriver{ DriverBase: eth.DriverBase{ - Ver: ver, - UI: token.UnitInfo, + ProtocolVersion: protocolVersion, + UI: token.UnitInfo, }, Token: token.Token, }) registeredTokens[assetID] = ð.VersionedToken{ - Token: token, - Ver: ver, + Token: token, + ContractVersion: protocolVersion.ContractVersion(), } } func init() { asset.Register(BipID, &Driver{eth.Driver{ DriverBase: eth.DriverBase{ - Ver: version, - UI: dexpolygon.UnitInfo, + ProtocolVersion: ethProtocolVersion, + UI: dexpolygon.UnitInfo, }, }}) - registerToken(testTokenID, 0) - registerToken(usdcID, 0) - registerToken(wethTokenID, 0) - registerToken(wbtcTokenID, 0) - - if blockPollIntervalStr != "" { - blockPollInterval, _ = time.ParseDuration(blockPollIntervalStr) - if blockPollInterval < time.Second { - panic(fmt.Sprintf("invalid value for blockPollIntervalStr: %q", blockPollIntervalStr)) - } - } + registerToken(testTokenID, dexeth.ProtocolVersionZero) + registerToken(usdcID, dexeth.ProtocolVersionZero) + registerToken(wethTokenID, dexeth.ProtocolVersionZero) + registerToken(wbtcTokenID, dexeth.ProtocolVersionZero) } const ( BipID = 966 ethContractVersion = 0 - version = 0 + ethProtocolVersion = dexeth.ProtocolVersionZero ) var ( @@ -65,12 +58,6 @@ var ( usdcID, _ = dex.BipSymbolID("usdc.polygon") wethTokenID, _ = dex.BipSymbolID("weth.polygon") wbtcTokenID, _ = dex.BipSymbolID("wbtc.polygon") - - // blockPollInterval is the delay between calls to bestBlockHash to check - // for new blocks. Modify at compile time via blockPollIntervalStr: - // go build -tags lgpl -ldflags "-X 'decred.org/dcrdex/server/asset/polygon.blockPollIntervalStr=10s'" - blockPollInterval = time.Second - blockPollIntervalStr string ) type Driver struct {