diff --git a/client/asset/bch/bch.go b/client/asset/bch/bch.go index c83ddabf32..94a6b0305d 100644 --- a/client/asset/bch/bch.go +++ b/client/asset/bch/bch.go @@ -113,6 +113,7 @@ var ( rpcWalletDefinition, // electrumWalletDefinition, // getinfo RPC needs backport: https://github.com/Electron-Cash/Electron-Cash/pull/2399 }, + ProtocolVersions: []uint32{0}, } ) diff --git a/client/asset/btc/btc.go b/client/asset/btc/btc.go index 40e1bfc11c..477cd0087d 100644 --- a/client/asset/btc/btc.go +++ b/client/asset/btc/btc.go @@ -255,6 +255,7 @@ var ( electrumWalletDefinition, }, LegacyWalletIndex: 1, + ProtocolVersions: []uint32{0}, } client http.Client @@ -3399,12 +3400,8 @@ func (btc *baseWallet) AuditContract(coinID, contract, txData dex.Bytes, rebroad // LocktimeExpired returns true if the specified contract's locktime has // expired, making it possible to issue a Refund. -func (btc *baseWallet) LocktimeExpired(_ context.Context, contract dex.Bytes) (bool, time.Time, error) { - _, _, locktime, _, err := dexbtc.ExtractSwapDetails(contract, btc.segwit, btc.chainParams) - if err != nil { - return false, time.Time{}, fmt.Errorf("error extracting contract locktime: %w", err) - } - contractExpiry := time.Unix(int64(locktime), 0).UTC() +func (btc *baseWallet) LocktimeExpired(_ context.Context, deets *dex.SwapContractDetails) (bool, time.Time, error) { + contractExpiry := time.Unix(int64(deets.LockTime), 0).UTC() medianTime, err := btc.node.medianTime() // TODO: pass ctx if err != nil { return false, time.Time{}, fmt.Errorf("error getting median time: %w", err) @@ -3418,7 +3415,7 @@ func (btc *baseWallet) LocktimeExpired(_ context.Context, contract dex.Bytes) (b // // This method blocks until the redemption is found, an error occurs or the // provided context is canceled. -func (btc *intermediaryWallet) FindRedemption(ctx context.Context, coinID, _ dex.Bytes) (redemptionCoin, secret dex.Bytes, err error) { +func (btc *intermediaryWallet) FindRedemption(ctx context.Context, coinID, _ dex.Bytes, _ *dex.SwapContractDetails) (redemptionCoin, secret dex.Bytes, err error) { txHash, vout, err := decodeCoinID(coinID) if err != nil { return nil, nil, fmt.Errorf("cannot decode contract coin id: %w", err) @@ -3656,7 +3653,7 @@ func (btc *intermediaryWallet) tryRedemptionRequests(ctx context.Context, startB // wallet does not store it, even though it was known when the init transaction // was created. The client should store this information for persistence across // sessions. -func (btc *baseWallet) Refund(coinID, contract dex.Bytes, feeRate uint64) (dex.Bytes, error) { +func (btc *baseWallet) Refund(coinID, contract dex.Bytes, _ *dex.SwapContractDetails, feeRate uint64) (dex.Bytes, error) { txHash, vout, err := decodeCoinID(coinID) if err != nil { return nil, err @@ -3890,7 +3887,7 @@ func (btc *baseWallet) send(address string, val uint64, feeRate uint64, subtract // SwapConfirmations gets the number of confirmations for the specified swap // by first checking for a unspent output, and if not found, searching indexed // wallet transactions. -func (btc *baseWallet) SwapConfirmations(_ context.Context, id dex.Bytes, contract dex.Bytes, startTime time.Time) (uint32, bool, error) { +func (btc *baseWallet) SwapConfirmations(_ context.Context, id dex.Bytes, contract dex.Bytes, startTime time.Time, _ *dex.SwapContractDetails) (uint32, bool, error) { txHash, vout, err := decodeCoinID(id) if err != nil { return 0, false, err diff --git a/client/asset/btc/btc_test.go b/client/asset/btc/btc_test.go index b94f12faa1..24a355db89 100644 --- a/client/asset/btc/btc_test.go +++ b/client/asset/btc/btc_test.go @@ -2139,7 +2139,7 @@ func testFindRedemption(t *testing.T, segwit bool, walletType string) { }) // Check find redemption result. - _, checkSecret, err := wallet.FindRedemption(tCtx, coinID, nil) + _, checkSecret, err := wallet.FindRedemption(tCtx, coinID, nil, nil) if err != nil { t.Fatalf("error finding redemption: %v", err) } @@ -2149,7 +2149,7 @@ func testFindRedemption(t *testing.T, segwit bool, walletType string) { // gettransaction error node.getTransactionErr = tErr - _, _, err = wallet.FindRedemption(tCtx, coinID, nil) + _, _, err = wallet.FindRedemption(tCtx, coinID, nil, nil) if err == nil { t.Fatalf("no error for gettransaction rpc error") } @@ -2160,7 +2160,7 @@ func testFindRedemption(t *testing.T, segwit bool, walletType string) { delete(node.getCFilterScripts, *redeemBlockHash) timedCtx, cancel := context.WithTimeout(tCtx, 500*time.Millisecond) // 0.5 seconds is long enough defer cancel() - _, k, err := wallet.FindRedemption(timedCtx, coinID, nil) + _, k, err := wallet.FindRedemption(timedCtx, coinID, nil, nil) if timedCtx.Err() == nil || k != nil { // Expected ctx to cancel after timeout and no secret should be found. t.Fatalf("unexpected result for missing redemption: secret: %v, err: %v", k, err) @@ -2174,7 +2174,7 @@ func testFindRedemption(t *testing.T, segwit bool, walletType string) { // Canceled context deadCtx, cancelCtx := context.WithCancel(tCtx) cancelCtx() - _, _, err = wallet.FindRedemption(deadCtx, coinID, nil) + _, _, err = wallet.FindRedemption(deadCtx, coinID, nil, nil) if err == nil { t.Fatalf("no error for canceled context") } @@ -2185,7 +2185,7 @@ func testFindRedemption(t *testing.T, segwit bool, walletType string) { redeemVin.SignatureScript = randBytes(100) node.blockchainMtx.Unlock() - _, _, err = wallet.FindRedemption(tCtx, coinID, nil) + _, _, err = wallet.FindRedemption(tCtx, coinID, nil, nil) if err == nil { t.Fatalf("no error for wrong redemption") } @@ -2195,7 +2195,7 @@ func testFindRedemption(t *testing.T, segwit bool, walletType string) { node.blockchainMtx.Unlock() // Sanity check to make sure it passes again. - _, _, err = wallet.FindRedemption(tCtx, coinID, nil) + _, _, err = wallet.FindRedemption(tCtx, coinID, nil, nil) if err != nil { t.Fatalf("error after clearing errors: %v", err) } @@ -2234,7 +2234,7 @@ func testRefund(t *testing.T, segwit bool, walletType string) { node.getTransactionErr = WalletTransactionNotFound contractOutput := newOutput(&txHash, 0, 1e8) - _, err = wallet.Refund(contractOutput.ID(), contract, feeSuggestion) + _, err = wallet.Refund(contractOutput.ID(), contract, nil, feeSuggestion) if err != nil { t.Fatalf("refund error: %v", err) } @@ -2243,14 +2243,14 @@ func testRefund(t *testing.T, segwit bool, walletType string) { badReceipt := &tReceipt{ coin: &tCoin{id: make([]byte, 15)}, } - _, err = wallet.Refund(badReceipt.coin.id, badReceipt.Contract(), feeSuggestion) + _, err = wallet.Refund(badReceipt.coin.id, badReceipt.Contract(), nil, feeSuggestion) if err == nil { t.Fatalf("no error for bad receipt") } ensureErr := func(tag string) { delete(node.checkpoints, outPt) - _, err = wallet.Refund(contractOutput.ID(), contract, feeSuggestion) + _, err = wallet.Refund(contractOutput.ID(), contract, nil, feeSuggestion) if err == nil { t.Fatalf("no error for %q", tag) } @@ -2266,7 +2266,7 @@ func testRefund(t *testing.T, segwit bool, walletType string) { // bad contract badContractOutput := newOutput(tTxHash, 0, 1e8) badContract := randBytes(50) - _, err = wallet.Refund(badContractOutput.ID(), badContract, feeSuggestion) + _, err = wallet.Refund(badContractOutput.ID(), badContract, nil, feeSuggestion) if err == nil { t.Fatalf("no error for bad contract") } @@ -2301,7 +2301,7 @@ func testRefund(t *testing.T, segwit bool, walletType string) { node.badSendHash = nil // Sanity check that we can succeed again. - _, err = wallet.Refund(contractOutput.ID(), contract, feeSuggestion) + _, err = wallet.Refund(contractOutput.ID(), contract, nil, feeSuggestion) if err != nil { t.Fatalf("re-refund error: %v", err) } @@ -2507,7 +2507,7 @@ func testConfirmations(t *testing.T, segwit bool, walletType string) { matchTime := swapBlock.Header.Timestamp // Bad coin id - _, _, err := wallet.SwapConfirmations(context.Background(), randBytes(35), contract, matchTime) + _, _, err := wallet.SwapConfirmations(context.Background(), randBytes(35), contract, matchTime, nil) if err == nil { t.Fatalf("no error for bad coin ID") } @@ -2519,7 +2519,7 @@ func testConfirmations(t *testing.T, segwit bool, walletType string) { } node.txOutRes = txOutRes node.getCFilterScripts[*blockHash] = [][]byte{pkScript} - confs, _, err := wallet.SwapConfirmations(context.Background(), coinID, contract, matchTime) + confs, _, err := wallet.SwapConfirmations(context.Background(), coinID, contract, matchTime, nil) if err != nil { t.Fatalf("error for gettransaction path: %v", err) } @@ -2531,7 +2531,7 @@ func testConfirmations(t *testing.T, segwit bool, walletType string) { node.txOutRes = nil node.getCFilterScripts[*blockHash] = nil node.getTransactionErr = tErr - _, _, err = wallet.SwapConfirmations(context.Background(), coinID, contract, matchTime) + _, _, err = wallet.SwapConfirmations(context.Background(), coinID, contract, matchTime, nil) if err == nil { t.Fatalf("no error for gettransaction error") } @@ -2547,7 +2547,7 @@ func testConfirmations(t *testing.T, segwit bool, walletType string) { node.getCFilterScripts[*spendingBlockHash] = [][]byte{pkScript} node.walletTxSpent = true - _, spent, err := wallet.SwapConfirmations(context.Background(), coinID, contract, matchTime) + _, spent, err := wallet.SwapConfirmations(context.Background(), coinID, contract, matchTime, nil) if err != nil { t.Fatalf("error for spent swap: %v", err) } diff --git a/client/asset/btc/electrum.go b/client/asset/btc/electrum.go index 50f34f6e40..74759d185f 100644 --- a/client/asset/btc/electrum.go +++ b/client/asset/btc/electrum.go @@ -235,7 +235,7 @@ func (btc *ExchangeWalletElectrum) tryRedemptionRequests(ctx context.Context) { // FindRedemption locates a swap contract output's redemption transaction input // and the secret key used to spend the output. -func (btc *ExchangeWalletElectrum) FindRedemption(ctx context.Context, coinID, contract dex.Bytes) (redemptionCoin, secret dex.Bytes, err error) { +func (btc *ExchangeWalletElectrum) FindRedemption(ctx context.Context, coinID, contract dex.Bytes, _ *dex.SwapContractDetails) (redemptionCoin, secret dex.Bytes, err error) { txHash, vout, err := decodeCoinID(coinID) if err != nil { return nil, nil, err diff --git a/client/asset/btc/electrum_test.go b/client/asset/btc/electrum_test.go index bf42639f6c..698b36fe15 100644 --- a/client/asset/btc/electrum_test.go +++ b/client/asset/btc/electrum_test.go @@ -128,7 +128,7 @@ func TestElectrumExchangeWallet(t *testing.T) { } // FindRedemption - redeemCoin, secretBytes, err := eew.FindRedemption(ctx, toCoinID(swapTxHash, swapVout), contract) + redeemCoin, secretBytes, err := eew.FindRedemption(ctx, toCoinID(swapTxHash, swapVout), contract, nil) if err != nil { t.Fatal(err) } diff --git a/client/asset/btc/livetest/livetest.go b/client/asset/btc/livetest/livetest.go index 8a73be113b..193b9111a4 100644 --- a/client/asset/btc/livetest/livetest.go +++ b/client/asset/btc/livetest/livetest.go @@ -351,7 +351,7 @@ func Run(t *testing.T, cfg *Config) { confContract := receipts[0].Contract() checkConfs := func(n uint32, expSpent bool) { t.Helper() - confs, spent, err := rig.secondWallet.SwapConfirmations(context.Background(), confCoin.ID(), confContract, tStart) + confs, spent, err := rig.secondWallet.SwapConfirmations(context.Background(), confCoin.ID(), confContract, tStart, nil) if err != nil { if n > 0 || !errors.Is(err, asset.CoinNotFoundError) { t.Fatalf("error getting %d confs: %v", n, err) @@ -406,7 +406,7 @@ func Run(t *testing.T, cfg *Config) { if auditCoin.Value() != swapVal { t.Fatalf("wrong contract value. wanted %d, got %d", swapVal, auditCoin.Value()) } - confs, spent, err := rig.firstWallet.SwapConfirmations(tCtx, receipt.Coin().ID(), receipt.Contract(), tStart) + confs, spent, err := rig.firstWallet.SwapConfirmations(tCtx, receipt.Coin().ID(), receipt.Contract(), tStart, nil) if err != nil { t.Fatalf("error getting confirmations: %v", err) } @@ -460,7 +460,7 @@ func Run(t *testing.T, cfg *Config) { TryFunc: func() wait.TryDirective { ctx, cancel := context.WithTimeout(tCtx, time.Second) defer cancel() - _, _, err = rig.secondWallet.FindRedemption(ctx, swapReceipt.Coin().ID(), nil) + _, _, err = rig.secondWallet.FindRedemption(ctx, swapReceipt.Coin().ID(), nil, nil) if err != nil { return wait.TryAgain } @@ -478,7 +478,7 @@ func Run(t *testing.T, cfg *Config) { } // Check that there is 1 confirmation on the swap checkConfs(expConfs, true) - _, checkKey, err := rig.secondWallet.FindRedemption(ctx, swapReceipt.Coin().ID(), nil) + _, checkKey, err := rig.secondWallet.FindRedemption(ctx, swapReceipt.Coin().ID(), nil, nil) if err != nil { t.Fatalf("error finding confirmed redemption: %v", err) } @@ -525,7 +525,7 @@ func Run(t *testing.T, cfg *Config) { mine() } - coinID, err := rig.secondWallet.Refund(swapReceipt.Coin().ID(), swapReceipt.Contract(), 100) + coinID, err := rig.secondWallet.Refund(swapReceipt.Coin().ID(), swapReceipt.Contract(), nil, 100) if err != nil { t.Fatalf("refund error: %v", err) } diff --git a/client/asset/dcr/dcr.go b/client/asset/dcr/dcr.go index e7989e8faa..b138db31f9 100644 --- a/client/asset/dcr/dcr.go +++ b/client/asset/dcr/dcr.go @@ -233,6 +233,7 @@ var ( ConfigOpts: append(rpcOpts, walletOpts...), }, }, + ProtocolVersions: []uint32{0}, } swapFeeBumpKey = "swapfeebump" splitKey = "swapsplit" @@ -2520,12 +2521,8 @@ func (dcr *ExchangeWallet) lookupTxOutput(ctx context.Context, txHash *chainhash // LocktimeExpired returns true if the specified contract's locktime has // expired, making it possible to issue a Refund. -func (dcr *ExchangeWallet) LocktimeExpired(ctx context.Context, contract dex.Bytes) (bool, time.Time, error) { - _, _, locktime, _, err := dexdcr.ExtractSwapDetails(contract, dcr.chainParams) - if err != nil { - return false, time.Time{}, fmt.Errorf("error extracting contract locktime: %w", err) - } - contractExpiry := time.Unix(int64(locktime), 0).UTC() +func (dcr *ExchangeWallet) LocktimeExpired(ctx context.Context, deets *dex.SwapContractDetails) (bool, time.Time, error) { + contractExpiry := time.Unix(int64(deets.LockTime), 0).UTC() dcr.tipMtx.RLock() blockHash := dcr.currentTip.hash dcr.tipMtx.RUnlock() @@ -2542,7 +2539,7 @@ func (dcr *ExchangeWallet) LocktimeExpired(ctx context.Context, contract dex.Byt // // This method blocks until the redemption is found, an error occurs or the // provided context is canceled. -func (dcr *ExchangeWallet) FindRedemption(ctx context.Context, coinID, _ dex.Bytes) (redemptionCoin, secret dex.Bytes, err error) { +func (dcr *ExchangeWallet) FindRedemption(ctx context.Context, coinID, _ dex.Bytes, _ *dex.SwapContractDetails) (redemptionCoin, secret dex.Bytes, err error) { txHash, vout, err := decodeCoinID(coinID) if err != nil { return nil, nil, fmt.Errorf("cannot decode contract coin id: %w", err) @@ -2906,7 +2903,7 @@ func (dcr *ExchangeWallet) fatalFindRedemptionsError(err error, contractOutpoint // wallet does not store it, even though it was known when the init transaction // was created. The client should store this information for persistence across // sessions. -func (dcr *ExchangeWallet) Refund(coinID, contract dex.Bytes, feeRate uint64) (dex.Bytes, error) { +func (dcr *ExchangeWallet) Refund(coinID, contract dex.Bytes, _ *dex.SwapContractDetails, feeRate uint64) (dex.Bytes, error) { // Caller should provide a non-zero fee rate, so we could just do // dcr.feeRateWithFallback(feeRate), but be permissive for now. if feeRate == 0 { @@ -3124,7 +3121,7 @@ func (dcr *ExchangeWallet) ValidateSecret(secret, secretHash []byte) bool { // cannot see non-wallet transactions until they are mined. // // If the coin is located, but recognized as spent, no error is returned. -func (dcr *ExchangeWallet) SwapConfirmations(ctx context.Context, coinID, contract dex.Bytes, matchTime time.Time) (confs uint32, spent bool, err error) { +func (dcr *ExchangeWallet) SwapConfirmations(ctx context.Context, coinID, contract dex.Bytes, matchTime time.Time, _ *dex.SwapContractDetails) (confs uint32, spent bool, err error) { txHash, vout, err := decodeCoinID(coinID) if err != nil { return 0, false, err diff --git a/client/asset/dcr/dcr_test.go b/client/asset/dcr/dcr_test.go index 6d8c136966..b2cc466947 100644 --- a/client/asset/dcr/dcr_test.go +++ b/client/asset/dcr/dcr_test.go @@ -1819,7 +1819,7 @@ func TestFindRedemption(t *testing.T) { // Add the redemption to mempool and check if wallet.FindRedemption finds it. redeemTx := makeRawTx(inputs, []dex.Bytes{otherScript}) node.blockchain.addRawTx(-1, redeemTx) - _, checkSecret, err := wallet.FindRedemption(tCtx, coinID, nil) + _, checkSecret, err := wallet.FindRedemption(tCtx, coinID, nil, nil) if err != nil { t.Fatalf("error finding redemption: %v", err) } @@ -1829,7 +1829,7 @@ func TestFindRedemption(t *testing.T) { // Move the redemption to a new block and check if wallet.FindRedemption finds it. _, redeemBlock := node.blockchain.addRawTx(contractHeight+2, makeRawTx(inputs, []dex.Bytes{otherScript})) - _, checkSecret, err = wallet.FindRedemption(tCtx, coinID, nil) + _, checkSecret, err = wallet.FindRedemption(tCtx, coinID, nil, nil) if err != nil { t.Fatalf("error finding redemption: %v", err) } @@ -1839,7 +1839,7 @@ func TestFindRedemption(t *testing.T) { // gettransaction error node.walletTxErr = tErr - _, _, err = wallet.FindRedemption(tCtx, coinID, nil) + _, _, err = wallet.FindRedemption(tCtx, coinID, nil, nil) if err == nil { t.Fatalf("no error for gettransaction rpc error") } @@ -1847,7 +1847,7 @@ func TestFindRedemption(t *testing.T) { // getcfilterv2 error node.rawErr[methodGetCFilterV2] = tErr - _, _, err = wallet.FindRedemption(tCtx, coinID, nil) + _, _, err = wallet.FindRedemption(tCtx, coinID, nil, nil) if err == nil { t.Fatalf("no error for getcfilterv2 rpc error") } @@ -1857,7 +1857,7 @@ func TestFindRedemption(t *testing.T) { redeemBlock.Transactions[0].TxIn[1].PreviousOutPoint.Hash = chainhash.Hash{} ctx, cancel := context.WithTimeout(tCtx, 2*time.Second) defer cancel() // ctx should auto-cancel after 2 seconds, but this is apparently good practice to prevent leak - _, k, err := wallet.FindRedemption(ctx, coinID, nil) + _, k, err := wallet.FindRedemption(ctx, coinID, nil, nil) if ctx.Err() == nil || k != nil { // Expected ctx to cancel after timeout and no secret should be found. t.Fatalf("unexpected result for missing redemption: secret: %v, err: %v", k, err) @@ -1867,14 +1867,14 @@ func TestFindRedemption(t *testing.T) { // Canceled context deadCtx, cancelCtx := context.WithCancel(tCtx) cancelCtx() - _, _, err = wallet.FindRedemption(deadCtx, coinID, nil) + _, _, err = wallet.FindRedemption(deadCtx, coinID, nil, nil) if err == nil { t.Fatalf("no error for canceled context") } // Expect FindRedemption to error because of bad input sig. redeemBlock.Transactions[0].TxIn[1].SignatureScript = randBytes(100) - _, _, err = wallet.FindRedemption(tCtx, coinID, nil) + _, _, err = wallet.FindRedemption(tCtx, coinID, nil, nil) if err == nil { t.Fatalf("no error for wrong redemption") } @@ -1882,14 +1882,14 @@ func TestFindRedemption(t *testing.T) { // Wrong script type for output node.walletTx.Hex, _ = makeTxHex(inputs, []dex.Bytes{otherScript, otherScript}) - _, _, err = wallet.FindRedemption(tCtx, coinID, nil) + _, _, err = wallet.FindRedemption(tCtx, coinID, nil, nil) if err == nil { t.Fatalf("no error for wrong script type") } node.walletTx.Hex = txHex // Sanity check to make sure it passes again. - _, _, err = wallet.FindRedemption(tCtx, coinID, nil) + _, _, err = wallet.FindRedemption(tCtx, coinID, nil, nil) if err != nil { t.Fatalf("error after clearing errors: %v", err) } @@ -1932,7 +1932,7 @@ func TestRefund(t *testing.T) { } contractOutput := newOutput(tTxHash, 0, 1e8, wire.TxTreeRegular) - _, err = wallet.Refund(contractOutput.ID(), contract, feeSuggestion) + _, err = wallet.Refund(contractOutput.ID(), contract, nil, feeSuggestion) if err != nil { t.Fatalf("refund error: %v", err) } @@ -1941,14 +1941,14 @@ func TestRefund(t *testing.T) { badReceipt := &tReceipt{ coin: &tCoin{id: make([]byte, 15)}, } - _, err = wallet.Refund(badReceipt.coin.id, badReceipt.contract, feeSuggestion) + _, err = wallet.Refund(badReceipt.coin.id, badReceipt.contract, nil, feeSuggestion) if err == nil { t.Fatalf("no error for bad receipt") } // gettxout error node.txOutErr = tErr - _, err = wallet.Refund(contractOutput.ID(), contract, feeSuggestion) + _, err = wallet.Refund(contractOutput.ID(), contract, nil, feeSuggestion) if err == nil { t.Fatalf("no error for missing utxo") } @@ -1956,14 +1956,14 @@ func TestRefund(t *testing.T) { // bad contract badContractOutput := newOutput(tTxHash, 0, 1e8, wire.TxTreeRegular) - _, err = wallet.Refund(badContractOutput.ID(), randBytes(50), feeSuggestion) + _, err = wallet.Refund(badContractOutput.ID(), randBytes(50), nil, feeSuggestion) if err == nil { t.Fatalf("no error for bad contract") } // Too small. node.txOutRes[bigOutID] = newTxOutResult(nil, 100, 2) - _, err = wallet.Refund(contractOutput.ID(), contract, feeSuggestion) + _, err = wallet.Refund(contractOutput.ID(), contract, nil, feeSuggestion) if err == nil { t.Fatalf("no error for value < fees") } @@ -1971,7 +1971,7 @@ func TestRefund(t *testing.T) { // signature error node.privWIFErr = tErr - _, err = wallet.Refund(contractOutput.ID(), contract, feeSuggestion) + _, err = wallet.Refund(contractOutput.ID(), contract, nil, feeSuggestion) if err == nil { t.Fatalf("no error for dumpprivkey rpc error") } @@ -1979,7 +1979,7 @@ func TestRefund(t *testing.T) { // send error node.sendRawErr = tErr - _, err = wallet.Refund(contractOutput.ID(), contract, feeSuggestion) + _, err = wallet.Refund(contractOutput.ID(), contract, nil, feeSuggestion) if err == nil { t.Fatalf("no error for sendrawtransaction rpc error") } @@ -1989,14 +1989,14 @@ func TestRefund(t *testing.T) { var badHash chainhash.Hash badHash[0] = 0x05 node.sendRawHash = &badHash - _, err = wallet.Refund(contractOutput.ID(), contract, feeSuggestion) + _, err = wallet.Refund(contractOutput.ID(), contract, nil, feeSuggestion) if err == nil { t.Fatalf("no error for tx hash") } node.sendRawHash = nil // Sanity check that we can succeed again. - _, err = wallet.Refund(contractOutput.ID(), contract, feeSuggestion) + _, err = wallet.Refund(contractOutput.ID(), contract, nil, feeSuggestion) if err != nil { t.Fatalf("re-refund error: %v", err) } diff --git a/client/asset/dcr/simnet_test.go b/client/asset/dcr/simnet_test.go index d6d71a25c7..2dc95be91d 100644 --- a/client/asset/dcr/simnet_test.go +++ b/client/asset/dcr/simnet_test.go @@ -274,7 +274,7 @@ func runTest(t *testing.T, splitTx bool) { confContract := receipts[0].Contract() checkConfs := func(n uint32, expSpent bool) { t.Helper() - confs, spent, err := rig.beta().SwapConfirmations(tCtx, confCoin.ID(), confContract, tStart) + confs, spent, err := rig.beta().SwapConfirmations(tCtx, confCoin.ID(), confContract, tStart, nil) if err != nil { t.Fatalf("error getting %d confs: %v", n, err) } @@ -324,7 +324,7 @@ func runTest(t *testing.T, splitTx bool) { if swapOutput.Value() != swapVal { t.Fatalf("wrong contract value. wanted %d, got %d", swapVal, swapOutput.Value()) } - confs, spent, err := rig.alpha().SwapConfirmations(context.TODO(), swapOutput.ID(), receipt.Contract(), tStart) + confs, spent, err := rig.alpha().SwapConfirmations(context.TODO(), swapOutput.ID(), receipt.Contract(), tStart, nil) if err != nil { t.Fatalf("error getting confirmations: %v", err) } @@ -364,7 +364,7 @@ func runTest(t *testing.T, splitTx bool) { waitNetwork() ctx, cancel := context.WithDeadline(tCtx, time.Now().Add(time.Second*5)) defer cancel() - _, checkKey, err := rig.beta().FindRedemption(ctx, swapReceipt.Coin().ID(), nil) + _, checkKey, err := rig.beta().FindRedemption(ctx, swapReceipt.Coin().ID(), nil, nil) if err != nil { t.Fatalf("error finding unconfirmed redemption: %v", err) } @@ -384,7 +384,7 @@ func runTest(t *testing.T, splitTx bool) { } ctx, cancel2 := context.WithDeadline(tCtx, time.Now().Add(time.Second*5)) defer cancel2() - _, _, err = rig.beta().FindRedemption(ctx, swapReceipt.Coin().ID(), nil) + _, _, err = rig.beta().FindRedemption(ctx, swapReceipt.Coin().ID(), nil, nil) if err != nil { t.Fatalf("error finding confirmed redemption: %v", err) } @@ -420,7 +420,7 @@ func runTest(t *testing.T, splitTx bool) { swapReceipt = receipts[0] waitNetwork() - _, err = rig.beta().Refund(swapReceipt.Coin().ID(), swapReceipt.Contract(), tDCR.MaxFeeRate/4) + _, err = rig.beta().Refund(swapReceipt.Coin().ID(), swapReceipt.Contract(), nil, tDCR.MaxFeeRate/4) if err != nil { t.Fatalf("refund error: %v", err) } diff --git a/client/asset/doge/doge.go b/client/asset/doge/doge.go index fef2a815f2..b27c118499 100644 --- a/client/asset/doge/doge.go +++ b/client/asset/doge/doge.go @@ -88,6 +88,7 @@ var ( DefaultConfigPath: dexbtc.SystemConfigPath("dogecoin"), ConfigOpts: configOpts, }}, + ProtocolVersions: []uint32{0}, } ) diff --git a/client/asset/eth/contractor.go b/client/asset/eth/contractor.go index fa1f029a5c..4a313e6382 100644 --- a/client/asset/eth/contractor.go +++ b/client/asset/eth/contractor.go @@ -6,8 +6,10 @@ package eth import ( + "bytes" "context" "crypto/sha256" + "errors" "fmt" "math/big" "time" @@ -17,8 +19,10 @@ import ( "decred.org/dcrdex/dex/encode" "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" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/abi/bind" @@ -30,20 +34,20 @@ import ( // The intention is that if a new contract is implemented, the contractor // interface itself will not require any updates. type contractor interface { - swap(ctx context.Context, secretHash [32]byte) (*dexeth.SwapState, error) - initiate(*bind.TransactOpts, []*asset.Contract) (*types.Transaction, error) + status(context.Context, *dex.SwapContractDetails) (dexeth.SwapStep, [32]byte, uint32, error) + initiate(*bind.TransactOpts, []*dex.SwapContractDetails) (*types.Transaction, error) redeem(txOpts *bind.TransactOpts, redeems []*asset.Redemption) (*types.Transaction, error) - refund(opts *bind.TransactOpts, secretHash [32]byte) (*types.Transaction, error) + refund(*bind.TransactOpts, *dex.SwapContractDetails) (*types.Transaction, error) estimateInitGas(ctx context.Context, n int) (uint64, error) - estimateRedeemGas(ctx context.Context, secrets [][32]byte) (uint64, error) - estimateRefundGas(ctx context.Context, secretHash [32]byte) (uint64, error) - isRedeemable(secretHash, secret [32]byte) (bool, error) + estimateRedeemGas(ctx context.Context, secrets [][32]byte, contracts []*dex.SwapContractDetails) (uint64, error) + estimateRefundGas(ctx context.Context, contract *dex.SwapContractDetails) (uint64, error) + isRedeemable(secret [32]byte, contract *dex.SwapContractDetails) (bool, error) // value checks the incoming or outgoing contract value. This is just the // one of redeem, refund, or initiate values. It is not an error if the // transaction does not pay to the contract, and the values returned in that // case will always be zero. value(context.Context, *types.Transaction) (incoming, outgoing uint64, err error) - isRefundable(secretHash [32]byte) (bool, error) + isRefundable(contract *dex.SwapContractDetails) (bool, error) } // tokenContractor interacts with an ERC20 token contract and a token swap @@ -73,6 +77,18 @@ type contractV0 interface { IsRefundable(opts *bind.CallOpts, secretHash [32]byte) (bool, error) } +var _ contractV0 = (*swapv0.ETHSwap)(nil) + +type contractV1 interface { + Initiate(opts *bind.TransactOpts, contracts []swapv1.ETHSwapContract) (*types.Transaction, error) + Redeem(opts *bind.TransactOpts, redemptions []swapv1.ETHSwapRedemption) (*types.Transaction, error) + State(opts *bind.CallOpts, c swapv1.ETHSwapContract) (swapv1.ETHSwapRecord, error) + Refund(opts *bind.TransactOpts, c swapv1.ETHSwapContract) (*types.Transaction, error) + IsRedeemable(opts *bind.CallOpts, c swapv1.ETHSwapContract) (bool, error) +} + +var _ contractV1 = (*swapv1.ETHSwap)(nil) + // contractorV0 is the contractor for contract version 0. // Redeem and Refund methods of swapv0.ETHSwap already have suitable return types. type contractorV0 struct { @@ -113,32 +129,31 @@ func newV0Contractor(net dex.Network, acctAddr common.Address, cb bind.ContractB } // initiate sends the initiations to the swap contract's initiate function. -func (c *contractorV0) initiate(txOpts *bind.TransactOpts, contracts []*asset.Contract) (*types.Transaction, error) { - inits := make([]swapv0.ETHSwapInitiation, 0, len(contracts)) - secrets := make(map[[32]byte]bool, len(contracts)) +func (c *contractorV0) initiate(txOpts *bind.TransactOpts, details []*dex.SwapContractDetails) (*types.Transaction, error) { + inits := make([]swapv0.ETHSwapInitiation, 0, len(details)) + secrets := make(map[[32]byte]bool, len(details)) - for _, contract := range contracts { - if len(contract.SecretHash) != dexeth.SecretHashSize { - return nil, fmt.Errorf("wrong secret hash length. wanted %d, got %d", dexeth.SecretHashSize, len(contract.SecretHash)) + for _, deets := range details { + if len(deets.SecretHash) != dexeth.SecretHashSize { + return nil, fmt.Errorf("wrong secret hash length. wanted %d, got %d", dexeth.SecretHashSize, len(deets.SecretHash)) } var secretHash [32]byte - copy(secretHash[:], contract.SecretHash) + copy(secretHash[:], deets.SecretHash) if secrets[secretHash] { - return nil, fmt.Errorf("secret hash %s is a duplicate", contract.SecretHash) + return nil, fmt.Errorf("secret hash %s is a duplicate", deets.SecretHash) } secrets[secretHash] = true - if !common.IsHexAddress(contract.Address) { - return nil, fmt.Errorf("%q is not an address", contract.Address) + if !common.IsHexAddress(deets.To) { + return nil, fmt.Errorf("%q is not an address", deets.To) } - inits = append(inits, swapv0.ETHSwapInitiation{ - RefundTimestamp: big.NewInt(int64(contract.LockTime)), + RefundTimestamp: big.NewInt(int64(deets.LockTime)), SecretHash: secretHash, - Participant: common.HexToAddress(contract.Address), - Value: c.evmify(contract.Value), + Participant: common.HexToAddress(deets.To), + Value: c.evmify(deets.Value), }) } @@ -150,7 +165,7 @@ func (c *contractorV0) redeem(txOpts *bind.TransactOpts, redemptions []*asset.Re redemps := make([]swapv0.ETHSwapRedemption, 0, len(redemptions)) secretHashes := make(map[[32]byte]bool, len(redemptions)) for _, r := range redemptions { - secretB, secretHashB := r.Secret, r.Spends.SecretHash + secretB, secretHashB := r.Secret, r.SwapDetails.SecretHash if len(secretB) != 32 || len(secretHashB) != 32 { return nil, fmt.Errorf("invalid secret and/or secret hash sizes, %d and %d", len(secretB), len(secretHashB)) } @@ -180,7 +195,6 @@ func (c *contractorV0) swap(ctx context.Context, secretHash [32]byte) (*dexeth.S if err != nil { return nil, err } - return &dexeth.SwapState{ BlockHeight: state.InitBlockNumber.Uint64(), LockTime: time.Unix(state.RefundBlockTimestamp.Int64(), 0), @@ -192,26 +206,51 @@ func (c *contractorV0) swap(ctx context.Context, secretHash [32]byte) (*dexeth.S }, nil } +func (c *contractorV0) status(ctx context.Context, contract *dex.SwapContractDetails) (step dexeth.SwapStep, secret [32]byte, blockNumber uint32, err error) { + var secretHash [32]byte + copy(secretHash[:], contract.SecretHash) + swap, err := c.swap(ctx, secretHash) + if err != nil { + return + } + return swap.State, swap.Secret, uint32(swap.BlockHeight), nil +} + +func (c *contractorV0) refundImpl(txOpts *bind.TransactOpts, secretHash [32]byte) (*types.Transaction, error) { + return c.contractV0.Refund(txOpts, secretHash) +} + // refund issues the refund command to the swap contract. Use isRefundable first // to ensure the refund will be accepted. -func (c *contractorV0) refund(txOpts *bind.TransactOpts, secretHash [32]byte) (*types.Transaction, error) { - return c.contractV0.Refund(txOpts, secretHash) +func (c *contractorV0) refund(txOpts *bind.TransactOpts, contract *dex.SwapContractDetails) (*types.Transaction, error) { + var secretHash [32]byte + copy(secretHash[:], contract.SecretHash) + return c.refundImpl(txOpts, secretHash) } -// isRedeemable exposes the isRedeemable method of the swap contract. -func (c *contractorV0) isRedeemable(secretHash, secret [32]byte) (bool, error) { +func (c *contractorV0) isRedeemableImpl(secretHash, secret [32]byte) (bool, error) { return c.contractV0.IsRedeemable(&bind.CallOpts{From: c.acctAddr}, secretHash, secret) } -// isRefundable exposes the isRefundable method of the swap contract. -func (c *contractorV0) isRefundable(secretHash [32]byte) (bool, error) { +// isRedeemable exposes the isRedeemable method of the swap contract. +func (c *contractorV0) isRedeemable(secret [32]byte, contract *dex.SwapContractDetails) (bool, error) { + var secretHash [32]byte + copy(secretHash[:], contract.SecretHash) + return c.isRedeemableImpl(secretHash, secret) +} + +func (c *contractorV0) isRefundableImpl(secretHash [32]byte) (bool, error) { return c.contractV0.IsRefundable(&bind.CallOpts{From: c.acctAddr}, secretHash) } -// estimateRedeemGas estimates the gas used to redeem. The secret hashes -// supplied must reference existing swaps, so this method can't be used until -// the swap is initiated. -func (c *contractorV0) estimateRedeemGas(ctx context.Context, secrets [][32]byte) (uint64, error) { +// isRefundable exposes the isRefundable method of the swap contract. +func (c *contractorV0) isRefundable(contract *dex.SwapContractDetails) (bool, error) { + var secretHash [32]byte + copy(secretHash[:], contract.SecretHash) + return c.isRefundableImpl(secretHash) +} + +func (c *contractorV0) estimateRedeemGasImpl(ctx context.Context, secrets [][32]byte) (uint64, error) { redemps := make([]swapv0.ETHSwapRedemption, 0, len(secrets)) for _, secret := range secrets { redemps = append(redemps, swapv0.ETHSwapRedemption{ @@ -222,11 +261,24 @@ func (c *contractorV0) estimateRedeemGas(ctx context.Context, secrets [][32]byte return c.estimateGas(ctx, nil, "redeem", redemps) } +// estimateRedeemGas estimates the gas used to redeem. The secret hashes +// supplied must reference existing swaps, so this method can't be used until +// the swap is initiated. +func (c *contractorV0) estimateRedeemGas(ctx context.Context, secrets [][32]byte, _ []*dex.SwapContractDetails) (uint64, error) { + return c.estimateRedeemGasImpl(ctx, secrets) +} + +func (c *contractorV0) estimateRefundGasImpl(ctx context.Context, secretHash [32]byte) (uint64, error) { + return c.estimateGas(ctx, nil, "refund", secretHash) +} + // estimateRefundGas estimates the gas used to refund. The secret hashes // supplied must reference existing swaps that are refundable, so this method // can't be used until the swap is initiated and the lock time has expired. -func (c *contractorV0) estimateRefundGas(ctx context.Context, secretHash [32]byte) (uint64, error) { - return c.estimateGas(ctx, nil, "refund", secretHash) +func (c *contractorV0) estimateRefundGas(ctx context.Context, deets *dex.SwapContractDetails) (uint64, error) { + var secretHash [32]byte + copy(secretHash[:], deets.SecretHash) + return c.estimateRefundGasImpl(ctx, secretHash) } // estimateInitGas estimates the gas used to initiate n generic swaps. The @@ -255,17 +307,7 @@ func (c *contractorV0) estimateInitGas(ctx context.Context, n int) (uint64, erro // estimateGas estimates the gas used to interact with the swap contract. func (c *contractorV0) estimateGas(ctx context.Context, value *big.Int, method string, args ...interface{}) (uint64, error) { - data, err := c.abi.Pack(method, args...) - if err != nil { - return 0, fmt.Errorf("Pack error: %v", err) - } - - return c.cb.EstimateGas(ctx, ethereum.CallMsg{ - From: c.acctAddr, - To: &c.contractAddr, - Data: data, - Value: value, - }) + return estimateGas(ctx, c.acctAddr, c.contractAddr, c.abi, c.cb, value, method, args...) } // value calculates the incoming or outgoing value of the transaction, excluding @@ -286,10 +328,10 @@ func (c *contractorV0) value(ctx context.Context, tx *types.Transaction) (in, ou // incomingValue calculates the value being redeemed for refunded in the tx. func (c *contractorV0) incomingValue(ctx context.Context, tx *types.Transaction) (uint64, error) { - if redeems, err := dexeth.ParseRedeemData(tx.Data(), 0); err == nil { + if redeems, err := dexeth.ParseRedeemDataV0(tx.Data()); err == nil { var redeemed uint64 for _, redeem := range redeems { - swap, err := c.swap(ctx, redeem.SecretHash) + swap, err := c.contractV0.Swap(readOnlyCallOpts(ctx), redeem.SecretHash) if err != nil { return 0, fmt.Errorf("redeem swap error: %w", err) } @@ -297,11 +339,11 @@ func (c *contractorV0) incomingValue(ctx context.Context, tx *types.Transaction) } return redeemed, nil } - secretHash, err := dexeth.ParseRefundData(tx.Data(), 0) + secretHash, err := dexeth.ParseRefundDataV0(tx.Data()) if err != nil { return 0, nil } - swap, err := c.swap(ctx, secretHash) + swap, err := c.contractV0.Swap(readOnlyCallOpts(ctx), secretHash) if err != nil { return 0, fmt.Errorf("refund swap error: %w", err) } @@ -310,7 +352,7 @@ func (c *contractorV0) incomingValue(ctx context.Context, tx *types.Transaction) // outgoingValue calculates the value sent in swaps in the tx. func (c *contractorV0) outgoingValue(tx *types.Transaction) (swapped uint64) { - if inits, err := dexeth.ParseInitiateData(tx.Data(), 0); err == nil { + if inits, err := dexeth.ParseInitiateDataV0(tx.Data()); err == nil { for _, init := range inits { swapped += c.atomize(init.Value) } @@ -318,12 +360,51 @@ func (c *contractorV0) outgoingValue(tx *types.Transaction) (swapped uint64) { return } +// erc20Contractor supports the ERC20 ABI. Embedded in token contractors. +type erc20Contractor struct { + tokenContract *erc20.IERC20 + acct common.Address + contract common.Address +} + +// balance exposes the read-only balanceOf method of the erc20 token contract. +func (c *erc20Contractor) balance(ctx context.Context) (*big.Int, error) { + callOpts := &bind.CallOpts{ + From: c.acct, + Context: ctx, + } + + return c.tokenContract.BalanceOf(callOpts, c.acct) +} + +// allowance exposes the read-only allowance method of the erc20 token contract. +func (c *erc20Contractor) allowance(ctx context.Context) (*big.Int, error) { + callOpts := &bind.CallOpts{ + Pending: true, + From: c.acct, + Context: ctx, + } + return c.tokenContract.Allowance(callOpts, c.acct, c.contract) +} + +// approve sends an approve transaction approving the linked contract to call +// transferFrom for the specified amount. +func (c *erc20Contractor) approve(txOpts *bind.TransactOpts, amount *big.Int) (*types.Transaction, error) { + return c.tokenContract.Approve(txOpts, c.contract, amount) +} + +// transfer calls the transfer method of the erc20 token contract. Used for +// sends or withdrawals. +func (c *erc20Contractor) transfer(txOpts *bind.TransactOpts, addr common.Address, amount *big.Int) (*types.Transaction, error) { + return c.tokenContract.Transfer(txOpts, addr, amount) +} + // tokenContractorV0 is a contractor that implements the tokenContractor // methods, providing access to the methods of the token's ERC20 contract. type tokenContractorV0 struct { *contractorV0 - tokenAddr common.Address - tokenContract *erc20.IERC20 + *erc20Contractor + tokenAddr common.Address } var _ contractor = (*tokenContractorV0)(nil) @@ -366,77 +447,315 @@ func newV0TokenContractor(net dex.Network, assetID uint32, acctAddr common.Addre evmify: token.AtomicToEVM, atomize: token.EVMToAtomic, }, - tokenAddr: tokenAddr, - tokenContract: tokenContract, + tokenAddr: tokenAddr, + erc20Contractor: &erc20Contractor{ + tokenContract: tokenContract, + acct: acctAddr, + contract: swapContractAddr, + }, }, nil } -// balance exposes the read-only balanceOf method of the erc20 token contract. -func (c *tokenContractorV0) balance(ctx context.Context) (*big.Int, error) { - callOpts := &bind.CallOpts{ - From: c.acctAddr, - Context: ctx, +// estimateApproveGas estimates the gas needed to send an approve tx. +func (c *tokenContractorV0) estimateApproveGas(ctx context.Context, amount *big.Int) (uint64, error) { + return c.estimateGas(ctx, "approve", c.contractAddr, amount) +} + +// estimateTransferGas esimates the gas needed for a transfer tx. The account +// needs to have > amount tokens to use this method. +func (c *tokenContractorV0) estimateTransferGas(ctx context.Context, amount *big.Int) (uint64, error) { + return c.estimateGas(ctx, "transfer", c.acctAddr, amount) +} + +// estimateGas estimates the gas needed for methods on the ERC20 token contract. +// For estimating methods on the swap contract, use (contractorV0).estimateGas. +func (c *tokenContractorV0) estimateGas(ctx context.Context, method string, args ...interface{}) (uint64, error) { + return estimateGas(ctx, c.acctAddr, c.contractAddr, c.abi, c.cb, new(big.Int), method, args...) +} + +// value finds incoming or outgoing value for the tx to either the swap contract +// or the erc20 token contract. For the token contract, only transfer and +// transferFrom are parsed. It is not an error if this tx is a call to another +// method of the token contract, but no values will be parsed. +func (c *tokenContractorV0) value(ctx context.Context, tx *types.Transaction) (in, out uint64, err error) { + to := *tx.To() + if to == c.contractAddr { + return c.contractorV0.value(ctx, tx) + } + if to != c.tokenAddr { + return 0, 0, nil + } + + // Consider removing. We'll never be sending transferFrom transactions + // directly. + if sender, _, value, err := erc20.ParseTransferFromData(tx.Data()); err == nil && sender == c.acctAddr { + return 0, c.atomize(value), nil } - return c.tokenContract.BalanceOf(callOpts, c.acctAddr) + if _, value, err := erc20.ParseTransferData(tx.Data()); err == nil { + return 0, c.atomize(value), nil + } + + return 0, 0, nil } -// allowance exposes the read-only allowance method of the erc20 token contract. -func (c *tokenContractorV0) allowance(ctx context.Context) (*big.Int, error) { - callOpts := &bind.CallOpts{ - Pending: true, - From: c.acctAddr, - Context: ctx, +// tokenAddress exposes the token_address immutable address of the token-bound +// swap contract. +func (c *tokenContractorV0) tokenAddress() common.Address { + return c.tokenAddr +} + +type contractorV1 struct { + contractV1 + abi *abi.ABI + net dex.Network + contractAddr common.Address + acctAddr common.Address + cb bind.ContractBackend + isToken bool + evmify func(uint64) *big.Int + atomize func(*big.Int) uint64 +} + +var _ contractor = (*contractorV1)(nil) + +func newV1Contractor(net dex.Network, acctAddr common.Address, cb bind.ContractBackend) (contractor, error) { + contractAddr, exists := dexeth.ContractAddresses[1][net] + if !exists || contractAddr == (common.Address{}) { + return nil, fmt.Errorf("no contract address for version 0, net %s", net) } - return c.tokenContract.Allowance(callOpts, c.acctAddr, c.contractAddr) + c, err := swapv1.NewETHSwap(contractAddr, cb) + if err != nil { + return nil, err + } + return &contractorV1{ + contractV1: c, + abi: dexeth.ABIs[1], + net: net, + contractAddr: contractAddr, + acctAddr: acctAddr, + cb: cb, + atomize: dexeth.WeiToGwei, + }, nil } -// approve sends an approve transaction approving the linked contract to call -// transferFrom for the specified amount. -func (c *tokenContractorV0) approve(txOpts *bind.TransactOpts, amount *big.Int) (*types.Transaction, error) { - return c.tokenContract.Approve(txOpts, c.contractAddr, amount) +func (c *contractorV1) status(ctx context.Context, deets *dex.SwapContractDetails) (step dexeth.SwapStep, secret [32]byte, blockNumber uint32, err error) { + rec, err := c.State(&bind.CallOpts{From: c.acctAddr}, dexeth.SwapToV1(deets)) + if err != nil { + return 0, secret, 0, err + } + return dexeth.SwapStep(rec.State), rec.Secret, uint32(rec.BlockNumber.Uint64()), err } -// transfer calls the transfer method of the erc20 token contract. Used for -// sends or withdrawals. -func (c *tokenContractorV0) transfer(txOpts *bind.TransactOpts, addr common.Address, amount *big.Int) (*types.Transaction, error) { - return c.tokenContract.Transfer(txOpts, addr, amount) +func (c *contractorV1) initiate(txOpts *bind.TransactOpts, details []*dex.SwapContractDetails) (*types.Transaction, error) { + versionedContracts := make([]swapv1.ETHSwapContract, 0, len(details)) + for _, deets := range details { + versionedContracts = append(versionedContracts, dexeth.SwapToV1(deets)) + } + return c.Initiate(txOpts, versionedContracts) +} + +func (c *contractorV1) redeem(txOpts *bind.TransactOpts, redeems []*asset.Redemption) (*types.Transaction, error) { + versionedRedemptions := make([]swapv1.ETHSwapRedemption, 0, len(redeems)) + secretHashes := make(map[[32]byte]bool, len(redeems)) + for _, r := range redeems { + var secret [32]byte + copy(secret[:], r.Secret) + secretHash := sha256.Sum256(r.Secret) + if !bytes.Equal(secretHash[:], r.SwapDetails.SecretHash) { + return nil, errors.New("wrong secret hash") + } + if secretHashes[secretHash] { + return nil, fmt.Errorf("duplicate secret hash %x", secretHash[:]) + } + secretHashes[secretHash] = true + versionedRedemptions = append(versionedRedemptions, RedemptionToV1(secret, r.SwapDetails)) + } + return c.Redeem(txOpts, versionedRedemptions) +} + +func (c *contractorV1) refund(txOpts *bind.TransactOpts, contract *dex.SwapContractDetails) (*types.Transaction, error) { + return c.Refund(txOpts, dexeth.SwapToV1(contract)) +} + +func (c *contractorV1) estimateInitGas(ctx context.Context, n int) (uint64, error) { + initiations := make([]swapv1.ETHSwapContract, 0, n) + for j := 0; j < n; j++ { + var secretHash [32]byte + copy(secretHash[:], encode.RandomBytes(32)) + initiations = append(initiations, swapv1.ETHSwapContract{ + RefundTimestamp: 1, + SecretHash: secretHash, + Initiator: c.acctAddr, + Participant: common.BytesToAddress(encode.RandomBytes(20)), + Value: 1, + }) + } + + var value *big.Int + if !c.isToken { + value = dexeth.GweiToWei(uint64(n)) + } + + return c.estimateGas(ctx, value, "initiate", initiations) +} + +// estimateGas estimates the gas used to interact with the swap contract. +func (c *contractorV1) estimateGas(ctx context.Context, value *big.Int, method string, args ...interface{}) (uint64, error) { + return estimateGas(ctx, c.acctAddr, c.contractAddr, c.abi, c.cb, value, method, args...) +} + +func (c *contractorV1) estimateRedeemGas(ctx context.Context, secrets [][32]byte, details []*dex.SwapContractDetails) (uint64, error) { + if len(secrets) != len(details) { + return 0, fmt.Errorf("number of secrets (%d) does not match number of contracts (%d)", len(secrets), len(details)) + } + + redemps := make([]swapv1.ETHSwapRedemption, 0, len(secrets)) + for i, secret := range secrets { + redemps = append(redemps, swapv1.ETHSwapRedemption{ + Secret: secret, + C: dexeth.SwapToV1(details[i]), + }) + } + return c.estimateGas(ctx, nil, "redeem", redemps) +} + +func (c *contractorV1) estimateRefundGas(ctx context.Context, deets *dex.SwapContractDetails) (uint64, error) { + return c.estimateGas(ctx, nil, "refund", dexeth.SwapToV1(deets)) +} + +func (c *contractorV1) isRedeemable(secret [32]byte, deets *dex.SwapContractDetails) (bool, error) { + if is, err := c.IsRedeemable(&bind.CallOpts{From: c.acctAddr}, dexeth.SwapToV1(deets)); err != nil || !is { + return is, err + } + var secretHash [32]byte + copy(secretHash[:], deets.SecretHash) + return sha256.Sum256(secret[:]) == secretHash, nil +} + +func (c *contractorV1) isRefundable(deets *dex.SwapContractDetails) (bool, error) { + if is, err := c.IsRedeemable(&bind.CallOpts{From: c.acctAddr}, dexeth.SwapToV1(deets)); err != nil || !is { + return is, err + } + return time.Now().Unix() >= int64(deets.LockTime), nil +} + +func (c *contractorV1) incomingValue(ctx context.Context, tx *types.Transaction) (uint64, error) { + if redeems, err := dexeth.ParseRedeemDataV1(tx.Data()); err == nil { + var redeemed uint64 + for _, r := range redeems { + redeemed += r.Contract.Value + } + return redeemed, nil + } + refund, err := dexeth.ParseRefundDataV1(tx.Data()) + if err != nil { + return 0, nil + } + return refund.Value, nil +} + +// outgoingValue calculates the value sent in swaps in the tx. +func (c *contractorV1) outgoingValue(tx *types.Transaction) (swapped uint64) { + if inits, err := dexeth.ParseInitiateDataV1(tx.Data()); err == nil { + for _, init := range inits { + swapped += init.Value + } + } + return +} + +// value calculates the incoming or outgoing value of the transaction, excluding +// fees but including operations against the swap contract. +func (c *contractorV1) value(ctx context.Context, tx *types.Transaction) (in, out uint64, err error) { + if *tx.To() != c.contractAddr { + return 0, 0, nil + } + + if v, err := c.incomingValue(ctx, tx); err != nil { + return 0, 0, fmt.Errorf("incomingValue error: %w", err) + } else if v > 0 { + return v, 0, nil + } + + return 0, c.outgoingValue(tx), nil +} + +type tokenContractorV1 struct { + *contractorV1 + *erc20Contractor + tokenAddr common.Address +} + +func newV1TokenContractor(net dex.Network, assetID uint32, acctAddr common.Address, cb bind.ContractBackend) (tokenContractor, error) { + token, tokenAddr, swapContractAddr, err := dexeth.VersionedNetworkToken(assetID, 1, net) + if err != nil { + return nil, err + } + + c, err := erc20v1.NewERC20Swap(swapContractAddr, cb) + if err != nil { + return nil, err + } + + tokenContract, err := erc20.NewIERC20(tokenAddr, cb) + if err != nil { + return nil, err + } + + if boundAddr, err := c.TokenAddress(&bind.CallOpts{ + Context: context.TODO(), + }); err != nil { + return nil, fmt.Errorf("error reading bound token address %q: %w", tokenAddr, err) + } else if boundAddr != tokenAddr { + return nil, fmt.Errorf("wrong bound address. expected %s, got %s", tokenAddr, boundAddr) + } + + return &tokenContractorV1{ + contractorV1: &contractorV1{ + contractV1: c, + abi: dexeth.ABIs[1], + net: net, + contractAddr: swapContractAddr, + acctAddr: acctAddr, + cb: cb, + evmify: token.AtomicToEVM, + atomize: token.EVMToAtomic, + }, + tokenAddr: tokenAddr, + erc20Contractor: &erc20Contractor{ + tokenContract: tokenContract, + acct: acctAddr, + contract: swapContractAddr, + }, + }, nil } // estimateApproveGas estimates the gas needed to send an approve tx. -func (c *tokenContractorV0) estimateApproveGas(ctx context.Context, amount *big.Int) (uint64, error) { - return c.estimateGas(ctx, "approve", c.contractAddr, amount) +func (c *tokenContractorV1) estimateApproveGas(ctx context.Context, amount *big.Int) (uint64, error) { + return c.estimateTokenContractGas(ctx, "approve", c.contractAddr, amount) } // estimateTransferGas esimates the gas needed for a transfer tx. The account // needs to have > amount tokens to use this method. -func (c *tokenContractorV0) estimateTransferGas(ctx context.Context, amount *big.Int) (uint64, error) { - return c.estimateGas(ctx, "transfer", c.acctAddr, amount) +func (c *tokenContractorV1) estimateTransferGas(ctx context.Context, amount *big.Int) (uint64, error) { + return c.estimateTokenContractGas(ctx, "transfer", c.acctAddr, amount) } // estimateGas estimates the gas needed for methods on the ERC20 token contract. // For estimating methods on the swap contract, use (contractorV0).estimateGas. -func (c *tokenContractorV0) estimateGas(ctx context.Context, method string, args ...interface{}) (uint64, error) { - data, err := erc20.ERC20ABI.Pack(method, args...) - if err != nil { - return 0, fmt.Errorf("token estimateGas Pack error: %v", err) - } - - return c.cb.EstimateGas(ctx, ethereum.CallMsg{ - From: c.acctAddr, - To: &c.tokenAddr, - Data: data, - }) +func (c *tokenContractorV1) estimateTokenContractGas(ctx context.Context, method string, args ...interface{}) (uint64, error) { + return estimateGas(ctx, c.acctAddr, c.tokenAddr, erc20.ERC20ABI, c.cb, new(big.Int), method, args...) } // value finds incoming or outgoing value for the tx to either the swap contract // or the erc20 token contract. For the token contract, only transfer and // transferFrom are parsed. It is not an error if this tx is a call to another // method of the token contract, but no values will be parsed. -func (c *tokenContractorV0) value(ctx context.Context, tx *types.Transaction) (in, out uint64, err error) { +func (c *tokenContractorV1) value(ctx context.Context, tx *types.Transaction) (in, out uint64, err error) { to := *tx.To() if to == c.contractAddr { - return c.contractorV0.value(ctx, tx) + return c.contractorV1.value(ctx, tx) } if to != c.tokenAddr { return 0, 0, nil @@ -457,14 +776,48 @@ func (c *tokenContractorV0) value(ctx context.Context, tx *types.Transaction) (i // tokenAddress exposes the token_address immutable address of the token-bound // swap contract. -func (c *tokenContractorV0) tokenAddress() common.Address { +func (c *tokenContractorV1) tokenAddress() common.Address { return c.tokenAddr } +var _ contractor = (*tokenContractorV1)(nil) +var _ tokenContractor = (*tokenContractorV1)(nil) + +func RedemptionToV1(secret [32]byte, deets *dex.SwapContractDetails) swapv1.ETHSwapRedemption { + return swapv1.ETHSwapRedemption{ + C: dexeth.SwapToV1(deets), + Secret: secret, + } +} + +// readOnlyCallOpts is the CallOpts used for read-only contract method calls. +func readOnlyCallOpts(ctx context.Context) *bind.CallOpts { + return &bind.CallOpts{ + Pending: true, + Context: ctx, + } +} + +func estimateGas(ctx context.Context, from, to common.Address, abi *abi.ABI, cb bind.ContractBackend, value *big.Int, method string, args ...interface{}) (uint64, error) { + data, err := abi.Pack(method, args...) + if err != nil { + return 0, fmt.Errorf("Pack error: %v", err) + } + + return cb.EstimateGas(ctx, ethereum.CallMsg{ + From: from, + To: &to, + Data: data, + Value: value, + }) +} + var contractorConstructors = map[uint32]contractorConstructor{ 0: newV0Contractor, + 1: newV1Contractor, } var tokenContractorConstructors = map[uint32]tokenContractorConstructor{ 0: newV0TokenContractor, + 1: newV1TokenContractor, } diff --git a/client/asset/eth/contractor_test.go b/client/asset/eth/contractor_test.go index f0d5614db8..a9e7255547 100644 --- a/client/asset/eth/contractor_test.go +++ b/client/asset/eth/contractor_test.go @@ -9,6 +9,7 @@ import ( "testing" "decred.org/dcrdex/client/asset" + "decred.org/dcrdex/dex" "decred.org/dcrdex/dex/encode" dexeth "decred.org/dcrdex/dex/networks/eth" swapv0 "decred.org/dcrdex/dex/networks/eth/contracts/v0" @@ -60,17 +61,18 @@ func TestInitV0(t *testing.T) { const gweiVal = 123456 const lockTime = 100_000_000 - contract := &asset.Contract{ + deets := &dex.SwapContractDetails{ SecretHash: secretHashB, - Address: addrStr, + To: addrStr, + From: addrStr, Value: gweiVal, LockTime: lockTime, } - contracts := []*asset.Contract{contract} + details := []*dex.SwapContractDetails{deets} checkResult := func(tag string, wantErr bool) { - _, err := c.initiate(nil, contracts) + _, err := c.initiate(nil, details) if (err != nil) != wantErr { t.Fatal(tag) } @@ -96,29 +98,29 @@ func TestInitV0(t *testing.T) { } // wrong secret hash size - contract.SecretHash = encode.RandomBytes(20) + deets.SecretHash = encode.RandomBytes(20) checkResult("bad hash", true) - contract.SecretHash = encode.RandomBytes(32) + deets.SecretHash = encode.RandomBytes(32) // dupe hash - contracts = []*asset.Contract{contract, contract} + details = []*dex.SwapContractDetails{deets, deets} checkResult("dupe hash", true) // ok with two - contract2 := *contract - contract2.SecretHash = encode.RandomBytes(32) - contracts = []*asset.Contract{contract, &contract2} + deets2 := *deets + deets2.SecretHash = encode.RandomBytes(32) + details = []*dex.SwapContractDetails{deets, &deets2} checkResult("ok two", false) - contracts = []*asset.Contract{contract} + details = []*dex.SwapContractDetails{deets} if len(abiContract.lastInits) != 2 { t.Fatalf("two contracts weren't passed") } // bad address - contract.Address = "badaddress" + deets.To = "badaddress" checkResult("bad address", true) - contract.Address = addrStr + deets.To = addrStr // Initiate error abiContract.initErr = fmt.Errorf("test error") @@ -137,8 +139,8 @@ func TestRedeemV0(t *testing.T) { secretHashB := encode.RandomBytes(32) redemption := &asset.Redemption{ - Secret: secretB, - Spends: &asset.AuditInfo{SecretHash: secretHashB}, + Secret: secretB, + SwapDetails: &dex.SwapContractDetails{SecretHash: secretHashB}, } redemptions := []*asset.Redemption{redemption} @@ -164,9 +166,9 @@ func TestRedeemV0(t *testing.T) { } // bad secret hash length - redemption.Spends.SecretHash = encode.RandomBytes(20) + redemption.SwapDetails.SecretHash = encode.RandomBytes(20) checkResult("bad secret hash length", true) - redemption.Spends.SecretHash = encode.RandomBytes(32) + redemption.SwapDetails.SecretHash = encode.RandomBytes(32) // bad secret length redemption.Secret = encode.RandomBytes(20) @@ -184,8 +186,8 @@ func TestRedeemV0(t *testing.T) { // two OK redemption2 := &asset.Redemption{ - Secret: encode.RandomBytes(32), - Spends: &asset.AuditInfo{SecretHash: encode.RandomBytes(32)}, + Secret: encode.RandomBytes(32), + SwapDetails: &dex.SwapContractDetails{SecretHash: encode.RandomBytes(32)}, } redemptions = []*asset.Redemption{redemption, redemption2} checkResult("two ok", false) @@ -220,36 +222,24 @@ func TestSwapV0(t *testing.T) { // error path abiContract.swapErr = fmt.Errorf("test error") - _, err := c.swap(nil, [32]byte{}) + _, _, _, err := c.status(nil, &dex.SwapContractDetails{}) if err == nil { t.Fatalf("swap error not transmitted") } abiContract.swapErr = nil - swap, err := c.swap(nil, [32]byte{}) + step, secret, blockNumber, err := c.status(nil, &dex.SwapContractDetails{}) if err != nil { t.Fatalf("swap error: %v", err) } - if swap.Secret != secret { + if secret != secret { t.Fatalf("wrong secret") } - if dexeth.WeiToGwei(swap.Value) != valGwei { - t.Fatalf("wrong value") - } - if swap.BlockHeight != blockNum { + if blockNumber != blockNum { t.Fatalf("wrong block height") } - if swap.LockTime.Unix() != stamp { - t.Fatalf("wrong lock time") - } - if swap.Initiator != initiator { - t.Fatalf("initiator not transmitted") - } - if swap.Participant != participant { - t.Fatalf("participant not transmitted") - } - if swap.State != state { + if step != state { t.Fatalf("state not transmitted") } } diff --git a/client/asset/eth/eth.go b/client/asset/eth/eth.go index a8bfccf4c9..a59eda0be0 100644 --- a/client/asset/eth/eth.go +++ b/client/asset/eth/eth.go @@ -38,6 +38,7 @@ import ( "github.com/ethereum/go-ethereum/common" ethmath "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/params" "github.com/tyler-smith/go-bip39" ) @@ -65,6 +66,7 @@ const ( defaultGasFee = 82 // gwei defaultGasFeeLimit = 200 // gwei defaultSendGasLimit = 21_000 + approveGas = 4e5 walletTypeGeth = "geth" @@ -72,6 +74,9 @@ const ( // confirmations. Testing on testnet has shown spikes up to 2.5 // seconds. This value may need to be adjusted in the future. confCheckTimeout = 4 * time.Second + + contractVersionNewest = ^uint32(0) + contractVersionUnknown = contractVersionNewest - 1 ) var ( @@ -103,6 +108,8 @@ var ( Seeded: true, }, }, + Version: 0, + ProtocolVersions: []uint32{0, 1}, } chainIDs = map[dex.Network]int64{ @@ -129,6 +136,11 @@ var ( 0, // branch 0 0, // index 0 } + + // https://github.com/ethereum/go-ethereum/blob/16341e05636fd088aa04a27fca6dc5cda5dbab8f/eth/backend.go#L110-L113 + // ultimately results in a minimum fee rate by the filter applied at + // https://github.com/ethereum/go-ethereum/blob/4ebeca19d739a243dc0549bcaf014946cde95c4f/core/tx_pool.go#L626 + minGasPrice = ethconfig.Defaults.Miner.GasPrice ) // WalletConfig are wallet-level configuration settings. @@ -409,8 +421,10 @@ func (*ETHWallet) Info() *asset.WalletInfo { // Info returns basic information about the wallet and asset. func (w *TokenWallet) Info() *asset.WalletInfo { return &asset.WalletInfo{ - Name: w.token.Name, - UnitInfo: w.token.UnitInfo, + Name: w.token.Name, + UnitInfo: w.token.UnitInfo, + Version: 0, + ProtocolVersions: []uint32{0, 1}, } } @@ -428,6 +442,21 @@ func genWalletSeed(entropy []byte) ([]byte, error) { return bip39.NewSeed(mnemonic, ""), nil } +// 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 + case contractVersionNewest: + return contractVersionNewest + default: + return contractVersionUnknown + } +} + // CreateWallet creates a new internal ETH wallet and stores the private key // derived from the wallet seed. func CreateWallet(createWalletParams *asset.CreateWalletParams) error { @@ -995,8 +1024,8 @@ func (w *assetWallet) allowanceGasRequired(dexRedeemCfg *dex.Asset) (uint64, err } // gases gets the gas table for the specified contract version. -func (w *assetWallet) gases(contractVer uint32) *dexeth.Gases { - return gases(w.assetID, contractVer, w.net) +func (w *assetWallet) gases(serverVer uint32) *dexeth.Gases { + return gases(w.assetID, contractVersion(serverVer), w.net) } // PreRedeem generates an estimate of the range of redemption fees that could @@ -1372,7 +1401,7 @@ type swapReceipt struct { // lookup, but we cache these values to avoid this. expiration time.Time value uint64 - ver uint32 + serverVer uint32 contractAddr string // specified by ver, here for naive consumers } @@ -1393,7 +1422,7 @@ func (r *swapReceipt) Coin() asset.Coin { // Contract returns the swap's identifying data, which the concatenation of the // contract version and the secret hash. func (r *swapReceipt) Contract() dex.Bytes { - return dexeth.EncodeContractData(r.ver, r.secretHash) + return dexeth.EncodeContractData(r.serverVer, r.secretHash) } // String returns a string representation of the swapReceipt. The secret hash @@ -1453,7 +1482,7 @@ func (w *ETHWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, uint6 return fail("unfunded swap: %d < %d", reservedVal, swapVal+fees) } - tx, err := w.initiate(w.ctx, w.assetID, swaps.Contracts, swaps.FeeRate, gasLimit, cfg.Version) + tx, err := w.initiate(w.ctx, w.assetID, w.contractsToInitDetails(swaps.Contracts), swaps.FeeRate, gasLimit, cfg.Version) if err != nil { return fail("Swap: initiate error: %w", err) } @@ -1467,7 +1496,7 @@ func (w *ETHWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, uint6 value: swap.Value, txHash: txHash, secretHash: secretHash, - ver: cfg.Version, + serverVer: cfg.Version, contractAddr: dexeth.ContractAddresses[cfg.Version][w.net].String(), }) } @@ -1483,6 +1512,20 @@ func (w *ETHWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, uint6 return receipts, change, fees, nil } +func (w *baseWallet) contractsToInitDetails(contracts []*asset.Contract) []*dex.SwapContractDetails { + details := make([]*dex.SwapContractDetails, len(contracts)) + for i, c := range contracts { + details[i] = &dex.SwapContractDetails{ + From: w.addr.String(), + To: c.Address, + Value: c.Value, + SecretHash: c.SecretHash, + LockTime: c.LockTime, + } + } + return details +} + // Swap sends the swaps in a single transaction. The fees used returned are the // max fees that will possibly be used, since in ethereum with EIP-1559 we cannot // know exactly how much fees will be used. @@ -1529,7 +1572,7 @@ func (w *TokenWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, uin return fail("unfunded token swap fees: %d < %d", reservedParent, fees) } - tx, err := w.initiate(w.ctx, w.assetID, swaps.Contracts, swaps.FeeRate, gasLimit, cfg.Version) + tx, err := w.initiate(w.ctx, w.assetID, w.contractsToInitDetails(swaps.Contracts), swaps.FeeRate, gasLimit, cfg.Version) if err != nil { return fail("Swap: initiate error: %w", err) } @@ -1543,7 +1586,7 @@ func (w *TokenWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin, uin value: swap.Value, txHash: txHash, secretHash: secretHash, - ver: cfg.Version, + serverVer: cfg.Version, contractAddr: erc20.ContractAddresses[cfg.Version][w.net].String(), }) } @@ -1592,9 +1635,10 @@ func (w *assetWallet) Redeem(form *asset.RedeemForm, feeWallet *assetWallet, non return fail(errors.New("Redeem: must be called with at least 1 redemption")) } - var contractVer uint32 // require a consistent version since this is a single transaction + var serverVer uint32 // require a consistent version since this is a single transaction secrets := make([][32]byte, 0, n) var redeemedValue uint64 + details := make([]*dex.SwapContractDetails, len(form.Redemptions)) for i, redemption := range form.Redemptions { // NOTE: redemption.Spends.SecretHash is a dup of the hash extracted // from redemption.Spends.Contract. Even for scriptable UTXO assets, the @@ -1605,10 +1649,10 @@ func (w *assetWallet) Redeem(form *asset.RedeemForm, feeWallet *assetWallet, non return fail(fmt.Errorf("Redeem: invalid versioned swap contract data: %w", err)) } if i == 0 { - contractVer = ver - } else if contractVer != ver { - return fail(fmt.Errorf("Redeem: inconsistent contract versions in RedeemForm.Redemptions: "+ - "%d != %d", contractVer, ver)) + serverVer = ver + } else if serverVer != ver { + return fail(fmt.Errorf("Redeem: inconsistent server versions in RedeemForm.Redemptions: "+ + "%d != %d", serverVer, ver)) } // Use the contract's free public view function to validate the secret @@ -1618,7 +1662,9 @@ func (w *assetWallet) Redeem(form *asset.RedeemForm, feeWallet *assetWallet, non var secret [32]byte copy(secret[:], redemption.Secret) secrets = append(secrets, secret) - redeemable, err := w.isRedeemable(secretHash, secret, ver) + deets := redemption.SwapDetails + details[i] = deets + redeemable, err := w.isRedeemable(secret, deets, ver) if err != nil { return fail(fmt.Errorf("Redeem: failed to check if swap is redeemable: %w", err)) } @@ -1627,14 +1673,19 @@ func (w *assetWallet) Redeem(form *asset.RedeemForm, feeWallet *assetWallet, non secretHash, secret)) } - swapData, err := w.swap(w.ctx, secretHash, ver) + step, _, _, err := w.status(w.ctx, deets, serverVer) if err != nil { return nil, nil, 0, fmt.Errorf("error finding swap state: %w", err) } - redeemedValue += w.atomize(swapData.Value) + + if step != dexeth.SSInitiated { + return nil, nil, 0, fmt.Errorf("cannot redeem swap in state %s", step) + } + + redeemedValue += deets.Value } - g := w.gases(contractVer) + g := w.gases(serverVer) if g == nil { return fail(fmt.Errorf("no gas table")) } @@ -1649,7 +1700,8 @@ func (w *assetWallet) Redeem(form *asset.RedeemForm, feeWallet *assetWallet, non gasLimit, gasFeeCap := g.Redeem*n, form.FeeSuggestion originalFundsReserved := gasLimit * gasFeeCap - gasEst, err := w.estimateRedeemGas(w.ctx, secrets, contractVer) + + gasEst, err := w.estimateRedeemGas(w.ctx, secrets, details, serverVer) if err != nil { return fail(fmt.Errorf("error getting redemption estimate: %w", err)) } @@ -1695,7 +1747,7 @@ func (w *assetWallet) Redeem(form *asset.RedeemForm, feeWallet *assetWallet, non return fail(fmt.Errorf("error fetching best header: %w", err)) } - tx, err := w.redeem(w.ctx, w.assetID, form.Redemptions, gasFeeCap, gasLimit, contractVer, nonceOverride) + tx, err := w.redeem(w.ctx, w.assetID, form.Redemptions, gasFeeCap, gasLimit, serverVer, nonceOverride) if err != nil { return fail(fmt.Errorf("Redeem: redeem error: %w", err)) } @@ -1807,7 +1859,7 @@ func (w *assetWallet) tokenAllowance() (allowance *big.Int, err error) { // approveToken approves the token swap contract to spend tokens on behalf of // account handled by the wallet. -func (w *assetWallet) approveToken(amount *big.Int, maxFeeRate uint64, contractVer uint32) (tx *types.Transaction, err error) { +func (w *assetWallet) approveToken(amount *big.Int, maxFeeRate uint64, serverVer uint32) (tx *types.Transaction, err error) { w.nonceSendMtx.Lock() defer w.nonceSendMtx.Unlock() txOpts, err := w.node.txOpts(w.ctx, 0, approveGas, dexeth.GweiToWei(maxFeeRate)) @@ -1815,7 +1867,7 @@ func (w *assetWallet) approveToken(amount *big.Int, maxFeeRate uint64, contractV return nil, fmt.Errorf("addSignerToOpts error: %w", err) } - return tx, w.withTokenContractor(w.assetID, contractVer, func(c tokenContractor) error { + return tx, w.withTokenContractor(w.assetID, serverVer, func(c tokenContractor) error { tx, err = c.approve(txOpts, amount) if err == nil { w.log.Infof("Approval sent for %s at token address %s, nonce = %s", @@ -1984,19 +2036,44 @@ func (w *assetWallet) AuditContract(coinID, contract, serializedTx dex.Bytes, re return nil, fmt.Errorf("AuditContract: failed to decode contract data: %w", err) } - initiations, err := dexeth.ParseInitiateData(tx.Data(), version) - if err != nil { - return nil, fmt.Errorf("AuditContract: failed to parse initiate data: %w", err) - } + var val uint64 + var participant, initiator string + var lockTime time.Time + switch version { + case 0: + initiations, err := dexeth.ParseInitiateDataV0(tx.Data()) + if err != nil { + return nil, fmt.Errorf("AuditContract: failed to parse initiate data: %w", err) + } + + initiation, ok := initiations[secretHash] + if !ok { + return nil, errors.New("AuditContract: tx does not initiate secret hash") + } + val = w.atomize(initiation.Value) + participant = initiation.Participant.String() + lockTime = initiation.LockTime + case 1: + details, err := dexeth.ParseInitiateDataV1(tx.Data()) + if err != nil { + return nil, fmt.Errorf("AuditContract: failed to parse initiate data: %w", err) + } - initiation, ok := initiations[secretHash] - if !ok { - return nil, errors.New("AuditContract: tx does not initiate secret hash") + deets, ok := details[secretHash] + if !ok { + return nil, errors.New("AuditContract: tx does not initiate secret hash") + } + val = deets.Value + participant = deets.To + initiator = deets.From + lockTime = time.Unix(int64(deets.LockTime), 0) + default: + return nil, fmt.Errorf("unknown contract version %d", version) } coin := &coin{ id: txHash, - value: w.atomize(initiation.Value), + value: val, } // The counter-party should have broadcasted the contract tx but rebroadcast @@ -2011,8 +2088,9 @@ func (w *assetWallet) AuditContract(coinID, contract, serializedTx dex.Bytes, re } return &asset.AuditInfo{ - Recipient: initiation.Participant.Hex(), - Expiration: initiation.LockTime, + Recipient: participant, + Initiator: initiator, + Expiration: lockTime, Coin: coin, Contract: contract, SecretHash: secretHash[:], @@ -2021,28 +2099,13 @@ func (w *assetWallet) AuditContract(coinID, contract, serializedTx dex.Bytes, re // LocktimeExpired returns true if the specified contract's locktime has // expired, making it possible to issue a Refund. -func (w *assetWallet) LocktimeExpired(ctx context.Context, contract dex.Bytes) (bool, time.Time, error) { - contractVer, secretHash, err := dexeth.DecodeContractData(contract) - if err != nil { - return false, time.Time{}, err - } - - swap, err := w.swap(ctx, secretHash, contractVer) - if err != nil { - return false, time.Time{}, err - } - - // Time is not yet set for uninitiated swaps. - if swap.State == dexeth.SSNone { - return false, time.Time{}, asset.ErrSwapNotInitiated - } - +func (w *assetWallet) LocktimeExpired(ctx context.Context, deets *dex.SwapContractDetails) (bool, time.Time, error) { header, err := w.node.bestHeader(ctx) if err != nil { return false, time.Time{}, err } - blockTime := time.Unix(int64(header.Time), 0) - return swap.LockTime.Before(blockTime), swap.LockTime, nil + + return deets.LockTime < header.Time, time.Unix(int64(deets.LockTime), 0), nil } // findRedemptionResult is used internally for queued findRedemptionRequests. @@ -2053,8 +2116,9 @@ type findRedemptionResult struct { // findRedemptionRequest is a request that is waiting on a redemption result. type findRedemptionRequest struct { - contractVer uint32 - res chan *findRedemptionResult + serverVer uint32 + deets *dex.SwapContractDetails + res chan *findRedemptionResult } // sendFindRedemptionResult sends the result or logs a message if it cannot be @@ -2081,14 +2145,14 @@ func (w *assetWallet) findRedemptionRequests() map[[32]byte]*findRedemptionReque // FindRedemption checks the contract for a redemption. If the swap is initiated // but un-redeemed and un-refunded, FindRedemption will block until a redemption // is seen. -func (w *assetWallet) FindRedemption(ctx context.Context, _, contract dex.Bytes) (redemptionCoin, secret dex.Bytes, err error) { - contractVer, secretHash, err := dexeth.DecodeContractData(contract) +func (w *assetWallet) FindRedemption(ctx context.Context, _, contractData dex.Bytes, deets *dex.SwapContractDetails) (redemptionCoin, secret dex.Bytes, err error) { + serverVer, secretHash, err := dexeth.DecodeContractData(contractData) if err != nil { return nil, nil, err } // See if it's ready right away. - secret, err = w.findSecret(secretHash, contractVer) + secret, err = w.findSecret(deets, serverVer) if err != nil { return nil, nil, err } @@ -2099,8 +2163,9 @@ func (w *assetWallet) FindRedemption(ctx context.Context, _, contract dex.Bytes) // Not ready. Queue the request. req := &findRedemptionRequest{ - contractVer: contractVer, - res: make(chan *findRedemptionResult, 1), + serverVer: serverVer, + deets: deets, + res: make(chan *findRedemptionResult, 1), } w.findRedemptionMtx.Lock() @@ -2135,42 +2200,42 @@ func (w *assetWallet) FindRedemption(ctx context.Context, _, contract dex.Bytes) return findRedemptionCoinID, res.secret[:], nil } -func (w *assetWallet) findSecret(secretHash [32]byte, contractVer uint32) ([]byte, error) { +func (w *assetWallet) findSecret(deets *dex.SwapContractDetails, serverVer uint32) ([]byte, error) { ctx, cancel := context.WithTimeout(w.ctx, 10*time.Second) defer cancel() - swap, err := w.swap(ctx, secretHash, contractVer) + step, secret, _, err := w.status(ctx, deets, serverVer) if err != nil { return nil, err } - switch swap.State { + switch step { case dexeth.SSInitiated: return nil, nil // no redeem yet, but keep checking case dexeth.SSRedeemed: - return swap.Secret[:], nil + return secret[:], nil case dexeth.SSNone: - return nil, fmt.Errorf("swap %x does not exist", secretHash) + return nil, fmt.Errorf("swap %x does not exist", deets.SecretHash.String()) case dexeth.SSRefunded: - return nil, fmt.Errorf("swap %x is already refunded", secretHash) + return nil, fmt.Errorf("swap %x is already refunded", deets.SecretHash.String()) } - return nil, fmt.Errorf("unrecognized swap state %v", swap.State) + return nil, fmt.Errorf("unrecognized swap state %v", step) } // 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) { - version, secretHash, err := dexeth.DecodeContractData(contract) +func (w *assetWallet) Refund(coinID, contractData dex.Bytes, deets *dex.SwapContractDetails, feeRate uint64) (dex.Bytes, error) { + version, secretHash, err := dexeth.DecodeContractData(contractData) if err != nil { return nil, fmt.Errorf("Refund: failed to decode contract: %w", err) } - swap, err := w.swap(w.ctx, secretHash, version) + step, secret, _, err := w.status(w.ctx, deets, version) if err != nil { return nil, err } // It's possible the swap was refunded by someone else. In that case we // cannot know the refunding tx hash. - switch swap.State { + switch step { case dexeth.SSInitiated: // good, check refundability case dexeth.SSNone: return nil, asset.ErrSwapNotInitiated @@ -2180,11 +2245,11 @@ func (w *assetWallet) Refund(_, contract dex.Bytes, feeRate uint64) (dex.Bytes, return zeroHash[:], nil case dexeth.SSRedeemed: w.log.Infof("Swap with secret hash %x already redeemed with secret key %x.", - secretHash, swap.Secret) + secretHash, secret) return nil, asset.CoinNotFoundError // so caller knows to FindRedemption } - refundable, err := w.isRefundable(secretHash, version) + refundable, err := w.isRefundable(deets, version) if err != nil { return nil, fmt.Errorf("Refund: failed to check isRefundable: %w", err) } @@ -2192,7 +2257,7 @@ func (w *assetWallet) Refund(_, contract dex.Bytes, feeRate uint64) (dex.Bytes, return nil, fmt.Errorf("Refund: swap with secret hash %x is not refundable", secretHash) } - tx, err := w.refund(secretHash, feeRate, version) + tx, err := w.refund(deets, feeRate, version) if err != nil { return nil, fmt.Errorf("Refund: failed to call refund: %w", err) } @@ -2296,8 +2361,8 @@ func (w *assetWallet) RestorationInfo(seed []byte) ([]*asset.WalletRestoration, // SwapConfirmations gets the number of confirmations and the spend status // for the specified swap. -func (w *assetWallet) SwapConfirmations(ctx context.Context, coinID dex.Bytes, contract dex.Bytes, _ time.Time) (confs uint32, spent bool, err error) { - contractVer, secretHash, err := dexeth.DecodeContractData(contract) +func (w *assetWallet) SwapConfirmations(ctx context.Context, coinID dex.Bytes, contractData dex.Bytes, _ time.Time, deets *dex.SwapContractDetails) (confs uint32, spent bool, err error) { + contractVer, _, err := dexeth.DecodeContractData(contractData) if err != nil { return 0, false, err } @@ -2310,17 +2375,18 @@ func (w *assetWallet) SwapConfirmations(ctx context.Context, coinID dex.Bytes, c return 0, false, fmt.Errorf("error fetching best header: %w", err) } - swapData, err := w.swap(w.ctx, secretHash, contractVer) + step, _, blockNumber, err := w.status(w.ctx, deets, contractVer) if err != nil { return 0, false, fmt.Errorf("error finding swap state: %w", err) } - if swapData.State == dexeth.SSNone { + if step == dexeth.SSNone { return 0, false, asset.ErrSwapNotInitiated } - - spent = swapData.State >= dexeth.SSRedeemed - confs = uint32(hdr.Number.Uint64() - swapData.BlockHeight + 1) + spent = step >= dexeth.SSRedeemed + if !spent { + confs = uint32(hdr.Number.Uint64()) - blockNumber + 1 + } return } @@ -2709,35 +2775,77 @@ func (w *assetWallet) monitorTx(tx *types.Transaction, blockSubmitted uint64) { // in the batch that are still redeemable are included in the new transaction. // nonceOverride is set to a non-nil value when a specific nonce is required // (when a transaction has not been mined due to a low fee). -func (w *assetWallet) resubmitRedemption(tx *types.Transaction, contractVersion uint32, nonceOverride *uint64, feeWallet *assetWallet, monitoredTx *monitoredTx) (*common.Hash, error) { - parsedRedemptions, err := dexeth.ParseRedeemData(tx.Data(), contractVersion) - if err != nil { - return nil, fmt.Errorf("failed to parse redeem data: %w", err) - } +func (w *assetWallet) resubmitRedemption(tx *types.Transaction, redeem *asset.Redemption, serverVer uint32, nonceOverride *uint64, + feeWallet *assetWallet, monitoredTx *monitoredTx) (*common.Hash, error) { - redemptions := make([]*asset.Redemption, 0, len(parsedRedemptions)) + makeRedemption := func(deets *dex.SwapContractDetails, secret [32]byte) *asset.Redemption { + var secretHash [dexeth.SecretHashSize]byte + copy(secretHash[:], deets.SecretHash) + contractData := dexeth.EncodeContractData(serverVer, secretHash) + return &asset.Redemption{ + Spends: &asset.AuditInfo{ + Contract: contractData, + SecretHash: deets.SecretHash[:], + }, + SwapDetails: deets, + Secret: secret[:], + } + } - // Whether or not a a swap can be redeemed is checked in Redeem, but here - // we filter out unredeemable swaps in case one of the swaps in the tx - // was refunded/redeemed but the others were not. - for _, r := range parsedRedemptions { - redeemable, err := w.isRedeemable(r.SecretHash, r.Secret, contractVersion) + var redemptions []*asset.Redemption + switch serverVer { + case 0: + parsedRedemptions, err := dexeth.ParseRedeemDataV0(tx.Data()) if err != nil { + return nil, fmt.Errorf("failed to parse redeem data: %w", err) + } + redemptions = make([]*asset.Redemption, 0, len(parsedRedemptions)) + if err := w.withContractor(serverVer, func(ci contractor) error { + c := ci.(interface { + swap(context.Context, [32]byte) (*dexeth.SwapState, error) + }) + // Whether or not a a swap can be redeemed is checked in Redeem, but here + // we filter out unredeemable swaps in case one of the swaps in the tx + // was refunded/redeemed but the others were not. + for _, r := range parsedRedemptions { + swap, err := c.swap(context.TODO(), r.SecretHash) + if err != nil { + return err + } else if swap.State != dexeth.SSInitiated { + w.log.Warnf("swap %x is not redeemable. not resubmitting", r.SecretHash) + continue + } + + redemptions = append(redemptions, makeRedemption(&dex.SwapContractDetails{ + From: swap.Initiator.String(), + To: swap.Participant.String(), + LockTime: uint64(swap.LockTime.Unix()), + SecretHash: r.SecretHash[:], + Value: dexeth.WeiToGwei(swap.Value), + }, r.Secret)) + } + return nil + }); err != nil { return nil, err - } else if !redeemable { - w.log.Warnf("swap %x is not redeemable. not resubmitting", r.SecretHash) - continue } - contractData := dexeth.EncodeContractData(contractVersion, r.SecretHash) - redemptions = append(redemptions, &asset.Redemption{ - Spends: &asset.AuditInfo{ - Contract: contractData, - SecretHash: r.SecretHash[:], - }, - Secret: r.Secret[:], - }) + case 1: + redeems, err := dexeth.ParseRedeemDataV1(tx.Data()) + if err != nil { + return nil, err + } + for _, r := range redeems { + is, err := w.isRedeemable(r.Secret, r.Contract, serverVer) + if err != nil { + return nil, err + } else if !is { + w.log.Warnf("swap %x is not redeemable. not resubmitting", r.Contract.SecretHash) + continue + } + redemptions = append(redemptions, makeRedemption(r.Contract, r.Secret)) + } } + if len(redemptions) == 0 { return nil, fmt.Errorf("the swaps in tx %s are no longer redeemable. not resubmitting.", tx.Hash()) } @@ -2768,13 +2876,13 @@ func (w *assetWallet) resubmitRedemption(tx *types.Transaction, contractVersion // swapIsRedeemed checks if a swap is in the redeemed state. ErrSwapRefunded // is returned if the swap has been refunded. -func (w *assetWallet) swapIsRedeemed(secretHash common.Hash, contractVersion uint32) (bool, error) { - swap, err := w.swap(w.ctx, secretHash, contractVersion) +func (w *assetWallet) swapIsRedeemed(deets *dex.SwapContractDetails, serverVer uint32) (bool, error) { + step, _, _, err := w.status(w.ctx, deets, serverVer) if err != nil { return false, err } - switch swap.State { + switch step { case dexeth.SSRedeemed: return true, nil case dexeth.SSRefunded: @@ -2822,9 +2930,9 @@ func confStatus(confs uint64, txHash common.Hash) *asset.ConfirmRedemptionStatus // -- resubmits the tx with a new nonce if it has been nonce replaced // -- resubmits the tx with the same nonce but higher fee if the fee is too low // -- otherwise, resubmits the same tx to ensure propagation -func (w *assetWallet) checkUnconfirmedRedemption(secretHash common.Hash, contractVer uint32, txHash common.Hash, tx *types.Transaction, currentTip uint64, feeWallet *assetWallet, monitoredTx *monitoredTx) (*asset.ConfirmRedemptionStatus, error) { +func (w *assetWallet) checkUnconfirmedRedemption(redeem *asset.Redemption, contractVer uint32, txHash common.Hash, tx *types.Transaction, currentTip uint64, feeWallet *assetWallet, monitoredTx *monitoredTx) (*asset.ConfirmRedemptionStatus, error) { // Check if the swap has been redeemed by another transaction we are unaware of. - swapIsRedeemed, err := w.swapIsRedeemed(secretHash, contractVer) + swapIsRedeemed, err := w.swapIsRedeemed(redeem.SwapDetails, contractVer) if err != nil { return nil, err } @@ -2840,7 +2948,7 @@ func (w *assetWallet) checkUnconfirmedRedemption(secretHash common.Hash, contrac return nil, err } if confirmedNonce > tx.Nonce() { - replacementTxHash, err := w.resubmitRedemption(tx, contractVer, nil, feeWallet, monitoredTx) + replacementTxHash, err := w.resubmitRedemption(tx, redeem, contractVer, nil, feeWallet, monitoredTx) if err != nil { return nil, err } @@ -2857,7 +2965,7 @@ func (w *assetWallet) checkUnconfirmedRedemption(secretHash common.Hash, contrac w.log.Errorf("redemption tx %s has a gas fee cap %v lower than the current base fee %v", txHash, tx.GasFeeCap(), baseFee) nonce := tx.Nonce() - replacementTxHash, err := w.resubmitRedemption(tx, contractVer, &nonce, feeWallet, monitoredTx) + replacementTxHash, err := w.resubmitRedemption(tx, redeem, contractVer, &nonce, feeWallet, monitoredTx) if err != nil { return nil, err } @@ -2880,8 +2988,8 @@ func (w *assetWallet) checkUnconfirmedRedemption(secretHash common.Hash, contrac // when geth can also not find the transaction, instead of resubmitting an // 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, secretHash, err := dexeth.DecodeContractData(redemption.Spends.Contract) +func (w *assetWallet) confirmRedemptionWithoutMonitoredTx(txHash common.Hash, redeem *asset.Redemption, feeWallet *assetWallet) (*asset.ConfirmRedemptionStatus, error) { + serverVer, _, err := dexeth.DecodeContractData(redeem.Spends.Contract) if err != nil { return nil, fmt.Errorf("failed to decode contract data: %w", err) } @@ -2894,7 +3002,7 @@ func (w *assetWallet) confirmRedemptionWithoutMonitoredTx(txHash common.Hash, re tx, txBlock, err := w.node.getTransaction(w.ctx, txHash) if errors.Is(err, asset.CoinNotFoundError) { w.log.Errorf("ConfirmRedemption: geth could not find tx: %s", txHash) - swapIsRedeemed, err := w.swapIsRedeemed(secretHash, contractVer) + swapIsRedeemed, err := w.swapIsRedeemed(redeem.SwapDetails, serverVer) if err != nil { return nil, err } @@ -2905,7 +3013,7 @@ func (w *assetWallet) confirmRedemptionWithoutMonitoredTx(txHash common.Hash, re // If geth cannot find the transaction, and it also wasn't among the // monitored txs, we will resubmit the swap individually. txs, _, _, err := w.Redeem(&asset.RedeemForm{ - Redemptions: []*asset.Redemption{redemption}, + Redemptions: []*asset.Redemption{redeem}, FeeSuggestion: w.FeeRate(), }, nil, nil) if err != nil { @@ -2936,7 +3044,7 @@ func (w *assetWallet) confirmRedemptionWithoutMonitoredTx(txHash common.Hash, re if receipt.Status == types.ReceiptStatusSuccessful { return confStatus(txConfsNeededToConfirm, txHash), nil } - replacementTxHash, err := w.resubmitRedemption(tx, contractVer, nil, feeWallet, nil) + replacementTxHash, err := w.resubmitRedemption(tx, redeem, serverVer, nil, feeWallet, nil) if err != nil { return nil, err } @@ -2946,13 +3054,13 @@ func (w *assetWallet) confirmRedemptionWithoutMonitoredTx(txHash common.Hash, re return confStatus(confirmations, txHash), nil } - return w.checkUnconfirmedRedemption(secretHash, contractVer, txHash, tx, currentTip, feeWallet, nil) + return w.checkUnconfirmedRedemption(redeem, serverVer, txHash, tx, currentTip, feeWallet, nil) } // confirmRedemption checks the confirmation status of a redemption transaction. // It will resubmit transactions if it has been determined that the transaction // cannot be mined. -func (w *assetWallet) confirmRedemption(coinID dex.Bytes, redemption *asset.Redemption, feeWallet *assetWallet) (*asset.ConfirmRedemptionStatus, error) { +func (w *assetWallet) confirmRedemption(coinID dex.Bytes, redeem *asset.Redemption, feeWallet *assetWallet) (*asset.ConfirmRedemptionStatus, error) { if len(coinID) != common.HashLength { return nil, fmt.Errorf("expected coin ID to be a transaction hash, but it has a length of %d", len(coinID)) @@ -2963,7 +3071,7 @@ func (w *assetWallet) confirmRedemption(coinID dex.Bytes, redemption *asset.Rede monitoredTx, err := w.getLatestMonitoredTx(txHash) if err != nil { w.log.Error(err) - return w.confirmRedemptionWithoutMonitoredTx(txHash, redemption, feeWallet) + return w.confirmRedemptionWithoutMonitoredTx(txHash, redeem, feeWallet) } // This mutex is locked inside of getLatestMonitoredTx. defer monitoredTx.mtx.Unlock() @@ -2974,7 +3082,7 @@ func (w *assetWallet) confirmRedemption(coinID dex.Bytes, redemption *asset.Rede txHash = monitoredTxHash } - contractVer, secretHash, err := dexeth.DecodeContractData(redemption.Spends.Contract) + contractVer, _, err := dexeth.DecodeContractData(redeem.Spends.Contract) if err != nil { return nil, fmt.Errorf("failed to decode contract data: %w", err) } @@ -2999,7 +3107,7 @@ func (w *assetWallet) confirmRedemption(coinID dex.Bytes, redemption *asset.Rede return confStatus(0, txHash), nil } - swapIsRedeemed, err := w.swapIsRedeemed(secretHash, contractVer) + swapIsRedeemed, err := w.swapIsRedeemed(redeem.SwapDetails, contractVer) if err != nil { return nil, err } @@ -3007,7 +3115,7 @@ func (w *assetWallet) confirmRedemption(coinID dex.Bytes, redemption *asset.Rede return confStatus(txConfsNeededToConfirm, txHash), nil } - replacementTxHash, err := w.resubmitRedemption(monitoredTx.tx, contractVer, nil, feeWallet, monitoredTx) + replacementTxHash, err := w.resubmitRedemption(monitoredTx.tx, redeem, contractVer, nil, feeWallet, monitoredTx) if err != nil { return nil, err } @@ -3032,7 +3140,8 @@ func (w *assetWallet) confirmRedemption(coinID dex.Bytes, redemption *asset.Rede w.clearMonitoredTx(monitoredTx) return confStatus(txConfsNeededToConfirm, txHash), nil } - replacementTxHash, err := w.resubmitRedemption(tx, contractVer, nil, feeWallet, monitoredTx) + + replacementTxHash, err := w.resubmitRedemption(tx, redeem, contractVer, nil, feeWallet, monitoredTx) if err != nil { return nil, err } @@ -3048,13 +3157,13 @@ func (w *assetWallet) confirmRedemption(coinID dex.Bytes, redemption *asset.Rede return confStatus(0, txHash), nil } - return w.checkUnconfirmedRedemption(secretHash, contractVer, txHash, tx, currentTip, feeWallet, monitoredTx) + return w.checkUnconfirmedRedemption(redeem, contractVer, txHash, tx, currentTip, feeWallet, monitoredTx) } // checkFindRedemptions checks queued findRedemptionRequests. func (w *assetWallet) checkFindRedemptions() { for secretHash, req := range w.findRedemptionRequests() { - secret, err := w.findSecret(secretHash, req.contractVer) + secret, err := w.findSecret(req.deets, req.serverVer) if err != nil { w.sendFindRedemptionResult(req, secretHash, nil, err) } else if len(secret) > 0 { @@ -3167,29 +3276,35 @@ func (w *TokenWallet) sendToAddr(addr common.Address, amt uint64, maxFeeRate *bi }) } -// swap gets a swap keyed by secretHash in the contract. -func (w *assetWallet) swap(ctx context.Context, secretHash [32]byte, contractVer uint32) (swap *dexeth.SwapState, err error) { - return swap, w.withContractor(contractVer, func(c contractor) error { - swap, err = c.swap(ctx, secretHash) - return err +// status returns the current status of the swap. For v1 swaps, whether secret +// or blockNumber or neither is set depends on the current step. For +// SSNone and SSRefunded, neither is set. For SSInitiated, the blockNumber is +// set. For SSRedeemed, the secret is set. +func (w *assetWallet) status(ctx context.Context, deets *dex.SwapContractDetails, serverVer uint32) (step dexeth.SwapStep, secret [32]byte, blockNumber uint32, err error) { + return step, secret, blockNumber, w.withContractor(serverVer, func(c contractor) error { + step, secret, blockNumber, err = c.status(ctx, deets) + if err != nil { + return err + } + return nil }) } // initiate initiates multiple swaps in the same transaction. -func (w *assetWallet) initiate(ctx context.Context, assetID uint32, contracts []*asset.Contract, - maxFeeRate, gasLimit uint64, contractVer uint32) (tx *types.Transaction, err error) { +func (w *assetWallet) initiate(ctx context.Context, assetID uint32, details []*dex.SwapContractDetails, + maxFeeRate, gasLimit uint64, serverVer uint32) (tx *types.Transaction, err error) { var val uint64 if assetID == BipID { - for _, c := range contracts { - val += c.Value + for _, d := range details { + val += d.Value } } w.nonceSendMtx.Lock() defer w.nonceSendMtx.Unlock() txOpts, _ := w.node.txOpts(ctx, val, gasLimit, dexeth.GweiToWei(maxFeeRate)) - return tx, w.withContractor(contractVer, func(c contractor) error { - tx, err = c.initiate(txOpts, contracts) + return tx, w.withContractor(serverVer, func(c contractor) error { + tx, err = c.initiate(txOpts, details) return err }) } @@ -3204,17 +3319,17 @@ func (w *assetWallet) estimateInitGas(ctx context.Context, numSwaps int, contrac } // estimateRedeemGas checks the amount of gas that is used for the redemption. -func (w *assetWallet) estimateRedeemGas(ctx context.Context, secrets [][32]byte, contractVer uint32) (gas uint64, err error) { - return gas, w.withContractor(contractVer, func(c contractor) error { - gas, err = c.estimateRedeemGas(ctx, secrets) +func (w *assetWallet) estimateRedeemGas(ctx context.Context, secrets [][32]byte, details []*dex.SwapContractDetails, serverVer uint32) (gas uint64, err error) { + return gas, w.withContractor(serverVer, func(c contractor) error { + gas, err = c.estimateRedeemGas(ctx, secrets, details) return err }) } // estimateRefundGas checks the amount of gas that is used for a refund. -func (w *assetWallet) estimateRefundGas(ctx context.Context, secretHash [32]byte, contractVer uint32) (gas uint64, err error) { +func (w *assetWallet) estimateRefundGas(ctx context.Context, details *dex.SwapContractDetails, contractVer uint32) (gas uint64, err error) { return gas, w.withContractor(contractVer, func(c contractor) error { - gas, err = c.estimateRefundGas(ctx, secretHash) + gas, err = c.estimateRefundGas(ctx, details) return err }) } @@ -3251,7 +3366,8 @@ 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 { +func (w *assetWallet) withContractor(serverVer uint32, f func(contractor) error) error { + contractVer := contractVersion(serverVer) if contractVer == contractVersionNewest { var bestVer uint32 var bestContractor contractor @@ -3271,8 +3387,8 @@ func (w *assetWallet) withContractor(contractVer uint32, f func(contractor) erro } // withTokenContractor runs the provided function with the tokenContractor. -func (w *assetWallet) withTokenContractor(assetID, ver uint32, f func(tokenContractor) error) error { - return w.withContractor(ver, func(c contractor) error { +func (w *assetWallet) withTokenContractor(assetID, serverVer uint32, f func(tokenContractor) error) error { + return w.withContractor(serverVer, func(c contractor) error { tc, is := c.(tokenContractor) if !is { return fmt.Errorf("contractor for %d %T is not a tokenContractor", assetID, c) @@ -3301,7 +3417,9 @@ func (w *assetWallet) estimateTransferGas(val uint64) (gas uint64, err error) { // redeem redeems a swap contract. Any on-chain failure, such as this secret not // matching the hash, will not cause this to error. -func (w *assetWallet) redeem(ctx context.Context, assetID uint32, redemptions []*asset.Redemption, maxFeeRate, gasLimit uint64, contractVer uint32, nonceOverride *uint64) (tx *types.Transaction, err error) { +func (w *assetWallet) redeem(ctx context.Context, assetID uint32, redemptions []*asset.Redemption, + maxFeeRate, gasLimit uint64, serverVer uint32, nonceOverride *uint64) (tx *types.Transaction, err error) { + w.nonceSendMtx.Lock() defer w.nonceSendMtx.Unlock() txOpts, err := w.node.txOpts(ctx, 0, gasLimit, dexeth.GweiToWei(maxFeeRate)) @@ -3313,7 +3431,7 @@ func (w *assetWallet) redeem(ctx context.Context, assetID uint32, redemptions [] txOpts.Nonce = new(big.Int).SetUint64(*nonceOverride) } - return tx, w.withContractor(contractVer, func(c contractor) error { + return tx, w.withContractor(serverVer, func(c contractor) error { tx, err = c.redeem(txOpts, redemptions) return err }) @@ -3322,10 +3440,10 @@ func (w *assetWallet) redeem(ctx context.Context, assetID uint32, redemptions [] // refund refunds a swap contract using the account controlled by the wallet. // Any on-chain failure, such as the locktime not being past, will not cause // this to error. -func (w *assetWallet) refund(secretHash [32]byte, maxFeeRate uint64, contractVer uint32) (tx *types.Transaction, err error) { - gas := w.gases(contractVer) +func (w *assetWallet) refund(deets *dex.SwapContractDetails, maxFeeRate uint64, serverVer uint32) (tx *types.Transaction, err error) { + gas := w.gases(serverVer) if gas == nil { - return nil, fmt.Errorf("no gas table for asset %d, version %d", w.assetID, contractVer) + return nil, fmt.Errorf("no gas table for server %d, version %d", w.assetID, serverVer) } w.nonceSendMtx.Lock() defer w.nonceSendMtx.Unlock() @@ -3334,23 +3452,23 @@ func (w *assetWallet) refund(secretHash [32]byte, maxFeeRate uint64, contractVer return nil, fmt.Errorf("addSignerToOpts error: %w", err) } - return tx, w.withContractor(contractVer, func(c contractor) error { - tx, err = c.refund(txOpts, secretHash) + return tx, w.withContractor(serverVer, func(c contractor) error { + tx, err = c.refund(txOpts, deets) return err }) } // isRedeemable checks if the swap identified by secretHash is redeemable using secret. -func (w *assetWallet) isRedeemable(secretHash [32]byte, secret [32]byte, contractVer uint32) (redeemable bool, err error) { - return redeemable, w.withContractor(contractVer, func(c contractor) error { - redeemable, err = c.isRedeemable(secretHash, secret) +func (w *assetWallet) isRedeemable(secret [32]byte, deets *dex.SwapContractDetails, serverVer uint32) (redeemable bool, err error) { + return redeemable, w.withContractor(serverVer, func(c contractor) error { + redeemable, err = c.isRedeemable(secret, deets) return err }) } -func (w *assetWallet) isRefundable(secretHash [32]byte, contractVer uint32) (refundable bool, err error) { - return refundable, w.withContractor(contractVer, func(c contractor) error { - refundable, err = c.isRefundable(secretHash) +func (w *assetWallet) isRefundable(deets *dex.SwapContractDetails, serverVer uint32) (refundable bool, err error) { + return refundable, w.withContractor(serverVer, func(c contractor) error { + refundable, err = c.isRefundable(deets) return err }) } @@ -3432,14 +3550,12 @@ func GetGasEstimates(ctx context.Context, cl *nodeClient, c contractor, maxSwaps }() for n := 1; n <= maxSwaps; n++ { - // Estimate approve. - var approveTx, transferTx *types.Transaction if isToken { txOpts, err := cl.txOpts(ctx, 0, g.Approve*2, nil) if err != nil { return fmt.Errorf("error constructing signed tx opts for approve: %v", err) } - approveTx, err = tokenContractor.approve(txOpts, new(big.Int).SetBytes(encode.RandomBytes(8))) + approveTx, err := tokenContractor.approve(txOpts, new(big.Int).SetBytes(encode.RandomBytes(8))) if err != nil { return fmt.Errorf("error estimating approve gas: %v", err) } @@ -3447,26 +3563,50 @@ func GetGasEstimates(ctx context.Context, cl *nodeClient, c contractor, maxSwaps if err != nil { return fmt.Errorf("error constructing signed tx opts for transfer: %v", err) } - transferTx, err = tokenContractor.transfer(txOpts, cl.address(), big.NewInt(1)) + transferTx, err := tokenContractor.transfer(txOpts, cl.address(), big.NewInt(1)) if err != nil { return fmt.Errorf("error estimating transfer gas: %v", err) } + waitForMined() + receipt, err := cl.transactionReceipt(ctx, approveTx.Hash()) + if err != nil { + return err + } + if err := checkTxStatus(receipt, txOpts.GasLimit); err != nil { + return err + } + stats.approves = append(stats.approves, receipt.GasUsed) + receipt, err = cl.transactionReceipt(ctx, transferTx.Hash()) + if err != nil { + return err + } + if err := checkTxStatus(receipt, txOpts.GasLimit); err != nil { + return err + } + stats.transfers = append(stats.transfers, receipt.GasUsed) } - contracts := make([]*asset.Contract, 0, n) + details := make([]*dex.SwapContractDetails, 0, n) secrets := make([][32]byte, 0, n) + redemptions := make([]*asset.Redemption, 0, n) for i := 0; i < n; i++ { secretB := encode.RandomBytes(32) var secret [32]byte copy(secret[:], secretB) secretHash := sha256.Sum256(secretB) - contracts = append(contracts, &asset.Contract{ - Address: cl.address().String(), + deets := &dex.SwapContractDetails{ + To: cl.address().String(), + From: cl.address().String(), Value: 1, SecretHash: secretHash[:], LockTime: uint64(time.Now().Add(-time.Hour).Unix()), - }) + } + details = append(details, deets) secrets = append(secrets, secret) + redemptions = append(redemptions, &asset.Redemption{ + SwapDetails: deets, + Secret: secrets[i][:], + }) } var optsVal uint64 @@ -3479,7 +3619,7 @@ func GetGasEstimates(ctx context.Context, cl *nodeClient, c contractor, maxSwaps if err != nil { return fmt.Errorf("error constructing signed tx opts for %d swaps: %v", n, err) } - tx, err := c.initiate(txOpts, contracts) + tx, err := c.initiate(txOpts, details) if err != nil { return fmt.Errorf("initiate error for %d swaps: %v", n, err) } @@ -3493,44 +3633,12 @@ func GetGasEstimates(ctx context.Context, cl *nodeClient, c contractor, maxSwaps } stats.swaps = append(stats.swaps, receipt.GasUsed) - if isToken { - receipt, err = cl.transactionReceipt(ctx, approveTx.Hash()) - if err != nil { - return err - } - if err := checkTxStatus(receipt, txOpts.GasLimit); err != nil { - return err - } - stats.approves = append(stats.approves, receipt.GasUsed) - receipt, err = cl.transactionReceipt(ctx, transferTx.Hash()) - if err != nil { - return err - } - if err := checkTxStatus(receipt, txOpts.GasLimit); err != nil { - return err - } - stats.transfers = append(stats.transfers, receipt.GasUsed) - } - - // Estimate a refund - var firstSecretHash [32]byte - copy(firstSecretHash[:], contracts[0].SecretHash) - refundGas, err := c.estimateRefundGas(ctx, firstSecretHash) + refundGas, err := c.estimateRefundGas(ctx, details[0]) if err != nil { - return fmt.Errorf("error estimate refund gas: %w", err) + return fmt.Errorf("error estimating refund gas: %w", err) } stats.refunds = append(stats.refunds, refundGas) - redemptions := make([]*asset.Redemption, 0, n) - for i, contract := range contracts { - redemptions = append(redemptions, &asset.Redemption{ - Spends: &asset.AuditInfo{ - SecretHash: contract.SecretHash, - }, - Secret: secrets[i][:], - }) - } - txOpts, _ = cl.txOpts(ctx, 0, g.RedeemN(n)*2, nil) tx, err = c.redeem(txOpts, redemptions) if err != nil { diff --git a/client/asset/eth/eth_test.go b/client/asset/eth/eth_test.go index aa7443314e..70adead7e4 100644 --- a/client/asset/eth/eth_test.go +++ b/client/asset/eth/eth_test.go @@ -114,7 +114,9 @@ type testNode struct { receipt *types.Receipt receiptErr error - lastSignedTx *types.Transaction + lastSignedTx *types.Transaction + getTx *types.Transaction + sendTxTx *types.Transaction sendTxErr error simBackend bind.ContractBackend @@ -137,7 +139,6 @@ func newBalance(current, in, out uint64) *Balance { func (n *testNode) address() common.Address { return n.addr } - func (n *testNode) connect(ctx context.Context) error { return n.connectErr } @@ -277,7 +278,7 @@ type tContractor struct { lastRedeems []*asset.Redemption lastRefund struct { // tx *types.Transaction - secretHash [32]byte + secretHash []byte maxFeeRate *big.Int // contractVer uint32 } @@ -294,7 +295,20 @@ func (c *tContractor) swap(ctx context.Context, secretHash [32]byte) (*dexeth.Sw return swap, nil } -func (c *tContractor) initiate(*bind.TransactOpts, []*asset.Contract) (*types.Transaction, error) { +func (c *tContractor) status(ctx context.Context, contract *dex.SwapContractDetails) (step dexeth.SwapStep, secret [32]byte, blockNumber uint32, err error) { + if c.swapErr != nil { + return 0, secret, 0, c.swapErr + } + var secretHash [32]byte + copy(secretHash[:], contract.SecretHash) + swap, ok := c.swapMap[secretHash] + if !ok { + return 0, secret, 0, errors.New("swap not in map") + } + return swap.State, swap.Secret, uint32(swap.BlockHeight), nil +} + +func (c *tContractor) initiate(*bind.TransactOpts, []*dex.SwapContractDetails) (*types.Transaction, error) { return c.initTx, c.initErr } @@ -304,8 +318,8 @@ func (c *tContractor) redeem(txOpts *bind.TransactOpts, redeems []*asset.Redempt return c.redeemTx, c.redeemErr } -func (c *tContractor) refund(opts *bind.TransactOpts, secretHash [32]byte) (*types.Transaction, error) { - c.lastRefund.secretHash = secretHash +func (c *tContractor) refund(opts *bind.TransactOpts, contract *dex.SwapContractDetails) (*types.Transaction, error) { + c.lastRefund.secretHash = contract.SecretHash c.lastRefund.maxFeeRate = opts.GasFeeCap return c.refundTx, c.refundErr } @@ -314,22 +328,25 @@ func (c *tContractor) estimateInitGas(ctx context.Context, n int) (uint64, error return c.gasEstimates.SwapN(n), c.initGasErr } -func (c *tContractor) estimateRedeemGas(ctx context.Context, secrets [][32]byte) (uint64, error) { +func (c *tContractor) estimateRedeemGas(ctx context.Context, secrets [][32]byte, _ []*dex.SwapContractDetails) (uint64, error) { if c.redeemGasOverride != nil { return *c.redeemGasOverride, nil } return c.gasEstimates.RedeemN(len(secrets)), c.redeemGasErr } -func (c *tContractor) estimateRefundGas(ctx context.Context, secretHash [32]byte) (uint64, error) { +func (c *tContractor) estimateRefundGas(ctx context.Context, contract *dex.SwapContractDetails) (uint64, error) { return c.gasEstimates.Refund, c.refundGasErr } -func (c *tContractor) isRedeemable(secretHash, secret [32]byte) (bool, error) { +func (c *tContractor) isRedeemable(secret [32]byte, deets *dex.SwapContractDetails) (bool, error) { if c.redeemableErr != nil { return false, c.redeemableErr } + var secretHash [32]byte + copy(secretHash[:], deets.SecretHash) + if c.redeemableMap != nil { return c.redeemableMap[secretHash], nil } @@ -341,7 +358,7 @@ func (c *tContractor) value(_ context.Context, tx *types.Transaction) (incoming, return c.valueIn[tx.Hash()], c.valueOut[tx.Hash()], c.valueErr } -func (c *tContractor) isRefundable(secretHash [32]byte) (bool, error) { +func (c *tContractor) isRefundable(*dex.SwapContractDetails) (bool, error) { return c.refundable, c.refundableErr } @@ -966,7 +983,7 @@ func testRefund(t *testing.T, assetID uint32) { c.refundTx = tx } - refundID, err := eth.Refund(nil, contract, feeSuggestion) + refundID, err := eth.Refund(nil, contract, &dex.SwapContractDetails{SecretHash: secretHash[:]}, feeSuggestion) if test.wantErr { if err == nil { @@ -989,7 +1006,7 @@ func testRefund(t *testing.T, assetID uint32) { t.Fatalf(`%v: expected refund tx hash: %x = returned id: %s`, test.name, txHash, refundID) } - if secretHash != c.lastRefund.secretHash { + if !bytes.Equal(secretHash[:], c.lastRefund.secretHash) { t.Fatalf(`%v: secret hash in contract %x != used to call refund %x`, test.name, secretHash, c.lastRefund.secretHash) } @@ -1879,6 +1896,19 @@ func testRedeem(t *testing.T, assetID uint32) { Number: big.NewInt(bestBlock), } + newRedeem := func(idx int) *asset.Redemption { + return &asset.Redemption{ + Spends: &asset.AuditInfo{ + Contract: dexeth.EncodeContractData(contractVer, secretHashes[idx]), + }, + SwapDetails: &dex.SwapContractDetails{ + SecretHash: secretHashes[idx][:], + Value: 1e9, + }, + Secret: secrets[idx][:], + } + } + tests := []struct { name string form asset.RedeemForm @@ -1899,28 +1929,7 @@ func testRedeem(t *testing.T, assetID uint32) { baseFee: dexeth.GweiToWei(100), expectedGasFeeCap: dexeth.GweiToWei(100), form: asset.RedeemForm{ - Redemptions: []*asset.Redemption{ - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]), - SecretHash: secretHashes[0][:], // redundant for all current assets, unused with eth - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[0][:], - }, - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[1]), - SecretHash: secretHashes[1][:], - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[1][:], - }, - }, + Redemptions: []*asset.Redemption{newRedeem(0), newRedeem(1)}, FeeSuggestion: 100, }, }, @@ -1933,28 +1942,7 @@ func testRedeem(t *testing.T, assetID uint32) { expectedGasFeeCap: dexeth.GweiToWei(100), redeemGasOverride: &higherGasEstimate, form: asset.RedeemForm{ - Redemptions: []*asset.Redemption{ - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]), - SecretHash: secretHashes[0][:], // redundant for all current assets, unused with eth - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[0][:], - }, - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[1]), - SecretHash: secretHashes[1][:], - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[1][:], - }, - }, + Redemptions: []*asset.Redemption{newRedeem(0), newRedeem(1)}, FeeSuggestion: 100, }, }, @@ -1967,28 +1955,7 @@ func testRedeem(t *testing.T, assetID uint32) { expectedGasFeeCap: dexeth.GweiToWei(100), redeemGasOverride: &doubleGasEstimate, form: asset.RedeemForm{ - Redemptions: []*asset.Redemption{ - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]), - SecretHash: secretHashes[0][:], // redundant for all current assets, unused with eth - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[0][:], - }, - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[1]), - SecretHash: secretHashes[1][:], - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[1][:], - }, - }, + Redemptions: []*asset.Redemption{newRedeem(0), newRedeem(1)}, FeeSuggestion: 100, }, }, @@ -2000,28 +1967,7 @@ func testRedeem(t *testing.T, assetID uint32) { baseFee: dexeth.GweiToWei(100), redeemGasOverride: &moreThanDoubleGasEstimate, form: asset.RedeemForm{ - Redemptions: []*asset.Redemption{ - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]), - SecretHash: secretHashes[0][:], // redundant for all current assets, unused with eth - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[0][:], - }, - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[1]), - SecretHash: secretHashes[1][:], - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[1][:], - }, - }, + Redemptions: []*asset.Redemption{newRedeem(0), newRedeem(1)}, FeeSuggestion: 100, }, }, @@ -2033,28 +1979,7 @@ func testRedeem(t *testing.T, assetID uint32) { baseFee: dexeth.GweiToWei(100), redeemGasOverride: &higherGasEstimate, form: asset.RedeemForm{ - Redemptions: []*asset.Redemption{ - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]), - SecretHash: secretHashes[0][:], // redundant for all current assets, unused with eth - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[0][:], - }, - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[1]), - SecretHash: secretHashes[1][:], - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[1][:], - }, - }, + Redemptions: []*asset.Redemption{newRedeem(0), newRedeem(1)}, FeeSuggestion: 100, }, }, @@ -2067,28 +1992,7 @@ func testRedeem(t *testing.T, assetID uint32) { expectedGasFeeCap: dexeth.GweiToWei(300), redeemGasOverride: &higherGasEstimate, form: asset.RedeemForm{ - Redemptions: []*asset.Redemption{ - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]), - SecretHash: secretHashes[0][:], // redundant for all current assets, unused with eth - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[0][:], - }, - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[1]), - SecretHash: secretHashes[1][:], - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[1][:], - }, - }, + Redemptions: []*asset.Redemption{newRedeem(0), newRedeem(1)}, FeeSuggestion: 100, }, }, @@ -2101,28 +2005,7 @@ func testRedeem(t *testing.T, assetID uint32) { expectedGasFeeCap: dexeth.GweiToWei(298), redeemGasOverride: &higherGasEstimate, form: asset.RedeemForm{ - Redemptions: []*asset.Redemption{ - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]), - SecretHash: secretHashes[0][:], // redundant for all current assets, unused with eth - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[0][:], - }, - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[1]), - SecretHash: secretHashes[1][:], - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[1][:], - }, - }, + Redemptions: []*asset.Redemption{newRedeem(0), newRedeem(1)}, FeeSuggestion: 100, }, }, @@ -2134,28 +2017,7 @@ func testRedeem(t *testing.T, assetID uint32) { baseFee: dexeth.GweiToWei(100), form: asset.RedeemForm{ - Redemptions: []*asset.Redemption{ - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]), - SecretHash: secretHashes[0][:], - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[0][:], - }, - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[1]), - SecretHash: secretHashes[1][:], - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[1][:], - }, - }, + Redemptions: []*asset.Redemption{newRedeem(0), newRedeem(1)}, FeeSuggestion: 100, }, }, @@ -2167,28 +2029,7 @@ func testRedeem(t *testing.T, assetID uint32) { baseFee: dexeth.GweiToWei(100), isRedeemableErr: errors.New(""), form: asset.RedeemForm{ - Redemptions: []*asset.Redemption{ - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]), - SecretHash: secretHashes[0][:], - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[0][:], - }, - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[1]), - SecretHash: secretHashes[1][:], - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[1][:], - }, - }, + Redemptions: []*asset.Redemption{newRedeem(0), newRedeem(1)}, FeeSuggestion: 100, }, }, @@ -2200,18 +2041,7 @@ func testRedeem(t *testing.T, assetID uint32) { ethBal: dexeth.GweiToWei(10e9), baseFee: dexeth.GweiToWei(100), form: asset.RedeemForm{ - Redemptions: []*asset.Redemption{ - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[0]), - SecretHash: secretHashes[0][:], - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[0][:], - }, - }, + Redemptions: []*asset.Redemption{newRedeem(0)}, FeeSuggestion: 200, }, }, @@ -2222,18 +2052,7 @@ func testRedeem(t *testing.T, assetID uint32) { ethBal: dexeth.GweiToWei(10e9), baseFee: dexeth.GweiToWei(100), form: asset.RedeemForm{ - Redemptions: []*asset.Redemption{ - { - Spends: &asset.AuditInfo{ - Contract: dexeth.EncodeContractData(contractVer, secretHashes[2]), - SecretHash: secretHashes[2][:], - Coin: &coin{ - id: randomHash(), - }, - }, - Secret: secrets[2][:], - }, - }, + Redemptions: []*asset.Redemption{newRedeem(2)}, FeeSuggestion: 100, }, }, @@ -2289,7 +2108,7 @@ func testRedeem(t *testing.T, assetID uint32) { t.Fatalf("%v: expected fees %d, but got %d", test.name, expectedFees, fees) } - // Check that value of output coin is as axpected + // Check that value of output coin is as expected var totalSwapValue uint64 for _, redemption := range test.form.Redemptions { _, secretHash, err := dexeth.DecodeContractData(redemption.Spends.Contract) @@ -2302,8 +2121,8 @@ func testRedeem(t *testing.T, assetID uint32) { totalSwapValue += dexeth.WeiToGwei(swap.Value) } if out.Value() != totalSwapValue { - t.Fatalf("expected coin value to be %d but got %d", - totalSwapValue, out.Value()) + t.Fatalf("%q expected coin value to be %d but got %d", + test.name, totalSwapValue, out.Value()) } // Check that gas limit in the transaction is as expected @@ -2870,7 +2689,9 @@ func TestSwapConfirmation(t *testing.T) { defer cancel() checkResult := func(expErr bool, expConfs uint32, expSpent bool) { - confs, spent, err := eth.SwapConfirmations(ctx, nil, dexeth.EncodeContractData(ver, secretHash), time.Time{}) + t.Helper() + confs, spent, err := eth.SwapConfirmations(ctx, nil, dexeth.EncodeContractData(ver, secretHash), + time.Time{}, &dex.SwapContractDetails{SecretHash: secretHash[:]}) if err != nil { if expErr { return @@ -2904,15 +2725,11 @@ func TestSwapConfirmation(t *testing.T) { // ErrSwapNotInitiated state.State = dexeth.SSNone - _, _, err := eth.SwapConfirmations(ctx, nil, dexeth.EncodeContractData(0, secretHash), time.Time{}) + _, _, err := eth.SwapConfirmations(ctx, nil, dexeth.EncodeContractData(0, secretHash), + time.Time{}, &dex.SwapContractDetails{SecretHash: secretHash[:]}) if !errors.Is(err, asset.ErrSwapNotInitiated) { t.Fatalf("expected ErrSwapNotInitiated, got %v", err) } - - // 1 conf, spent - state.BlockHeight = 6 - state.State = dexeth.SSRedeemed - checkResult(false, 1, true) } func TestDriverOpen(t *testing.T) { @@ -3071,13 +2888,15 @@ func TestLocktimeExpired(t *testing.T) { var secretHash [32]byte copy(secretHash[:], encode.RandomBytes(32)) + lockTime := time.Now() + state := &dexeth.SwapState{ - LockTime: time.Now(), + LockTime: lockTime, State: dexeth.SSInitiated, } header := &types.Header{ - Time: uint64(time.Now().Add(time.Second).Unix()), + Time: uint64(lockTime.Add(time.Second).Unix()), } node.tContractor.swapMap[secretHash] = state @@ -3088,7 +2907,7 @@ func TestLocktimeExpired(t *testing.T) { ensureResult := func(tag string, expErr, expExpired bool) { t.Helper() - expired, _, err := eth.LocktimeExpired(context.Background(), contract) + expired, _, err := eth.LocktimeExpired(context.Background(), &dex.SwapContractDetails{LockTime: uint64(lockTime.Unix())}) switch { case err != nil: if !expErr { @@ -3097,7 +2916,7 @@ func TestLocktimeExpired(t *testing.T) { case expErr: t.Fatalf("%s: expected error, got none", tag) case expExpired != expired: - t.Fatalf("%s: expired wrong. %t != %t", tag, expired, expExpired) + t.Fatalf("%s: expired wrong. expected %t, got %t", tag, expExpired, expired) } } @@ -3109,29 +2928,9 @@ func TestLocktimeExpired(t *testing.T) { ensureResult("header error", true, false) node.bestHdrErr = nil - // swap not initiated - saveState := state.State - state.State = dexeth.SSNone - ensureResult("swap not initiated", true, false) - state.State = saveState - - // missing swap - delete(node.tContractor.swapMap, secretHash) - ensureResult("missing swap", true, false) - node.tContractor.swapMap[secretHash] = state - // lock time not expired - state.LockTime = time.Now().Add(time.Minute) + lockTime = time.Now().Add(time.Minute) ensureResult("lock time not expired", false, false) - - // wrong contract version - contract[3] = 1 - ensureResult("wrong contract version", true, false) - contract[3] = 0 - - // bad contract - contract = append(contract, 0) - ensureResult("bad contract", true, false) } func TestFindRedemption(t *testing.T) { @@ -3170,7 +2969,7 @@ func testFindRedemption(t *testing.T, assetID uint32) { var err error ctx, cancel := context.WithTimeout(baseCtx, time.Second) defer cancel() - _, secretB, err := eth.FindRedemption(ctx, nil, contract) + _, secretB, err := eth.FindRedemption(ctx, nil, contract, &dex.SwapContractDetails{SecretHash: secretHash[:]}) if err != nil { if wantErr { return @@ -3262,7 +3061,7 @@ func testFindRedemption(t *testing.T, assetID uint32) { eth.findRedemptionMtx.Unlock() res := make(chan error, 1) go func() { - _, _, err := eth.FindRedemption(baseCtx, nil, contract) + _, _, err := eth.FindRedemption(baseCtx, nil, contract, &dex.SwapContractDetails{SecretHash: secretHash[:]}) res <- err }() @@ -3664,6 +3463,9 @@ func testConfirmRedemption(t *testing.T, assetID uint32) { Spends: &asset.AuditInfo{ Contract: dexeth.EncodeContractData(0, secretHash), }, + SwapDetails: &dex.SwapContractDetails{ + SecretHash: secretHash[:], + }, Secret: secret[:], } } @@ -3787,7 +3589,7 @@ func testConfirmRedemption(t *testing.T, assetID uint32) { }, swapMap: map[[32]byte]*dexeth.SwapState{ secretHashes[0]: { - State: dexeth.SSRedeemed, + State: dexeth.SSInitiated, }, }, monitoredTxs: map[common.Hash]*monitoredTx{ @@ -4289,7 +4091,6 @@ func testConfirmRedemption(t *testing.T, assetID uint32) { t.Fatalf("%s: error storing monitored tx: %v", test.name, err) } } - result, err := w.ConfirmRedemption(test.coinID, test.redemption) if test.expectErr { if err == nil { @@ -4301,7 +4102,7 @@ func testConfirmRedemption(t *testing.T, assetID uint32) { continue } if err != nil { - t.Fatalf("%s: unexpected error %v", test.name, err) + t.Fatalf("%s: unexpected error: %v", test.name, err) } // Check that the correct swaps were resubmitted diff --git a/client/asset/eth/nodeclient.go b/client/asset/eth/nodeclient.go index 275b5b6600..59903e83c0 100644 --- a/client/asset/eth/nodeclient.go +++ b/client/asset/eth/nodeclient.go @@ -20,7 +20,6 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/ethclient" "github.com/ethereum/go-ethereum/les" "github.com/ethereum/go-ethereum/node" @@ -32,17 +31,7 @@ import ( "github.com/ethereum/go-ethereum/consensus/misc" ) -const ( - contractVersionNewest = ^uint32(0) - approveGas = 4e5 -) - var ( - // https://github.com/ethereum/go-ethereum/blob/16341e05636fd088aa04a27fca6dc5cda5dbab8f/eth/backend.go#L110-L113 - // ultimately results in a minimum fee rate by the filter applied at - // https://github.com/ethereum/go-ethereum/blob/4ebeca19d739a243dc0549bcaf014946cde95c4f/core/tx_pool.go#L626 - minGasPrice = ethconfig.Defaults.Miner.GasPrice - // Check that nodeClient satisfies the ethFetcher interface. _ ethFetcher = (*nodeClient)(nil) ) diff --git a/client/asset/eth/nodeclient_harness_test.go b/client/asset/eth/nodeclient_harness_test.go index 6944104f7e..436478f7bb 100644 --- a/client/asset/eth/nodeclient_harness_test.go +++ b/client/asset/eth/nodeclient_harness_test.go @@ -28,10 +28,12 @@ import ( "errors" "fmt" "math/big" + "math/rand" "os" "os/exec" "os/signal" "path/filepath" + "strings" "sync" "testing" "time" @@ -88,6 +90,7 @@ const ( var ( homeDir = os.Getenv("HOME") + harnessCtlDir = filepath.Join(homeDir, "dextest", "eth", "harness-ctl") simnetWalletDir = filepath.Join(homeDir, "dextest", "eth", "client_rpc_tests", "simnet") participantWalletDir = filepath.Join(homeDir, "dextest", "eth", "client_rpc_tests", "participant") testnetWalletDir = filepath.Join(homeDir, "ethtest", "testnet_contract_tests", "walletA") @@ -104,20 +107,20 @@ var ( participantAcct *accounts.Account participantEthClient *nodeClient ethSwapContractAddr common.Address - tokenSwapContractAddr common.Address - testTokenContractAddr common.Address simnetID int64 = 42 simnetContractor contractor participantContractor contractor simnetTokenContractor tokenContractor participantTokenContractor tokenContractor - ethGases = dexeth.VersionedGases[0] - tokenGases = &dexeth.Tokens[testTokenID].NetTokens[dex.Simnet].SwapContracts[0].Gas + ethGases *dexeth.Gases + tokenGases *dexeth.Gases testnetSecPerBlock = 15 * time.Second // secPerBlock is one for simnet, because it takes one second to mine a // block currently. Is set in code to testnetSecPerBlock if runing on // testnet. secPerBlock = time.Second + v1 bool + ver uint32 ) func newContract(stamp uint64, secretHash [32]byte, val uint64) *asset.Contract { @@ -129,10 +132,14 @@ func newContract(stamp uint64, secretHash [32]byte, val uint64) *asset.Contract } } -func newRedeem(secret, secretHash [32]byte) *asset.Redemption { +func newRedeem(secret, secretHash [32]byte, valg, lockTime uint64) *asset.Redemption { return &asset.Redemption{ - Spends: &asset.AuditInfo{ + SwapDetails: &dex.SwapContractDetails{ + From: ethClient.address().String(), + To: participantEthClient.address().String(), + LockTime: lockTime, SecretHash: secretHash[:], + Value: valg, }, Secret: secret[:], } @@ -222,11 +229,11 @@ func runSimnet(m *testing.M) (int, error) { // ETH swap contract. token := dexeth.Tokens[testTokenID].NetTokens[dex.Simnet] - fmt.Printf("ETH swap contract address is %v\n", dexeth.ContractAddresses[0][dex.Simnet]) - fmt.Printf("Token swap contract addr is %v\n", token.SwapContracts[0].Address) + 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("Test token contract addr is %v\n", token.Address) - ethSwapContractAddr = dexeth.ContractAddresses[0][dex.Simnet] + ethSwapContractAddr = dexeth.ContractAddresses[ver][dex.Simnet] err = setupWallet(simnetWalletDir, simnetWalletSeed, "localhost:30355", dex.Simnet) if err != nil { @@ -285,24 +292,10 @@ func runSimnet(m *testing.M) (int, error) { participantAcct = &accts[0] participantAddr = participantAcct.Address - if simnetContractor, err = newV0Contractor(dex.Simnet, simnetAddr, ethClient.contractBackend()); err != nil { - return 1, fmt.Errorf("newV0Contractor error: %w", err) - } - if participantContractor, err = newV0Contractor(dex.Simnet, participantAddr, participantEthClient.contractBackend()); err != nil { - return 1, fmt.Errorf("participant newV0Contractor error: %w", err) - } - - if simnetTokenContractor, err = newV0TokenContractor(dex.Simnet, testTokenID, simnetAddr, ethClient.contractBackend()); err != nil { - return 1, fmt.Errorf("newV0TokenContractor error: %w", err) - } - - // I don't know why this is needed for the participant client but not - // the initiator. Without this, we'll get a bind.ErrNoCode from - // (*BoundContract).Call while calling (*ERC20Swap).TokenAddress. - time.Sleep(time.Second) - - if participantTokenContractor, err = newV0TokenContractor(dex.Simnet, testTokenID, participantAddr, participantEthClient.contractBackend()); err != nil { - return 1, fmt.Errorf("participant newV0TokenContractor error: %w", err) + if v1 { + prepareV1SimnetContractors() + } else { + prepareV0SimnetContractors() } if err := ethClient.unlock(pw); err != nil { @@ -313,8 +306,6 @@ func runSimnet(m *testing.M) (int, error) { } // Fund the wallets. Can use the simharness package once #1738 is merged. - homeDir, _ := os.UserHomeDir() - harnessCtlDir := filepath.Join(homeDir, "dextest", "eth", "harness-ctl") send := func(exe, addr, amt string) error { cmd := exec.CommandContext(ctx, exe, addr, amt) cmd.Dir = harnessCtlDir @@ -375,7 +366,7 @@ func runTestnet(m *testing.M) (int, error) { return 1, fmt.Errorf("error creating testnet participant wallet dir: %v", err) } secPerBlock = testnetSecPerBlock - ethSwapContractAddr = dexeth.ContractAddresses[0][dex.Testnet] + ethSwapContractAddr = dexeth.ContractAddresses[ver][dex.Testnet] fmt.Printf("ETH swap contract address is %v\n", ethSwapContractAddr) err = setupWallet(testnetWalletDir, testnetWalletSeed, "localhost:30355", dex.Testnet) if err != nil { @@ -456,10 +447,15 @@ func runTestnet(m *testing.M) (int, error) { participantAcct = &accts[0] participantAddr = participantAcct.Address - if simnetContractor, err = newV0Contractor(dex.Testnet, simnetAddr, ethClient.contractBackend()); err != nil { + c := newV0Contractor + if ver == 1 { + c = newV1Contractor + } + + if simnetContractor, err = c(dex.Testnet, simnetAddr, ethClient.contractBackend()); err != nil { return 1, fmt.Errorf("newV0Contractor error: %w", err) } - if participantContractor, err = newV0Contractor(dex.Testnet, participantAddr, participantEthClient.contractBackend()); err != nil { + if participantContractor, err = c(dex.Testnet, participantAddr, participantEthClient.contractBackend()); err != nil { return 1, fmt.Errorf("participant newV0Contractor error: %w", err) } @@ -486,8 +482,62 @@ func runTestnet(m *testing.M) (int, error) { return code, nil } +func prepareV0SimnetContractors() (err error) { + return prepareSimnetContractors(newV0Contractor, newV0TokenContractor) +} + +func prepareV1SimnetContractors() (err error) { + return prepareSimnetContractors(newV1Contractor, newV1TokenContractor) +} + +func prepareSimnetContractors(c contractorConstructor, tc tokenContractorConstructor) (err error) { + if simnetContractor, err = c(dex.Simnet, simnetAddr, ethClient.contractBackend()); err != nil { + return fmt.Errorf("new contractor error: %w", err) + } + if participantContractor, err = c(dex.Simnet, participantAddr, participantEthClient.contractBackend()); err != nil { + return fmt.Errorf("participant new contractor error: %w", err) + } + + if simnetTokenContractor, err = tc(dex.Simnet, testTokenID, simnetAddr, ethClient.contractBackend()); err != nil { + return fmt.Errorf("new token contractor error: %w", err) + } + + // I don't know why this is needed for the participant client but not + // the initiator. Without this, we'll get a bind.ErrNoCode from + // (*BoundContract).Call while calling (*ERC20Swap).TokenAddress. + time.Sleep(time.Second) + + if participantTokenContractor, err = tc(dex.Simnet, testTokenID, participantAddr, participantEthClient.contractBackend()); err != nil { + return fmt.Errorf("participant new token contractor error: %w", err) + } + return +} + func TestMain(m *testing.M) { + rand.Seed(time.Now().UnixNano()) dexeth.MaybeReadSimnetAddrs() + targs := make(map[string]string) + for _, param := range strings.Split(os.Getenv("TARGS"), " ") { + if param == "" { + continue + } + var arg string + a := strings.Split(param, "=") + if len(a) > 1 { + param = a[0] + arg = strings.Join(a[1:], "=") + } + targs[strings.TrimLeft(param, "-")] = arg + } + + if _, exists := targs["v1"]; exists { + v1 = true + ver = 1 + } + + ethGases = dexeth.VersionedGases[ver] + tokenGases = &dexeth.Tokens[testTokenID].NetTokens[dex.Simnet].SwapContracts[ver].Gas + var cancel context.CancelFunc ctx, cancel = context.WithCancel(context.Background()) c := make(chan os.Signal, 1) @@ -633,7 +683,6 @@ func TestContract(t *testing.T) { if !t.Run("testAddressesHaveFunds", testAddressesHaveFundsFn(100_000_000 /* gwei */)) { t.Fatal("not enough funds") } - t.Run("testSwap", func(t *testing.T) { testSwap(t, BipID) }) t.Run("testInitiate", func(t *testing.T) { testInitiate(t, BipID) }) t.Run("testRedeem", func(t *testing.T) { testRedeem(t, BipID) }) t.Run("testRefund", func(t *testing.T) { testRefund(t, BipID) }) @@ -646,7 +695,6 @@ func TestGas(t *testing.T) { } func TestTokenContract(t *testing.T) { - t.Run("testTokenSwap", func(t *testing.T) { testSwap(t, testTokenID) }) t.Run("testInitiateToken", func(t *testing.T) { testInitiate(t, testTokenID) }) t.Run("testRedeemToken", func(t *testing.T) { testRedeem(t, testTokenID) }) t.Run("testRefundToken", func(t *testing.T) { testRefund(t, testTokenID) }) @@ -857,17 +905,6 @@ func testPendingTransactions(t *testing.T) { spew.Dump(txs) } -func testSwap(t *testing.T, assetID uint32) { - var secretHash [32]byte - copy(secretHash[:], encode.RandomBytes(32)) - swap, err := simnetContractor.swap(ctx, secretHash) - if err != nil { - t.Fatal(err) - } - // Should be empty. - spew.Dump(swap) -} - func testSyncProgress(t *testing.T) { spew.Dump(ethClient.syncProgress()) } @@ -928,28 +965,28 @@ func feesAtBlk(ctx context.Context, n *nodeClient, blkNum int64) (fees *big.Int, // initiateOverflow is just like *contractorV0.initiate but sets the first swap // value to a max uint256 minus one emv unit. -func initiateOverflow(c *contractorV0, txOpts *bind.TransactOpts, contracts []*asset.Contract) (*types.Transaction, error) { - inits := make([]swapv0.ETHSwapInitiation, 0, len(contracts)) - secrets := make(map[[32]byte]bool, len(contracts)) +func initiateOverflow(c *contractorV0, txOpts *bind.TransactOpts, details []*dex.SwapContractDetails) (*types.Transaction, error) { + inits := make([]swapv0.ETHSwapInitiation, 0, len(details)) + secrets := make(map[[32]byte]bool, len(details)) - for i, contract := range contracts { - if len(contract.SecretHash) != dexeth.SecretHashSize { - return nil, fmt.Errorf("wrong secret hash length. wanted %d, got %d", dexeth.SecretHashSize, len(contract.SecretHash)) + for i, deets := range details { + if len(deets.SecretHash) != dexeth.SecretHashSize { + return nil, fmt.Errorf("wrong secret hash length. wanted %d, got %d", dexeth.SecretHashSize, len(deets.SecretHash)) } var secretHash [32]byte - copy(secretHash[:], contract.SecretHash) + copy(secretHash[:], deets.SecretHash) if secrets[secretHash] { - return nil, fmt.Errorf("secret hash %s is a duplicate", contract.SecretHash) + return nil, fmt.Errorf("secret hash %s is a duplicate", deets.SecretHash) } secrets[secretHash] = true - if !common.IsHexAddress(contract.Address) { - return nil, fmt.Errorf("%q is not an address", contract.Address) + if !common.IsHexAddress(deets.To) { + return nil, fmt.Errorf("%q is not an address", deets.To) } - val := c.evmify(contract.Value) + val := c.evmify(deets.Value) if i == 0 { val = big.NewInt(2) val.Exp(val, big.NewInt(256), nil) @@ -957,9 +994,9 @@ func initiateOverflow(c *contractorV0, txOpts *bind.TransactOpts, contracts []*a fmt.Println(val) } inits = append(inits, swapv0.ETHSwapInitiation{ - RefundTimestamp: big.NewInt(int64(contract.LockTime)), + RefundTimestamp: big.NewInt(int64(deets.LockTime)), SecretHash: secretHash, - Participant: common.HexToAddress(contract.Address), + Participant: common.HexToAddress(deets.To), Value: val, }) } @@ -976,7 +1013,7 @@ func testInitiate(t *testing.T, assetID uint32) { sc := simnetContractor balance := func() (*big.Int, error) { - return ethClient.addressBalance(ctx, simnetAddr) + return ethClient.addressBalance(ctx, ethClient.address()) } gases := ethGases if !isETH { @@ -987,125 +1024,123 @@ func testInitiate(t *testing.T, assetID uint32) { gases = tokenGases } + now := uint64(time.Now().Unix()) + // Create a slice of random secret hashes that can be used in the tests and // make sure none of them have been used yet. numSecretHashes := 10 secretHashes := make([][32]byte, numSecretHashes) for i := 0; i < numSecretHashes; i++ { - copy(secretHashes[i][:], encode.RandomBytes(32)) - swap, err := sc.swap(ctx, secretHashes[i]) - if err != nil { - t.Fatal("unable to get swap state") - } - state := dexeth.SwapStep(swap.State) - if state != dexeth.SSNone { - t.Fatalf("unexpected swap state: want %s got %s", dexeth.SSNone, state) - } + secretHashes[i] = bytesToArray(encode.RandomBytes(32)) } - now := uint64(time.Now().Unix()) + newDetails := func(idx int, v uint64) *dex.SwapContractDetails { + return &dex.SwapContractDetails{ + From: ethClient.address().String(), + To: participantEthClient.address().String(), + LockTime: now, + SecretHash: secretHashes[idx][:], + Value: v, + } + } tests := []struct { name string - swaps []*asset.Contract + swaps []*dex.SwapContractDetails success, overflow bool swapErr bool }{ { name: "1 swap ok", success: true, - swaps: []*asset.Contract{ - newContract(now, secretHashes[0], 2), + swaps: []*dex.SwapContractDetails{ + newDetails(0, 2), }, }, { - name: "1 swap with existing hash", + name: "1 swap with pre-used details", success: false, - swaps: []*asset.Contract{ - newContract(now, secretHashes[0], 1), + swaps: []*dex.SwapContractDetails{ + newDetails(0, 2), }, }, { name: "2 swaps ok", success: true, - swaps: []*asset.Contract{ - newContract(now, secretHashes[1], 1), - newContract(now, secretHashes[2], 1), + swaps: []*dex.SwapContractDetails{ + newDetails(1, 1), + newDetails(2, 1), }, }, { name: "2 swaps repeated hash", success: false, - swaps: []*asset.Contract{ - newContract(now, secretHashes[3], 1), - newContract(now, secretHashes[3], 1), + swaps: []*dex.SwapContractDetails{ + newDetails(3, 1), + newDetails(3, 1), }, swapErr: true, }, - { - name: "1 swap nil refundtimestamp", - success: false, - swaps: []*asset.Contract{ - newContract(0, secretHashes[4], 1), - }, - }, { // Preventing this used to need explicit checks before solidity 0.8, but now the // compiler checks for integer overflows by default. name: "value addition overflows", success: false, overflow: true, - swaps: []*asset.Contract{ - newContract(now, secretHashes[5], 0), // Will be set to max uint256 - 1 evm unit - newContract(now, secretHashes[6], 3), + swaps: []*dex.SwapContractDetails{ + newDetails(5, 0), // Will be set to max uint256 - 1 evm unit + newDetails(6, 3), }, }, { name: "swap with 0 value", success: false, - swaps: []*asset.Contract{ - newContract(now, secretHashes[7], 0), - newContract(now, secretHashes[8], 1), + swaps: []*dex.SwapContractDetails{ + newDetails(7, 0), + newDetails(8, 1), }, }, } for _, test := range tests { - var originalParentBal *big.Int - originalBal, err := balance() if err != nil { t.Fatalf("balance error for asset %d, test %s: %v", assetID, test.name, err) } + var originalParentBal *big.Int + if !isETH { + originalParentBal, err = ethClient.addressBalance(ctx, ethClient.address()) + if err != nil { + t.Fatalf("balance error for eth, test %s: %v", test.name, err) + } + } + var totalVal uint64 originalStates := make(map[string]dexeth.SwapStep) for _, tSwap := range test.swaps { - swap, err := sc.swap(ctx, bytesToArray(tSwap.SecretHash)) + step, _, _, err := sc.status(ctx, tSwap) if err != nil { - t.Fatalf("%s: swap error: %v", test.name, err) + t.Fatalf("%s: status error: %v", test.name, err) } - originalStates[tSwap.SecretHash.String()] = dexeth.SwapStep(swap.State) + originalStates[tSwap.SecretHash.String()] = step totalVal += tSwap.Value } optsVal := totalVal - if !isETH { - optsVal = 0 - originalParentBal, err = ethClient.addressBalance(ctx, simnetAddr) - if err != nil { - t.Fatalf("balance error for eth, test %s: %v", test.name, err) - } - } - if test.overflow { optsVal = 2 + } else if !isETH { + optsVal = 0 } expGas := gases.SwapN(len(test.swaps)) txOpts, _ := ethClient.txOpts(ctx, optsVal, expGas, dexeth.GweiToWei(maxFeeRate)) + + spew.Dump(txOpts) + var tx *types.Transaction - if test.overflow { + if test.overflow && !v1 { switch c := sc.(type) { case *contractorV0: tx, err = initiateOverflow(c, txOpts, test.swaps) @@ -1114,6 +1149,7 @@ func testInitiate(t *testing.T, assetID uint32) { } } else { tx, err = sc.initiate(txOpts, test.swaps) + spew.Dump(tx) } if err != nil { if test.swapErr { @@ -1151,6 +1187,7 @@ func testInitiate(t *testing.T, assetID uint32) { if test.success { wantBal.Sub(wantBal, dexeth.GweiToWei(totalVal)) } + bal, err := balance() if err != nil { t.Fatalf("%s: balance error: %v", test.name, err) @@ -1173,24 +1210,26 @@ func testInitiate(t *testing.T, assetID uint32) { diff := new(big.Int).Sub(wantBal, bal) if diff.CmpAbs(new(big.Int)) != 0 { + fmt.Println("Original balance:", fmtWei(originalBal), ", New balance:", fmtWei(bal), + ", Balance change:", fmtWei(new(big.Int).Sub(bal, originalBal)), + ", Tx fees:", fmtWei(txFee), ", Swap val:", fmtWei(dexeth.GweiToWei(totalVal))) t.Fatalf("%s: unexpected balance change: want %d got %d gwei, diff = %.9f gwei", test.name, dexeth.WeiToGwei(wantBal), dexeth.WeiToGwei(bal), float64(diff.Int64())/dexeth.GweiFactor) } for _, tSwap := range test.swaps { - swap, err := sc.swap(ctx, bytesToArray(tSwap.SecretHash)) + step, _, _, err := sc.status(ctx, tSwap) if err != nil { t.Fatalf("%s: swap error post-init: %v", test.name, err) } - state := dexeth.SwapStep(swap.State) - if test.success && state != dexeth.SSInitiated { - t.Fatalf("%s: wrong success swap state: want %s got %s", test.name, dexeth.SSInitiated, state) + if test.success && step != dexeth.SSInitiated { + t.Fatalf("%s: wrong success swap state: want %s got %s", test.name, dexeth.SSInitiated, step) } originalState := originalStates[hex.EncodeToString(tSwap.SecretHash[:])] - if !test.success && state != originalState { - t.Fatalf("%s: wrong error swap state: want %s got %s", test.name, originalState, state) + if !test.success && step != originalState { + t.Fatalf("%s: wrong error swap state: want %s got %s", test.name, originalState, step) } } } @@ -1216,9 +1255,19 @@ func testRedeemGas(t *testing.T, assetID uint32) { // Initiate swaps now := uint64(time.Now().Unix()) - swaps := make([]*asset.Contract, 0, numSwaps) + newDetails := func(idx int, v uint64) *dex.SwapContractDetails { + return &dex.SwapContractDetails{ + From: ethClient.address().String(), + To: participantEthClient.address().String(), + LockTime: now, + SecretHash: secretHashes[idx][:], + Value: v, + } + } + + details := make([]*dex.SwapContractDetails, 0, numSwaps) for i := 0; i < numSwaps; i++ { - swaps = append(swaps, newContract(now, secretHashes[i], 1)) + details = append(details, newDetails(i, 1)) } gases := ethGases @@ -1232,8 +1281,8 @@ func testRedeemGas(t *testing.T, assetID uint32) { pc = participantTokenContractor } - txOpts, _ := ethClient.txOpts(ctx, optsVal, gases.SwapN(len(swaps)), dexeth.GweiToWei(maxFeeRate)) - tx, err := c.initiate(txOpts, swaps) + txOpts, _ := ethClient.txOpts(ctx, optsVal, gases.SwapN(len(details)), dexeth.GweiToWei(maxFeeRate)) + tx, err := c.initiate(txOpts, details) if err != nil { t.Fatalf("Unable to initiate swap: %v ", err) } @@ -1252,20 +1301,20 @@ func testRedeemGas(t *testing.T, assetID uint32) { } // Make sure swaps were properly initiated - for i := range swaps { - swap, err := c.swap(ctx, bytesToArray(swaps[i].SecretHash)) + for _, deets := range details { + step, _, _, err := c.status(ctx, deets) if err != nil { t.Fatal("unable to get swap state") } - if swap.State != dexeth.SSInitiated { - t.Fatalf("unexpected swap state: want %s got %s", dexeth.SSInitiated, swap.State) + if step != dexeth.SSInitiated { + t.Fatalf("unexpected swap state: want %s got %s", dexeth.SSInitiated, step) } } // Test gas usage of redeem function var previous uint64 for i := 0; i < numSwaps; i++ { - gas, err := pc.estimateRedeemGas(ctx, secrets[:i+1]) + gas, err := pc.estimateRedeemGas(ctx, secrets[:i+1], details[:i+1]) if err != nil { t.Fatalf("Error estimating gas for redeem function: %v", err) } @@ -1313,13 +1362,23 @@ func testRedeem(t *testing.T, assetID uint32) { c, pc = simnetTokenContractor, participantTokenContractor } + newDetails := func(idx int, v uint64) *dex.SwapContractDetails { + return &dex.SwapContractDetails{ + From: ethClient.address().String(), + To: participantEthClient.address().String(), + LockTime: lockTime, + SecretHash: secretHashes[idx][:], + Value: v, + } + } + tests := []struct { name string sleepNBlocks int redeemerClient *nodeClient redeemer *accounts.Account redeemerContractor contractor - swaps []*asset.Contract + swaps []*dex.SwapContractDetails redemptions []*asset.Redemption isRedeemable []bool finalStates []dexeth.SwapStep @@ -1332,8 +1391,8 @@ func testRedeem(t *testing.T, assetID uint32) { redeemerClient: participantEthClient, redeemer: participantAcct, redeemerContractor: pc, - swaps: []*asset.Contract{newContract(lockTime, secretHashes[0], 1)}, - redemptions: []*asset.Redemption{newRedeem(secrets[0], secretHashes[0])}, + swaps: []*dex.SwapContractDetails{newDetails(0, 1)}, + redemptions: []*asset.Redemption{newRedeem(secrets[0], secretHashes[0], 1, lockTime)}, isRedeemable: []bool{true}, finalStates: []dexeth.SwapStep{dexeth.SSRedeemed}, addAmt: true, @@ -1344,13 +1403,13 @@ func testRedeem(t *testing.T, assetID uint32) { redeemerClient: participantEthClient, redeemer: participantAcct, redeemerContractor: pc, - swaps: []*asset.Contract{ - newContract(lockTime, secretHashes[1], 1), - newContract(lockTime, secretHashes[2], 1), + swaps: []*dex.SwapContractDetails{ + newDetails(1, 1), + newDetails(2, 1), }, redemptions: []*asset.Redemption{ - newRedeem(secrets[1], secretHashes[1]), - newRedeem(secrets[2], secretHashes[2]), + newRedeem(secrets[1], secretHashes[1], 1, lockTime), + newRedeem(secrets[2], secretHashes[2], 1, lockTime), }, isRedeemable: []bool{true, true}, finalStates: []dexeth.SwapStep{ @@ -1364,8 +1423,8 @@ func testRedeem(t *testing.T, assetID uint32) { redeemerClient: participantEthClient, redeemer: participantAcct, redeemerContractor: pc, - swaps: []*asset.Contract{newContract(lockTime, secretHashes[3], 1)}, - redemptions: []*asset.Redemption{newRedeem(secrets[3], secretHashes[3])}, + swaps: []*dex.SwapContractDetails{newDetails(3, 1)}, + redemptions: []*asset.Redemption{newRedeem(secrets[3], secretHashes[3], 1, lockTime)}, isRedeemable: []bool{true}, finalStates: []dexeth.SwapStep{dexeth.SSRedeemed}, addAmt: true, @@ -1376,8 +1435,8 @@ func testRedeem(t *testing.T, assetID uint32) { redeemerClient: ethClient, redeemer: simnetAcct, redeemerContractor: c, - swaps: []*asset.Contract{newContract(lockTime, secretHashes[4], 1)}, - redemptions: []*asset.Redemption{newRedeem(secrets[4], secretHashes[4])}, + swaps: []*dex.SwapContractDetails{newDetails(4, 1)}, + redemptions: []*asset.Redemption{newRedeem(secrets[4], secretHashes[4], 1, lockTime)}, isRedeemable: []bool{false}, finalStates: []dexeth.SwapStep{dexeth.SSInitiated}, addAmt: false, @@ -1388,11 +1447,12 @@ func testRedeem(t *testing.T, assetID uint32) { redeemerClient: participantEthClient, redeemer: participantAcct, redeemerContractor: pc, - swaps: []*asset.Contract{newContract(lockTime, secretHashes[5], 1)}, - redemptions: []*asset.Redemption{newRedeem(secrets[6], secretHashes[5])}, + swaps: []*dex.SwapContractDetails{newDetails(5, 1)}, + redemptions: []*asset.Redemption{newRedeem(secrets[6], secretHashes[5], 1, lockTime)}, isRedeemable: []bool{false}, finalStates: []dexeth.SwapStep{dexeth.SSInitiated}, addAmt: false, + expectRedeemErr: v1, // Why doesn't v0 error on this? }, { name: "duplicate secret hashes", @@ -1401,15 +1461,15 @@ func testRedeem(t *testing.T, assetID uint32) { redeemerClient: participantEthClient, redeemer: participantAcct, redeemerContractor: pc, - swaps: []*asset.Contract{ - newContract(lockTime, secretHashes[7], 1), - newContract(lockTime, secretHashes[8], 1), + swaps: []*dex.SwapContractDetails{ + newDetails(7, 1), + newDetails(8, 1), }, redemptions: []*asset.Redemption{ - newRedeem(secrets[7], secretHashes[7]), - newRedeem(secrets[7], secretHashes[7]), + newRedeem(secrets[7], secretHashes[7], 1, lockTime), + newRedeem(secrets[7], secretHashes[7], 1, lockTime), }, - isRedeemable: []bool{true, true}, + isRedeemable: []bool{true, false /* doesn't match SwapContractDetails */}, finalStates: []dexeth.SwapStep{ dexeth.SSInitiated, dexeth.SSInitiated, @@ -1420,14 +1480,13 @@ func testRedeem(t *testing.T, assetID uint32) { for _, test := range tests { var optsVal uint64 - for i, contract := range test.swaps { - swap, err := c.swap(ctx, bytesToArray(test.swaps[i].SecretHash)) + for _, contract := range test.swaps { + step, _, _, err := c.status(ctx, contract) if err != nil { t.Fatal("unable to get swap state") } - state := dexeth.SwapStep(swap.State) - if state != dexeth.SSNone { - t.Fatalf("unexpected swap state for test %v: want %s got %s", test.name, dexeth.SSNone, state) + if step != dexeth.SSNone { + t.Fatalf("unexpected swap state for test %v: want %s got %s", test.name, dexeth.SSNone, step) } if isETH { optsVal += contract.Value @@ -1466,23 +1525,23 @@ func testRedeem(t *testing.T, assetID uint32) { } fmt.Printf("Gas used for %d inits: %d \n", len(test.swaps), receipt.GasUsed) - for i := range test.swaps { - swap, err := test.redeemerContractor.swap(ctx, bytesToArray(test.swaps[i].SecretHash)) + for i, contract := range test.swaps { + step, _, _, err := test.redeemerContractor.status(ctx, contract) if err != nil { t.Fatal("unable to get swap state") } - if swap.State != dexeth.SSInitiated { - t.Fatalf("unexpected swap state for test %v: want %s got %s", test.name, dexeth.SSInitiated, swap.State) + if step != dexeth.SSInitiated { + t.Fatalf("unexpected swap state for test %v: want %s got %s", test.name, dexeth.SSInitiated, step) } expected := test.isRedeemable[i] redemption := test.redemptions[i] - isRedeemable, err := test.redeemerContractor.isRedeemable(bytesToArray(redemption.Spends.SecretHash), bytesToArray(redemption.Secret)) + isRedeemable, err := test.redeemerContractor.isRedeemable(bytesToArray(redemption.Secret), contract) if err != nil { t.Fatalf(`test "%v": error calling isRedeemable: %v`, test.name, err) } else if isRedeemable != expected { - t.Fatalf(`test "%v": expected isRedeemable to be %v, but got %v. swap = %+v`, test.name, expected, isRedeemable, swap) + t.Fatalf(`test "%v": expected isRedeemable to be %v, but got %v. step = %s`, test.name, expected, isRedeemable, step) } } @@ -1500,6 +1559,11 @@ func testRedeem(t *testing.T, assetID uint32) { } expGas := gases.RedeemN(len(test.redemptions)) + // Ethereum is weird. For v1, if I use 42,000 here, it will often fail, + // using all of the gas, so presumably it fails because of insufficient + // gas. But if I set a higher limit, then it will succeed, and oddly + // will never use 42,000 gas. Why does it use more gas when I use a + // lower limit? txOpts, _ = test.redeemerClient.txOpts(ctx, 0, expGas, dexeth.GweiToWei(maxFeeRate)) tx, err = test.redeemerContractor.redeem(txOpts, test.redemptions) if test.expectRedeemErr { @@ -1522,12 +1586,12 @@ func testRedeem(t *testing.T, assetID uint32) { t.Fatalf("%s: failed to get redeem receipt: %v", test.name, err) } spew.Dump(receipt) - expSuccess := !test.expectRedeemErr && test.addAmt err = checkTxStatus(receipt, txOpts.GasLimit) if err != nil && expSuccess { t.Fatalf("%s: failed redeem transaction status: %v", test.name, err) } + fmt.Printf("Gas used for %d redeems, success = %t: %d \n", len(test.swaps), expSuccess, receipt.GasUsed) bal, err := balance() @@ -1579,15 +1643,14 @@ func testRedeem(t *testing.T, assetID uint32) { test.name, dexeth.WeiToGwei(wantBal), dexeth.WeiToGwei(bal), float64(diff.Int64())/dexeth.GweiFactor) } - for i, redemption := range test.redemptions { - swap, err := c.swap(ctx, bytesToArray(redemption.Spends.SecretHash)) + for i := range test.redemptions { + step, _, _, err := c.status(ctx, test.swaps[i]) if err != nil { t.Fatalf("unexpected error for test %v: %v", test.name, err) } - state := dexeth.SwapStep(swap.State) - if state != test.finalStates[i] { + if step != test.finalStates[i] { t.Fatalf("unexpected swap state for test %v [%d]: want %s got %s", - test.name, i, test.finalStates[i], state) + test.name, i, test.finalStates[i], step) } } } @@ -1616,7 +1679,14 @@ func testRefundGas(t *testing.T, assetID uint32) { lockTime := uint64(time.Now().Unix()) txOpts, _ := ethClient.txOpts(ctx, optsVal, gases.SwapN(1), nil) - _, err := c.initiate(txOpts, []*asset.Contract{newContract(lockTime, secretHash, 1)}) + deets := &dex.SwapContractDetails{ + From: ethClient.address().String(), + To: participantEthClient.address().String(), + LockTime: lockTime, + SecretHash: secretHash[:], + Value: 1, + } + _, err := c.initiate(txOpts, []*dex.SwapContractDetails{deets}) if err != nil { t.Fatalf("Unable to initiate swap: %v ", err) } @@ -1624,16 +1694,15 @@ func testRefundGas(t *testing.T, assetID uint32) { t.Fatalf("unexpected error while waiting to mine: %v", err) } - swap, err := c.swap(ctx, secretHash) + step, _, _, err := c.status(ctx, deets) if err != nil { t.Fatal("unable to get swap state") } - state := dexeth.SwapStep(swap.State) - if state != dexeth.SSInitiated { - t.Fatalf("unexpected swap state: want %s got %s", dexeth.SSInitiated, state) + if step != dexeth.SSInitiated { + t.Fatalf("unexpected swap state: want %s got %s", dexeth.SSInitiated, step) } - gas, err := c.estimateRefundGas(ctx, secretHash) + gas, err := c.estimateRefundGas(ctx, deets) if err != nil { t.Fatalf("Error estimating gas for refund function: %v", err) } @@ -1726,38 +1795,56 @@ func testRefund(t *testing.T, assetID uint32) { var secret [32]byte copy(secret[:], encode.RandomBytes(32)) secretHash := sha256.Sum256(secret[:]) + inLocktime := uint64(time.Now().Add(test.addTime).Unix()) + deets := &dex.SwapContractDetails{ + From: ethClient.address().String(), + To: participantEthClient.address().String(), + LockTime: inLocktime, + SecretHash: secretHash[:], + Value: amt, + } - swap, err := test.refunderContractor.swap(ctx, secretHash) + step, _, _, err := test.refunderContractor.status(ctx, deets) if err != nil { t.Fatalf("%s: unable to get swap state pre-init", test.name) } - if swap.State != dexeth.SSNone { - t.Fatalf("unexpected swap state for test %v: want %s got %s", test.name, dexeth.SSNone, swap.State) + if step != dexeth.SSNone { + t.Fatalf("unexpected swap state for test %v: want %s got %s", test.name, dexeth.SSNone, step) } - inLocktime := uint64(time.Now().Add(test.addTime).Unix()) - txOpts, _ := ethClient.txOpts(ctx, optsVal, gases.SwapN(1), nil) - _, err = c.initiate(txOpts, []*asset.Contract{newContract(inLocktime, secretHash, amt)}) + _, err = c.initiate(txOpts, []*dex.SwapContractDetails{deets}) if err != nil { t.Fatalf("%s: initiate error: %v ", test.name, err) } - if test.redeem { - if err := waitForMined(t, sleepForNBlocks, false); err != nil { - t.Fatalf("%s: pre-redeem mining error: %v", test.name, err) - } + if err := waitForMined(t, sleepForNBlocks, false); err != nil { + t.Fatalf("%s: pre-redeem mining error: %v", test.name, err) + } - txOpts, _ = participantEthClient.txOpts(ctx, 0, gases.RedeemN(1), nil) - _, err := pc.redeem(txOpts, []*asset.Redemption{newRedeem(secret, secretHash)}) + if test.redeem { + expGas := gases.RedeemN(1) + txOpts, _ = participantEthClient.txOpts(ctx, 0, expGas, nil) + tx, err := pc.redeem(txOpts, []*asset.Redemption{newRedeem(secret, secretHash, amt, deets.LockTime)}) if err != nil { t.Fatalf("%s: redeem error: %v", test.name, err) } - } - // This waitForMined will always take test.sleep to complete. - if err := waitForMined(t, sleepForNBlocks, true); err != nil { - t.Fatalf("unexpected post-init mining error for test %v: %v", test.name, err) + // This waitForMined will always take test.sleep to complete. + if err := waitForMined(t, sleepForNBlocks, true); err != nil { + t.Fatalf("unexpected post-init mining error for test %v: %v", test.name, err) + } + + r, err := ethClient.transactionReceipt(ctx, tx.Hash()) + if err != nil { + t.Fatalf("error getting redemption receipt: %v", err) + } + if err = checkTxStatus(r, txOpts.GasLimit); err != nil { + t.Fatalf("redemption error: %v", err) + } + if r.GasUsed > expGas { + t.Fatalf("gas used, %d, is more than expected max, %d", r.GasUsed, expGas) + } } var originalParentBal *big.Int @@ -1773,7 +1860,7 @@ func testRefund(t *testing.T, assetID uint32) { t.Fatalf("%s: balance error: %v", test.name, err) } - isRefundable, err := test.refunderContractor.isRefundable(secretHash) + isRefundable, err := test.refunderContractor.isRefundable(deets) if err != nil { t.Fatalf("%s: isRefundable error %v", test.name, err) } @@ -1783,7 +1870,7 @@ func testRefund(t *testing.T, assetID uint32) { } txOpts, _ = test.refunderClient.txOpts(ctx, 0, gases.Refund, nil) - tx, err := test.refunderContractor.refund(txOpts, secretHash) + tx, err := test.refunderContractor.refund(txOpts, deets) if err != nil { t.Fatalf("%s: refund error: %v", test.name, err) } @@ -1850,16 +1937,19 @@ func testRefund(t *testing.T, assetID uint32) { diff := new(big.Int).Sub(wantBal, bal) if diff.CmpAbs(dexeth.GweiToWei(1)) >= 0 { + fmt.Println("Original balance:", fmtWei(originalBal), ", New balance:", fmtWei(bal), + ", Balance change:", fmtWei(new(big.Int).Sub(bal, originalBal)), + ", Tx fees:", fmtWei(txFee), ", Swap val:", fmtWei(dexeth.GweiToWei(amt))) t.Fatalf("%s: unexpected balance change: want %d got %d, diff = %d", test.name, dexeth.WeiToGwei(wantBal), dexeth.WeiToGwei(bal), dexeth.WeiToGwei(diff)) } - swap, err = test.refunderContractor.swap(ctx, secretHash) + step, _, _, err = test.refunderContractor.status(ctx, deets) if err != nil { t.Fatalf("%s: post-refund swap error: %v", test.name, err) } - if swap.State != test.finalState { - t.Fatalf("%s: wrong swap state: want %s got %s", test.name, test.finalState, swap.State) + if step != test.finalState { + t.Fatalf("%s: wrong swap state: want %s got %s", test.name, test.finalState, step) } } } @@ -1932,6 +2022,7 @@ func TestReplayAttack(t *testing.T) { } var secretHash [32]byte + var inLocktime uint64 // Make four swaps that should be locked and refundable and one that is // soon refundable. for i := 0; i < 5; i++ { @@ -1940,10 +2031,17 @@ func TestReplayAttack(t *testing.T) { secretHash = sha256.Sum256(secret[:]) if i != 4 { - inLocktime := uint64(time.Now().Add(time.Hour).Unix()) + inLocktime = uint64(time.Now().Add(time.Hour).Unix()) + details := []*dex.SwapContractDetails{{ + From: ethClient.address().String(), + To: participantEthClient.address().String(), + LockTime: inLocktime, + SecretHash: secretHash[:], + Value: 1, + }} txOpts, _ := ethClient.txOpts(ctx, 1, ethGases.SwapN(1), nil) - _, err = simnetContractor.initiate(txOpts, []*asset.Contract{newContract(inLocktime, secretHash, 1)}) + _, err = simnetContractor.initiate(txOpts, details) if err != nil { t.Fatalf("unable to initiate swap: %v ", err) } @@ -1957,12 +2055,12 @@ func TestReplayAttack(t *testing.T) { intermediateContractVal, _ := ethClient.addressBalance(ctx, ethSwapContractAddr) t.Logf("intermediate contract value %d", dexeth.WeiToGwei(intermediateContractVal)) - inLocktime := time.Now().Add(-1 * time.Second).Unix() + inLocktime = uint64(time.Now().Add(-1 * time.Second).Unix()) // Set some variables in the contract used for the exploit. This // will fail (silently) due to require(msg.origin == msg.sender) // in the real contract. txOpts, _ := ethClient.txOpts(ctx, 1, defaultSendGasLimit*5, nil) - _, err := reentryContract.SetUsUpTheBomb(txOpts, ethSwapContractAddr, secretHash, big.NewInt(inLocktime), participantAddr) + _, err := reentryContract.SetUsUpTheBomb(txOpts, ethSwapContractAddr, secretHash, big.NewInt(int64(inLocktime)), participantAddr) if err != nil { t.Fatalf("unable to set up the bomb: %v", err) } @@ -2041,13 +2139,18 @@ func TestReplayAttack(t *testing.T) { // The exploit failed and status should be SSNone because initiation also // failed. - swap, err := simnetContractor.swap(ctx, secretHash) + step, _, _, err := simnetContractor.status(ctx, &dex.SwapContractDetails{ + From: ethClient.address().String(), + To: participantEthClient.address().String(), + LockTime: inLocktime, + SecretHash: secretHash[:], + Value: 1, + }) if err != nil { t.Fatal(err) } - state := dexeth.SwapStep(swap.State) - if state != dexeth.SSNone { - t.Fatalf("unexpected swap state: want %s got %s", dexeth.SSNone, state) + if step != dexeth.SSNone { + t.Fatalf("unexpected swap state: want %s got %s", dexeth.SSNone, step) } // The contract should hold four more ether because initiation of one @@ -2221,3 +2324,7 @@ func exportAccountsFromNode(node *node.Node) ([]accounts.Account, error) { } return ks.Accounts(), nil } + +func fmtWei(v *big.Int) string { + return fmt.Sprintf("%.9f gwei", float64(new(big.Int).Div(v, big.NewInt(dexeth.GweiFactor)).Int64())) +} diff --git a/client/asset/interface.go b/client/asset/interface.go index 8a32a30c17..2909c1a05a 100644 --- a/client/asset/interface.go +++ b/client/asset/interface.go @@ -178,9 +178,12 @@ type Token struct { type WalletInfo struct { // Name is the display name for the currency, e.g. "Decred" Name string `json:"name"` - // Version is the Wallet's version number, which is used to signal when - // major changes are made to internal details such as coin ID encoding and - // contract structure that must be common to a server's. + // ProtocolVersions is the Wallet's accepted server version numbers. + // TODO: Handle unsupported protocols at the UI (instruct user to update + // dexc). + ProtocolVersions []uint32 `json:"protocolVersions"` + // Version is the highest server backend version supported by the client. + // Deprecated: Use ProtocolVersions. Version uint32 `json:"version"` // AvailableWallets is an ordered list of available WalletDefinition. The // first WalletDefinition is considered the default, and might, for instance @@ -325,7 +328,7 @@ type Wallet interface { // contract can be refunded since assets have different rules to satisfy the // lock. For example, in Bitcoin the median of the last 11 blocks must be // past the expiry time, not the current time. - LocktimeExpired(ctx context.Context, contract dex.Bytes) (bool, time.Time, error) + LocktimeExpired(ctx context.Context, deets *dex.SwapContractDetails) (bool, time.Time, error) // FindRedemption watches for the input that spends the specified // coin and contract, and returns the spending input and the // secret key when it finds a spender. @@ -342,7 +345,7 @@ type Wallet interface { // NOTE: This could potentially be a long and expensive operation if // performed long after the swap is broadcast; might be better executed from // a goroutine. - FindRedemption(ctx context.Context, coinID, contract dex.Bytes) (redemptionCoin, secret dex.Bytes, err error) + FindRedemption(ctx context.Context, coinID, contractData dex.Bytes, deets *dex.SwapContractDetails) (redemptionCoin, secret dex.Bytes, err error) // Refund refunds a contract. This can only be used after the time lock has // expired AND if the contract has not been redeemed/refunded. This method // MUST return an asset.CoinNotFoundError error if the swap is already @@ -351,7 +354,7 @@ type Wallet interface { // the unspent coin info as the wallet does not store it, even though it was // known when the init transaction was created. The client should store this // information for persistence across sessions. - Refund(coinID, contract dex.Bytes, feeRate uint64) (dex.Bytes, error) + Refund(coinID, contractData dex.Bytes, deets *dex.SwapContractDetails, feeRate uint64) (dex.Bytes, error) // DepositAddress returns an address for depositing funds into the exchange // wallet. DepositAddress() (string, error) @@ -378,7 +381,7 @@ type Wallet interface { // be accurate. // The contract and matchTime are provided so that wallets may search for // the coin using light filters. - SwapConfirmations(ctx context.Context, coinID dex.Bytes, contract dex.Bytes, matchTime time.Time) (confs uint32, spent bool, err error) + SwapConfirmations(ctx context.Context, coinID dex.Bytes, contract dex.Bytes, matchTime time.Time, deets *dex.SwapContractDetails) (confs uint32, spent bool, err error) // ValidateSecret checks that the secret hashes to the secret hash. ValidateSecret(secret, secretHash []byte) bool // SyncStatus is information about the blockchain sync status. It should @@ -646,6 +649,7 @@ type Receipt interface { type AuditInfo struct { // Recipient is the string-encoded recipient address. Recipient string + Initiator string // Expiration is the unix timestamp of the contract time lock expiration. Expiration time.Time // Coin is the coin that contains the contract. @@ -686,7 +690,7 @@ type Swaps struct { // Contract is a swap contract. type Contract struct { - // Address is the receiving address. + // From is the receiving address. Address string // Value is the amount being traded. Value uint64 @@ -701,6 +705,8 @@ type Contract struct { type Redemption struct { // Spends is the AuditInfo for the swap output being spent. Spends *AuditInfo + // SwapDetails is the swap details. + SwapDetails *dex.SwapContractDetails // Secret is the secret key needed to satisfy the swap contract. Secret dex.Bytes } diff --git a/client/asset/ltc/ltc.go b/client/asset/ltc/ltc.go index feb58cd6e3..22580278f7 100644 --- a/client/asset/ltc/ltc.go +++ b/client/asset/ltc/ltc.go @@ -119,6 +119,7 @@ var ( rpcWalletDefinition, electrumWalletDefinition, }, + ProtocolVersions: []uint32{1}, } ) diff --git a/client/asset/zec/zec.go b/client/asset/zec/zec.go index 7a4ca5c517..7ee72e0568 100644 --- a/client/asset/zec/zec.go +++ b/client/asset/zec/zec.go @@ -101,6 +101,7 @@ var ( ConfigOpts: configOpts, NoAuth: true, }}, + ProtocolVersions: []uint32{0}, } ) diff --git a/client/core/core.go b/client/core/core.go index 87af059f03..5f86e9e67d 100644 --- a/client/core/core.go +++ b/client/core/core.go @@ -5077,6 +5077,16 @@ func (w *walletSet) trimmedConventionalRateString(r uint64) string { return strings.TrimRight(strings.TrimRight(s, "0"), ".") } +func checkWalletDEXCompatibility(a *dex.Asset, w *xcWallet) error { + for _, ver := range w.Info().ProtocolVersions { + if a.Version == ver { + return nil + } + } + return newError(walletErr, "%s wallet version %d is not one of the wallet's supported versions %+v", + unbip(w.AssetID), a.Version, w.Info().ProtocolVersions) +} + // walletSet constructs a walletSet. func (c *Core) walletSet(dc *dexConnection, baseID, quoteID uint32, sell bool) (*walletSet, error) { dc.assetsMtx.RLock() @@ -5100,14 +5110,11 @@ func (c *Core) walletSet(dc *dexConnection, baseID, quoteID uint32, sell bool) ( return nil, newError(missingWalletErr, "no wallet found for %s", unbip(quoteID)) } - if ver := baseWallet.Info().Version; baseAsset.Version != ver { - return nil, newError(walletErr, "wallet asset %d version %d does not match server asset version %d", - baseID, ver, baseAsset.Version) + if err := checkWalletDEXCompatibility(baseAsset, baseWallet); err != nil { + return nil, err } - - if ver := quoteWallet.Info().Version; quoteAsset.Version != ver { - return nil, newError(walletErr, "wallet asset %d version %d does not match server asset version %d", - quoteID, ver, quoteAsset.Version) + if err := checkWalletDEXCompatibility(quoteAsset, quoteWallet); err != nil { + return nil, err } // We actually care less about base/quote, and more about from/to, which diff --git a/client/core/core_test.go b/client/core/core_test.go index 019167cdb4..fa1f43385b 100644 --- a/client/core/core_test.go +++ b/client/core/core_test.go @@ -696,7 +696,7 @@ func newTWallet(assetID uint32) (*xcWallet, *TXCWallet) { func (w *TXCWallet) Info() *asset.WalletInfo { return &asset.WalletInfo{ - Version: 0, // match tUTXOAssetA/tUTXOAssetB + ProtocolVersions: []uint32{0}, // match tUTXOAssetA/tUTXOAssetB } } @@ -810,15 +810,15 @@ func (w *TXCWallet) AuditContract(coinID, contract, txData dex.Bytes, rebroadcas return w.auditInfo, w.auditErr } -func (w *TXCWallet) LocktimeExpired(_ context.Context, contract dex.Bytes) (bool, time.Time, error) { +func (w *TXCWallet) LocktimeExpired(_ context.Context, deets *dex.SwapContractDetails) (bool, time.Time, error) { return w.contractExpired, w.contractLockTime, nil } -func (w *TXCWallet) FindRedemption(ctx context.Context, coinID, _ dex.Bytes) (redemptionCoin, secret dex.Bytes, err error) { +func (w *TXCWallet) FindRedemption(ctx context.Context, coinID, _ dex.Bytes, _ *dex.SwapContractDetails) (redemptionCoin, secret dex.Bytes, err error) { return nil, nil, fmt.Errorf("not mocked") } -func (w *TXCWallet) Refund(refundCoin dex.Bytes, refundContract dex.Bytes, feeSuggestion uint64) (dex.Bytes, error) { +func (w *TXCWallet) Refund(refundCoin, contractData dex.Bytes, deets *dex.SwapContractDetails, feeSuggestion uint64) (dex.Bytes, error) { w.refundFeeSuggestion = feeSuggestion return w.refundCoin, w.refundErr } @@ -888,7 +888,7 @@ func (w *TXCWallet) tConfirmations(ctx context.Context, coinID dex.Bytes) (uint3 return w.confs[id], w.confsErr[id] } -func (w *TXCWallet) SwapConfirmations(ctx context.Context, coinID dex.Bytes, contract dex.Bytes, matchTime time.Time) (uint32, bool, error) { +func (w *TXCWallet) SwapConfirmations(ctx context.Context, coinID dex.Bytes, contractData dex.Bytes, matchTime time.Time, deets *dex.SwapContractDetails) (uint32, bool, error) { confs, err := w.tConfirmations(ctx, coinID) return confs, false, err } @@ -8409,6 +8409,7 @@ func TestConfirmRedemption(t *testing.T) { Address: addr, }, }, + trade: lo.Trade(), } tracker.matches = map[order.MatchID]*matchTracker{matchID: match} @@ -8889,7 +8890,7 @@ func TestSuspectTrades(t *testing.T) { setRedeems() tBtcWallet.redeemErr = tErr _, err = tCore.tick(tracker) - if err == nil || !strings.Contains(err.Error(), "error sending redeem transaction") { + if err == nil || !strings.Contains(err.Error(), "error redeeming") { t.Fatalf("redeem error not propagated. err = %v", err) } if tBtcWallet.redeemCounter != 1 { diff --git a/client/core/trade.go b/client/core/trade.go index 37d5ac49be..b41024475d 100644 --- a/client/core/trade.go +++ b/client/core/trade.go @@ -177,6 +177,55 @@ func (m *matchTracker) token() string { return hex.EncodeToString(m.MatchID[:4]) } +func (t *trackedTrade) theirContract(m *matchTracker) *dex.SwapContractDetails { + value := m.Quantity + if m.trade.Sell { + value = calc.BaseToQuote(m.Rate, m.Quantity) + } + + matchTime := m.matchTime() + lockTime := matchTime.Add(t.lockTimeMaker).Unix() + if m.Side == order.Maker { + lockTime = matchTime.Add(t.lockTimeTaker).Unix() + } + + var initiator string + // In practice, counterSwap should never be nil in the places we need it. + // Do a sanity check anyway. + if m.counterSwap != nil { + initiator = m.counterSwap.Initiator + } + + return &dex.SwapContractDetails{ + From: initiator, + To: t.Trade().Address, + Value: value, + SecretHash: m.MetaData.Proof.SecretHash, + LockTime: uint64(lockTime), + } +} + +func (t *trackedTrade) ourContract(m *matchTracker) *dex.SwapContractDetails { + value := m.Quantity + if !m.trade.Sell { + value = calc.BaseToQuote(m.Rate, m.Quantity) + } + + matchTime := m.matchTime() + lockTime := matchTime.Add(t.lockTimeTaker).Unix() + if m.Side == order.Maker { + lockTime = matchTime.Add(t.lockTimeMaker).Unix() + } + + return &dex.SwapContractDetails{ + From: t.Trade().FromAccount(), + To: m.Address, + Value: value, + SecretHash: m.MetaData.Proof.SecretHash, + LockTime: uint64(lockTime), + } +} + // trackedCancel is information necessary to track a cancel order. A // trackedCancel is always associated with a trackedTrade. type trackedCancel struct { @@ -985,7 +1034,7 @@ func (t *trackedTrade) counterPartyConfirms(ctx context.Context, match *matchTra wallet := t.wallets.toWallet coin := match.counterSwap.Coin - _, lockTime, err := wallet.LocktimeExpired(ctx, match.MetaData.Proof.CounterContract) + _, lockTime, err := wallet.LocktimeExpired(ctx, t.theirContract(match)) if err != nil { return fail(fmt.Errorf("error checking if locktime has expired on taker's contract on order %s, "+ "match %s: %w", t.ID(), match, err)) @@ -993,7 +1042,7 @@ func (t *trackedTrade) counterPartyConfirms(ctx context.Context, match *matchTra expired = time.Until(lockTime) < 0 // not necessarily refundable, but can be at any moment have, spent, err = wallet.swapConfirmations(ctx, coin.ID(), - match.MetaData.Proof.CounterContract, match.MetaData.Stamp) + match.MetaData.Proof.CounterContract, match.MetaData.Stamp, t.theirContract(match)) if err != nil { return fail(fmt.Errorf("failed to get confirmations of the counter-party's swap %s (%s) "+ "for match %s, order %v: %w", @@ -1251,7 +1300,7 @@ func (t *trackedTrade) isSwappable(ctx context.Context, match *matchTracker) (re // If we're the maker, check the confirmations anyway so we can notify. confs, spent, err := wallet.swapConfirmations(ctx, match.MetaData.Proof.MakerSwap, - match.MetaData.Proof.ContractData, match.MetaData.Stamp) + match.MetaData.Proof.ContractData, match.MetaData.Stamp, t.ourContract(match)) if err != nil && !errors.Is(err, asset.ErrSwapNotInitiated) { // No need to log an error if swap not initiated as this // is expected for newly made swaps involving contracts. @@ -1352,7 +1401,7 @@ func (t *trackedTrade) isRedeemable(ctx context.Context, match *matchTracker) (r // If we're the taker, check the confirmations anyway so we can notify. confs, spent, err := t.wallets.fromWallet.swapConfirmations(ctx, match.MetaData.Proof.TakerSwap, - match.MetaData.Proof.ContractData, match.MetaData.Stamp) + match.MetaData.Proof.ContractData, match.MetaData.Stamp, t.ourContract(match)) if err != nil && !errors.Is(err, asset.ErrSwapNotInitiated) { // No need to log an error if swap not initiated as this // is expected for newly made swaps involving contracts. @@ -1409,7 +1458,7 @@ func (t *trackedTrade) isRefundable(ctx context.Context, match *matchTracker) bo } // Issue a refund if our swap's locktime has expired. - swapLocktimeExpired, contractExpiry, err := wallet.LocktimeExpired(ctx, match.MetaData.Proof.ContractData) + swapLocktimeExpired, contractExpiry, err := wallet.LocktimeExpired(ctx, t.ourContract(match)) if err != nil { if !errors.Is(err, asset.ErrSwapNotInitiated) { // No need to log an error as this is expected for newly @@ -1476,7 +1525,7 @@ func (t *trackedTrade) shouldBeginFindRedemption(ctx context.Context, match *mat return false } - confs, spent, err := t.wallets.fromWallet.swapConfirmations(ctx, swapCoinID, proof.ContractData, match.MetaData.Stamp) + confs, spent, err := t.wallets.fromWallet.swapConfirmations(ctx, swapCoinID, proof.ContractData, match.MetaData.Stamp, t.ourContract(match)) if err != nil { if !errors.Is(err, asset.ErrSwapNotInitiated) { // No need to log an error if swap not initiated as this @@ -1942,10 +1991,10 @@ func (c *Core) swapMatchGroup(t *trackedTrade, matches []*matchTracker, errs *er matchTime := match.matchTime() lockTime := matchTime.Add(t.lockTimeTaker).UTC().Unix() if match.Side == order.Maker { + lockTime = matchTime.Add(t.lockTimeMaker).UTC().Unix() match.MetaData.Proof.Secret = encode.RandomBytes(32) secretHash := sha256.Sum256(match.MetaData.Proof.Secret) match.MetaData.Proof.SecretHash = secretHash[:] - lockTime = matchTime.Add(t.lockTimeMaker).UTC().Unix() } contracts[i] = &asset.Contract{ @@ -2290,8 +2339,9 @@ func (c *Core) redeemMatchGroup(t *trackedTrade, matches []*matchTracker, errs * redemptions := make([]*asset.Redemption, 0, len(matches)) for _, match := range matches { redemptions = append(redemptions, &asset.Redemption{ - Spends: match.counterSwap, - Secret: match.MetaData.Proof.Secret, + Spends: match.counterSwap, + SwapDetails: t.theirContract(match), + Secret: match.MetaData.Proof.Secret, }) } @@ -2355,7 +2405,7 @@ func (c *Core) redeemMatchGroup(t *trackedTrade, matches []*matchTracker, errs * } match.delayTicks(waitTime) } - errs.add("error sending redeem transaction: %v", err) + errs.add("error redeeming: %v", err) return } @@ -2530,8 +2580,9 @@ func (t *trackedTrade) confirmRedemption(match *matchTracker) { match.confirmRedemptionNumTries++ redemptionStatus, err := confirmer.ConfirmRedemption(dex.Bytes(redeemCoinID), &asset.Redemption{ - Spends: match.counterSwap, - Secret: match.MetaData.Proof.Secret, + Spends: match.counterSwap, + SwapDetails: t.theirContract(match), + Secret: match.MetaData.Proof.Secret, }) if errors.Is(asset.ErrSwapRefunded, err) { subject, details := t.formatDetails(TopicSwapRefunded, match.token(), t.token()) @@ -2612,7 +2663,7 @@ func (t *trackedTrade) findMakersRedemption(ctx context.Context, match *matchTra // Run redemption finder in goroutine. go func() { defer cancel() // don't leak the context when we reset match.cancelRedemptionSearch - redemptionCoinID, secret, err := t.wallets.fromWallet.FindRedemption(ctx, swapCoinID, swapContract) + redemptionCoinID, secret, err := t.wallets.fromWallet.FindRedemption(ctx, swapCoinID, swapContract, t.theirContract(match)) // Redemption search done, with or without error. // Keep the mutex locked for the remainder of this goroutine execution to @@ -2721,7 +2772,7 @@ func (c *Core) refundMatches(t *trackedTrade, matches []*matchTracker) (uint64, feeRate = c.feeSuggestionAny(refundAsset.ID) // includes wallet itself } - refundCoin, err := refundWallet.Refund(swapCoinID, contractToRefund, feeRate) + refundCoin, err := refundWallet.Refund(swapCoinID, contractToRefund, t.ourContract(match), feeRate) if err != nil { // CRITICAL - Refund must indicate if the swap is spent (i.e. // redeemed already) so that as taker we will start the diff --git a/client/core/types.go b/client/core/types.go index 58496a1a21..b261c0dcb2 100644 --- a/client/core/types.go +++ b/client/core/types.go @@ -104,7 +104,7 @@ type WalletBalance struct { // WalletState is the current status of an exchange wallet. type WalletState struct { Symbol string `json:"symbol"` - AssetID uint32 `json:"assetID"` + AssetID uint32 `json:"assetID"` // Deprecated: use ProtocolVersions Version uint32 `json:"version"` WalletType string `json:"type"` Traits asset.WalletTrait `json:"traits"` @@ -117,6 +117,8 @@ type WalletState struct { PeerCount uint32 `json:"peerCount"` Synced bool `json:"synced"` SyncProgress float32 `json:"syncProgress"` + // ProtocolVersions is the supported server protocol versions. + ProtocolVersions []uint32 `json:"protocolVersions"` } // User is information about the user's wallets and DEX accounts. diff --git a/client/core/wallet.go b/client/core/wallet.go index 5d9e557b06..7240a988b7 100644 --- a/client/core/wallet.go +++ b/client/core/wallet.go @@ -217,6 +217,9 @@ func (w *xcWallet) state() *WalletState { SyncProgress: w.syncProgress, WalletType: w.walletType, Traits: w.traits, + // ProtocolVersions were added around 0.6, and signal the supported + // server asset backend versions. + ProtocolVersions: winfo.ProtocolVersions, } } @@ -372,8 +375,8 @@ func (w *xcWallet) preAccelerate(swapCoins, accelerationCoins []dex.Bytes, chang // Context. If the coin cannot be located, an asset.CoinNotFoundError is // returned. If the coin is located, but recognized as spent, no error is // returned. -func (w *xcWallet) swapConfirmations(ctx context.Context, coinID []byte, contract []byte, matchTime uint64) (uint32, bool, error) { - return w.Wallet.SwapConfirmations(ctx, coinID, contract, time.UnixMilli(int64(matchTime))) +func (w *xcWallet) swapConfirmations(ctx context.Context, coinID []byte, contractData []byte, matchTime uint64, deets *dex.SwapContractDetails) (uint32, bool, error) { + return w.Wallet.SwapConfirmations(ctx, coinID, contractData, time.UnixMilli(int64(matchTime)), deets) } // feeRater is identical to calling w.Wallet.(asset.FeeRater). diff --git a/dex/asset.go b/dex/asset.go index f10a51e7c6..87dea6d72a 100644 --- a/dex/asset.go +++ b/dex/asset.go @@ -174,3 +174,17 @@ func IntDivUp(val, div int64) int64 { return q // return (val + div - 1) / div } + +// SwapContractDetails is critical swap data for one side of a trade. +type SwapContractDetails struct { + // From is the sending address. + From string + // To is the receiving address. + To string + // Value is the amount being traded. + Value uint64 + // SecretHash is the hash of the secret key. + SecretHash Bytes + // LockTime is the contract lock time in UNIX seconds. + LockTime uint64 +} diff --git a/dex/networks/erc20/contracts/ERC20SwapV0.sol b/dex/networks/erc20/contracts/ERC20SwapV0.sol index 7df9338bf2..5dd100fc99 100644 --- a/dex/networks/erc20/contracts/ERC20SwapV0.sol +++ b/dex/networks/erc20/contracts/ERC20SwapV0.sol @@ -6,7 +6,7 @@ pragma solidity = 0.8.15; // order to save on gas fees, a separate ERC20Swap contract is deployed // for each ERC20 token. After deployed, it keeps a map of swaps that // facilitates atomic swapping of ERC20 tokens with other crypto currencies -// that support time locks. +// that support time locks. // // It accomplishes this by holding tokens acquired during a swap initiation // until conditions are met. Prior to initiating a swap, the initiator must @@ -27,7 +27,7 @@ pragma solidity = 0.8.15; contract ERC20Swap { bytes4 private constant TRANSFER_FROM_SELECTOR = bytes4(keccak256("transferFrom(address,address,uint256)")); bytes4 private constant TRANSFER_SELECTOR = bytes4(keccak256("transfer(address,uint256)")); - + address public immutable token_address; // State is a type that hold's a contract's state. Empty is the uninitiated diff --git a/dex/networks/erc20/contracts/ERC20SwapV1.sol b/dex/networks/erc20/contracts/ERC20SwapV1.sol new file mode 100644 index 0000000000..8f4b8e69c9 --- /dev/null +++ b/dex/networks/erc20/contracts/ERC20SwapV1.sol @@ -0,0 +1,241 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +// pragma should be as specific as possible to allow easier validation. +pragma solidity = 0.8.15; + +// ETHSwap creates a contract to be deployed on an ethereum network. In +// order to save on gas fees, a separate ERC20Swap contract is deployed +// for each ERC20 token. After deployed, it keeps a map of swaps that +// facilitates atomic swapping of ERC20 tokens with other crypto currencies +// that support time locks. +// +// It accomplishes this by holding tokens acquired during a swap initiation +// until conditions are met. Prior to initiating a swap, the initiator must +// approve the ERC20Swap contract to be able to spend the initiator's tokens. +// When calling initiate, the necessary tokens for swaps are transferred to +// the swap contract. At this point the funds belong to the contract, and +// cannot be accessed by anyone else, not even the contract's deployer. The +// initiator sets a secret hash, a blocktime the funds will be accessible should +// they not be redeemed, and a participant who can redeem before or after the +// locktime. The participant can redeem at any time after the initiation +// transaction is mined if they have the secret that hashes to the secret hash. +// Otherwise, the initiator can refund funds any time after the locktime. +// +// This contract has no limits on gas used for any transactions. +// +// This contract cannot be used by other contracts or by a third party mediating +// the swap or multisig wallets. +contract ERC20Swap { + bytes4 private constant TRANSFER_FROM_SELECTOR = bytes4(keccak256("transferFrom(address,address,uint256)")); + bytes4 private constant TRANSFER_SELECTOR = bytes4(keccak256("transfer(address,uint256)")); + + address public immutable token_address; + + // State is a type that hold's a contract's state. Empty is the uninitiated + // or null value. + enum State { Empty, Filled, Redeemed, Refunded } + + struct Record { + State state; + bytes32 secret; + uint256 blockNumber; + } + + bytes32 constant RefundRecord = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; + + // swaps is a map of contract hashes to the "swap record". The swap record + // has the following interpretation. + // if (record == bytes32(0x00)): contract is uninitiated + // else if (uint256(record) < block.number && sha256(record) != contract.secretHash): + // contract is initiated and redeemable by the participant with the secret. + // else if (sha256(record) == contract.secretHash): contract has been redeemed + // else if (record == RefundRecord): contract has been refunded + // else: invalid record. Should be impossible by construction + mapping(bytes32 => bytes32) public swaps; + + // Contract is the information necessary for initialization and redemption + // or refund. The Contract itself is not stored on-chain. Instead, a key + // unique to the Contract is generated from the Contract data and keys + // the swap record. + struct Contract { + bytes32 secretHash; + address initiator; + uint64 refundTimestamp; + address participant; + uint64 value; + } + + // contractKey generates a key hash which commits to the contract data. The + // generated hash is used as a key in the swaps map. + function contractKey(Contract calldata c) public pure returns (bytes32) { + return sha256(bytes.concat(c.secretHash, bytes20(c.initiator), bytes20(c.participant), bytes8(c.value), bytes8(c.refundTimestamp))); + } + + // Redemption is the information necessary to redeem a Contract. Since we + // don't store the Contract itself, it must be provided as part of the + // redemption. + struct Redemption { + Contract c; + bytes32 secret; + } + + function secretValidates(bytes32 secret, bytes32 secretHash) public pure returns (bool) { + return sha256(bytes.concat(secret)) == secretHash; + } + + constructor(address token) { + token_address = token; + } + + // senderIsOrigin ensures that this contract cannot be used by other + // contracts, which reduces possible attack vectors. + modifier senderIsOrigin() { + require(tx.origin == msg.sender, "sender != origin"); + _; + } + + // retrieveRecord retrieves the current swap record for the contract. + function retrieveRecord(Contract calldata c) + private view returns (bytes32, bytes32, uint256) + { + bytes32 k = contractKey(c); + bytes32 record = swaps[k]; + return (k, record, uint256(record)); + } + + // state returns the current state of the swap. + function state(Contract calldata c) + public view returns(Record memory) + { + (, bytes32 record, uint256 blockNum) = retrieveRecord(c); + Record memory r; + if (blockNum == 0) { + r.state = State.Empty; + } else if (record == RefundRecord) { + r.state = State.Refunded; + } else if (secretValidates(record, c.secretHash)) { + r.state = State.Redeemed; + r.secret = record; + } else { + r.state = State.Filled; + r.blockNumber = blockNum; + } + return r; + } + + // initiate initiates an array of Contracts. + function initiate(Contract[] calldata contracts) + public + payable + senderIsOrigin() + { + uint initVal = 0; + for (uint i = 0; i < contracts.length; i++) { + Contract calldata c = contracts[i]; + + require(c.value > 0, "0 val"); + require(c.refundTimestamp > 0, "0 refundTimestamp"); + + bytes32 k = contractKey(c); + bytes32 record = swaps[k]; + require(record == bytes32(0), "swap not empty"); + + record = bytes32(block.number); + require(!secretValidates(record, c.secretHash), "hash collision"); + + swaps[k] = record; + + initVal += c.value * 1 gwei; + } + + bool success; + bytes memory data; + (success, data) = token_address.call(abi.encodeWithSelector(TRANSFER_FROM_SELECTOR, msg.sender, address(this), initVal)); + require(success && (data.length == 0 || abi.decode(data, (bool))), 'transfer from failed'); + } + + // isRedeemable returns whether or not a swap identified by secretHash + // can be redeemed using secret. + function isRedeemable(Contract calldata c) + public + view + returns (bool) + { + (, bytes32 record, uint256 blockNum) = retrieveRecord(c); + return blockNum != 0 && !secretValidates(record, c.secretHash); + } + + // redeem redeems a Contract. It checks that the sender is not a contract, + // and that the secret hash hashes to secretHash. msg.value is tranfered + // from ETHSwap to the sender. + // + // To prevent reentry attack, it is very important to check the state of the + // contract first, and change the state before proceeding to send. That way, + // the nested attacking function will throw upon trying to call redeem a + // second time. Currently, reentry is also not possible because contracts + // cannot use this contract. + function redeem(Redemption[] calldata redemptions) + public + senderIsOrigin() + { + uint amountToRedeem = 0; + for (uint i = 0; i < redemptions.length; i++) { + Redemption calldata r = redemptions[i]; + + require(r.c.participant == msg.sender, "not authed"); + + (bytes32 k, bytes32 record, uint256 blockNum) = retrieveRecord(r.c); + + // To be redeemable, the record needs to represent a valid block + // number. + require(blockNum > 0 && blockNum < block.number, "unfilled swap"); + + // Can't already be redeemed. + require(!secretValidates(record, r.c.secretHash), "already redeemed"); + + // Are they presenting the correct secret? + require(secretValidates(r.secret, r.c.secretHash), "invalid secret"); + + swaps[k] = r.secret; + amountToRedeem += r.c.value * 1 gwei; + } + + bool success; + bytes memory data; + (success, data) = token_address.call(abi.encodeWithSelector(TRANSFER_SELECTOR, msg.sender, amountToRedeem)); + require(success && (data.length == 0 || abi.decode(data, (bool))), 'transfer failed'); + } + + + // refund refunds a Contract. It checks that the sender is not a contract + // and that the refund time has passed. msg.value is transfered from the + // contract to the sender = Contract.participant. + // + // It is important to note that this also uses call.value which comes with + // no restrictions on gas used. See redeem for more info. + function refund(Contract calldata c) + public + senderIsOrigin() + { + // Is this contract even in a refundable state? + require(block.timestamp >= c.refundTimestamp, "locktime not expired"); + + // Retrieve the record. + (bytes32 k, bytes32 record, uint256 blockNum) = retrieveRecord(c); + + // Is this swap initialized? + require(blockNum > 0 && blockNum <= block.number, "swap not active"); + + // Is it already redeemed? + require(!secretValidates(record, c.secretHash), "swap already redeemed"); + + // Is it already refunded? + require(record != RefundRecord, "swap already refunded"); + + swaps[k] = RefundRecord; + + bool success; + bytes memory data; + (success, data) = token_address.call(abi.encodeWithSelector(TRANSFER_SELECTOR, msg.sender, c.value)); + require(success && (data.length == 0 || abi.decode(data, (bool))), 'transfer failed'); + } +} diff --git a/dex/networks/erc20/contracts/updatecontract.sh b/dex/networks/erc20/contracts/updatecontract.sh index cb17807233..80e11b1bbb 100755 --- a/dex/networks/erc20/contracts/updatecontract.sh +++ b/dex/networks/erc20/contracts/updatecontract.sh @@ -77,3 +77,16 @@ if [ "$VERSION" -eq "0" ]; then # Reorder the imports since we rewrote go-ethereum/event to a dcrdex package. gofmt -s -w "$CONTRACT_FILE" fi + +if [ "$VERSION" -eq "1" ]; then + perl -0pi -e 's/go-ethereum\/event"/go-ethereum\/event"\n\tethv1 "decred.org\/dcrdex\/dex\/networks\/eth\/contracts\/v1"/' $CONTRACT_FILE + + perl -0pi -e 's/\/\/ ERC20SwapContract[^}]*}\n\n//' $CONTRACT_FILE + perl -0pi -e 's/ERC20SwapContract/ethv1.ETHSwapContract/g' $CONTRACT_FILE + + perl -0pi -e 's/\/\/ ERC20SwapRedemption[^}]*}\n\n//' $CONTRACT_FILE + perl -0pi -e 's/ERC20SwapRedemption/ethv1.ETHSwapRedemption/g' $CONTRACT_FILE + + perl -0pi -e 's/\/\/ ERC20SwapRecord[^}]*}\n\n//' $CONTRACT_FILE + perl -0pi -e 's/ERC20SwapRecord/ethv1.ETHSwapRecord/g' $CONTRACT_FILE +fi diff --git a/dex/networks/erc20/contracts/v1/BinRuntimeV1.go b/dex/networks/erc20/contracts/v1/BinRuntimeV1.go new file mode 100644 index 0000000000..d21b51ca0e --- /dev/null +++ b/dex/networks/erc20/contracts/v1/BinRuntimeV1.go @@ -0,0 +1,6 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package v1 + +const ERC20SwapRuntimeBin = "6080604052600436106100865760003560e01c80637802689d116100595780637802689d146101265780638c8e8fee14610146578063d2544c0614610192578063eb84e7f2146101b2578063ed7cbed7146101ed57600080fd5b806338ec17681461008b578063428b16e1146100c157806364a97bff146100e357806377d7e031146100f6575b600080fd5b34801561009757600080fd5b506100ab6100a6366004610dea565b61020d565b6040516100b89190610e18565b60405180910390f35b3480156100cd57600080fd5b506100e16100dc366004610e5b565b6102c9565b005b6100e16100f1366004610ed0565b6105ef565b34801561010257600080fd5b50610116610111366004610f33565b610918565b60405190151581526020016100b8565b34801561013257600080fd5b50610116610141366004610dea565b610992565b34801561015257600080fd5b5061017a7f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b0390911681526020016100b8565b34801561019e57600080fd5b506100e16101ad366004610dea565b6109c5565b3480156101be57600080fd5b506101df6101cd366004610f55565b60006020819052908152604090205481565b6040519081526020016100b8565b3480156101f957600080fd5b506101df610208366004610dea565b610cc5565b60408051606081018252600080825260208201819052918101829052908061023484610dbe565b925092505061025e6040805160608101909152806000815260006020820181905260409091015290565b81600003610285578060005b9081600381111561027d5761027d610e02565b9052506102c1565b600183016102955780600361026a565b6102a0838635610918565b156102b55760028152602081018390526102c1565b60018152604081018290525b949350505050565b3233146102f15760405162461bcd60e51b81526004016102e890610f6e565b60405180910390fd5b6000805b828110156104c3573684848381811061031057610310610f98565b60c00291909101915033905061032c6080830160608401610fae565b6001600160a01b03161461036f5760405162461bcd60e51b815260206004820152600a6024820152691b9bdd08185d5d1a195960b21b60448201526064016102e8565b6000808061037c84610dbe565b92509250925060008111801561039157504381105b6103cd5760405162461bcd60e51b815260206004820152600d60248201526c0756e66696c6c6564207377617609c1b60448201526064016102e8565b6103d8828535610918565b156104185760405162461bcd60e51b815260206004820152601060248201526f185b1c9958591e481c995919595b595960821b60448201526064016102e8565b61042760a08501358535610918565b6104645760405162461bcd60e51b815260206004820152600e60248201526d1a5b9d985b1a59081cd958dc995d60921b60448201526064016102e8565b600083815260208190526040902060a0850180359091556104889060808601610fde565b61049690633b9aca0061101e565b6104aa9067ffffffffffffffff168761104e565b95505050505080806104bb90611066565b9150506102f5565b5060408051336024820152604480820184905282518083039091018152606490910182526020810180516001600160e01b031663a9059cbb60e01b17905290516000916060916001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169161053d9161107f565b6000604051808303816000865af19150503d806000811461057a576040519150601f19603f3d011682016040523d82523d6000602084013e61057f565b606091505b5090925090508180156105aa5750805115806105aa5750808060200190518101906105aa91906110ba565b6105e85760405162461bcd60e51b815260206004820152600f60248201526e1d1c985b9cd9995c8819985a5b1959608a1b60448201526064016102e8565b5050505050565b32331461060e5760405162461bcd60e51b81526004016102e890610f6e565b6000805b828110156107e8573684848381811061062d5761062d610f98565b905060a002019050600081608001602081019061064a9190610fde565b67ffffffffffffffff16116106895760405162461bcd60e51b81526020600482015260056024820152640c081d985b60da1b60448201526064016102e8565b600061069b6060830160408401610fde565b67ffffffffffffffff16116106e65760405162461bcd60e51b815260206004820152601160248201527003020726566756e6454696d657374616d7607c1b60448201526064016102e8565b60006106f182610cc5565b60008181526020819052604090205490915080156107425760405162461bcd60e51b815260206004820152600e60248201526d73776170206e6f7420656d70747960901b60448201526064016102e8565b504361074f818435610918565b1561078d5760405162461bcd60e51b815260206004820152600e60248201526d3430b9b41031b7b63634b9b4b7b760911b60448201526064016102e8565b60008281526020819052604090208190556107ae60a0840160808501610fde565b6107bc90633b9aca0061101e565b6107d09067ffffffffffffffff168661104e565b945050505080806107e090611066565b915050610612565b5060408051336024820152306044820152606480820184905282518083039091018152608490910182526020810180516001600160e01b03166323b872dd60e01b17905290516000916060916001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016916108689161107f565b6000604051808303816000865af19150503d80600081146108a5576040519150601f19603f3d011682016040523d82523d6000602084013e6108aa565b606091505b5090925090508180156108d55750805115806108d55750808060200190518101906108d591906110ba565b6105e85760405162461bcd60e51b81526020600482015260146024820152731d1c985b9cd9995c88199c9bdb4819985a5b195960621b60448201526064016102e8565b60008160028460405160200161093091815260200190565b60408051601f198184030181529082905261094a9161107f565b602060405180830381855afa158015610967573d6000803e3d6000fd5b5050506040513d601f19601f8201168201806040525081019061098a91906110dc565b149392505050565b60008060006109a084610dbe565b9250925050806000141580156102c157506109bc828535610918565b15949350505050565b3233146109e45760405162461bcd60e51b81526004016102e890610f6e565b6109f46060820160408301610fde565b67ffffffffffffffff16421015610a445760405162461bcd60e51b81526020600482015260146024820152731b1bd8dadd1a5b59481b9bdd08195e1c1a5c995960621b60448201526064016102e8565b6000806000610a5284610dbe565b925092509250600081118015610a685750438111155b610aa65760405162461bcd60e51b815260206004820152600f60248201526e73776170206e6f742061637469766560881b60448201526064016102e8565b610ab1828535610918565b15610af65760405162461bcd60e51b81526020600482015260156024820152741cddd85c08185b1c9958591e481c995919595b5959605a1b60448201526064016102e8565b60018201610b3e5760405162461bcd60e51b81526020600482015260156024820152741cddd85c08185b1c9958591e481c99599d5b991959605a1b60448201526064016102e8565b6000838152602081905260408120600019905560606001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000167fa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b33610baf60a08a0160808b01610fde565b6040516001600160a01b03909216602483015267ffffffffffffffff16604482015260640160408051601f198184030181529181526020820180516001600160e01b03166001600160e01b0319909416939093179092529051610c12919061107f565b6000604051808303816000865af19150503d8060008114610c4f576040519150601f19603f3d011682016040523d82523d6000602084013e610c54565b606091505b509092509050818015610c7f575080511580610c7f575080806020019051810190610c7f91906110ba565b610cbd5760405162461bcd60e51b815260206004820152600f60248201526e1d1c985b9cd9995c8819985a5b1959608a1b60448201526064016102e8565b505050505050565b600060028235610cdb6040850160208601610fae565b60601b846060016020810190610cf19190610fae565b60601b610d0460a0870160808801610fde565b60c01b610d176060880160408901610fde565b6040805160208101969096526bffffffffffffffffffffffff19948516908601529190921660548401526001600160c01b0319918216606884015260c01b16607082015260780160408051601f1981840301815290829052610d789161107f565b602060405180830381855afa158015610d95573d6000803e3d6000fd5b5050506040513d601f19601f82011682018060405250810190610db891906110dc565b92915050565b600080600080610dcd85610cc5565b600081815260208190526040902054909690955085945092505050565b600060a08284031215610dfc57600080fd5b50919050565b634e487b7160e01b600052602160045260246000fd5b8151606082019060048110610e3d57634e487b7160e01b600052602160045260246000fd5b80835250602083015160208301526040830151604083015292915050565b60008060208385031215610e6e57600080fd5b823567ffffffffffffffff80821115610e8657600080fd5b818501915085601f830112610e9a57600080fd5b813581811115610ea957600080fd5b86602060c083028501011115610ebe57600080fd5b60209290920196919550909350505050565b60008060208385031215610ee357600080fd5b823567ffffffffffffffff80821115610efb57600080fd5b818501915085601f830112610f0f57600080fd5b813581811115610f1e57600080fd5b86602060a083028501011115610ebe57600080fd5b60008060408385031215610f4657600080fd5b50508035926020909101359150565b600060208284031215610f6757600080fd5b5035919050565b60208082526010908201526f39b2b73232b910109e9037b934b3b4b760811b604082015260600190565b634e487b7160e01b600052603260045260246000fd5b600060208284031215610fc057600080fd5b81356001600160a01b0381168114610fd757600080fd5b9392505050565b600060208284031215610ff057600080fd5b813567ffffffffffffffff81168114610fd757600080fd5b634e487b7160e01b600052601160045260246000fd5b600067ffffffffffffffff8083168185168183048111821515161561104557611045611008565b02949350505050565b6000821982111561106157611061611008565b500190565b60006001820161107857611078611008565b5060010190565b6000825160005b818110156110a05760208186018101518583015201611086565b818111156110af576000828501525b509190910192915050565b6000602082840312156110cc57600080fd5b81518015158114610fd757600080fd5b6000602082840312156110ee57600080fd5b505191905056fea2646970667358221220b11370b262d8a8823e931b54ec9699764ab88c2b1a67a8ea6ac515fb264bfffa64736f6c634300080f0033" diff --git a/dex/networks/erc20/contracts/v1/contract.go b/dex/networks/erc20/contracts/v1/contract.go new file mode 100644 index 0000000000..3e0860e707 --- /dev/null +++ b/dex/networks/erc20/contracts/v1/contract.go @@ -0,0 +1,452 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package v1 + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" + ethv1 "decred.org/dcrdex/dex/networks/eth/contracts/v1" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription +) + +// ERC20SwapMetaData contains all meta data concerning the ERC20Swap contract. +var ERC20SwapMetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"token\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"initiator\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"refundTimestamp\",\"type\":\"uint64\"},{\"internalType\":\"address\",\"name\":\"participant\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"value\",\"type\":\"uint64\"}],\"internalType\":\"structERC20Swap.Contract\",\"name\":\"c\",\"type\":\"tuple\"}],\"name\":\"contractKey\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"initiator\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"refundTimestamp\",\"type\":\"uint64\"},{\"internalType\":\"address\",\"name\":\"participant\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"value\",\"type\":\"uint64\"}],\"internalType\":\"structERC20Swap.Contract[]\",\"name\":\"contracts\",\"type\":\"tuple[]\"}],\"name\":\"initiate\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"initiator\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"refundTimestamp\",\"type\":\"uint64\"},{\"internalType\":\"address\",\"name\":\"participant\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"value\",\"type\":\"uint64\"}],\"internalType\":\"structERC20Swap.Contract\",\"name\":\"c\",\"type\":\"tuple\"}],\"name\":\"isRedeemable\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"initiator\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"refundTimestamp\",\"type\":\"uint64\"},{\"internalType\":\"address\",\"name\":\"participant\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"value\",\"type\":\"uint64\"}],\"internalType\":\"structERC20Swap.Contract\",\"name\":\"c\",\"type\":\"tuple\"},{\"internalType\":\"bytes32\",\"name\":\"secret\",\"type\":\"bytes32\"}],\"internalType\":\"structERC20Swap.Redemption[]\",\"name\":\"redemptions\",\"type\":\"tuple[]\"}],\"name\":\"redeem\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"initiator\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"refundTimestamp\",\"type\":\"uint64\"},{\"internalType\":\"address\",\"name\":\"participant\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"value\",\"type\":\"uint64\"}],\"internalType\":\"structERC20Swap.Contract\",\"name\":\"c\",\"type\":\"tuple\"}],\"name\":\"refund\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"secret\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"}],\"name\":\"secretValidates\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"initiator\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"refundTimestamp\",\"type\":\"uint64\"},{\"internalType\":\"address\",\"name\":\"participant\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"value\",\"type\":\"uint64\"}],\"internalType\":\"structERC20Swap.Contract\",\"name\":\"c\",\"type\":\"tuple\"}],\"name\":\"state\",\"outputs\":[{\"components\":[{\"internalType\":\"enumERC20Swap.State\",\"name\":\"state\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"secret\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"}],\"internalType\":\"structERC20Swap.Record\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"name\":\"swaps\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"token_address\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]", + Bin: "0x60a060405234801561001057600080fd5b506040516111cb3803806111cb83398101604081905261002f91610040565b6001600160a01b0316608052610070565b60006020828403121561005257600080fd5b81516001600160a01b038116811461006957600080fd5b9392505050565b60805161112b6100a060003960008181610158015281816105130152818161083e0152610b5d015261112b6000f3fe6080604052600436106100865760003560e01c80637802689d116100595780637802689d146101265780638c8e8fee14610146578063d2544c0614610192578063eb84e7f2146101b2578063ed7cbed7146101ed57600080fd5b806338ec17681461008b578063428b16e1146100c157806364a97bff146100e357806377d7e031146100f6575b600080fd5b34801561009757600080fd5b506100ab6100a6366004610dea565b61020d565b6040516100b89190610e18565b60405180910390f35b3480156100cd57600080fd5b506100e16100dc366004610e5b565b6102c9565b005b6100e16100f1366004610ed0565b6105ef565b34801561010257600080fd5b50610116610111366004610f33565b610918565b60405190151581526020016100b8565b34801561013257600080fd5b50610116610141366004610dea565b610992565b34801561015257600080fd5b5061017a7f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b0390911681526020016100b8565b34801561019e57600080fd5b506100e16101ad366004610dea565b6109c5565b3480156101be57600080fd5b506101df6101cd366004610f55565b60006020819052908152604090205481565b6040519081526020016100b8565b3480156101f957600080fd5b506101df610208366004610dea565b610cc5565b60408051606081018252600080825260208201819052918101829052908061023484610dbe565b925092505061025e6040805160608101909152806000815260006020820181905260409091015290565b81600003610285578060005b9081600381111561027d5761027d610e02565b9052506102c1565b600183016102955780600361026a565b6102a0838635610918565b156102b55760028152602081018390526102c1565b60018152604081018290525b949350505050565b3233146102f15760405162461bcd60e51b81526004016102e890610f6e565b60405180910390fd5b6000805b828110156104c3573684848381811061031057610310610f98565b60c00291909101915033905061032c6080830160608401610fae565b6001600160a01b03161461036f5760405162461bcd60e51b815260206004820152600a6024820152691b9bdd08185d5d1a195960b21b60448201526064016102e8565b6000808061037c84610dbe565b92509250925060008111801561039157504381105b6103cd5760405162461bcd60e51b815260206004820152600d60248201526c0756e66696c6c6564207377617609c1b60448201526064016102e8565b6103d8828535610918565b156104185760405162461bcd60e51b815260206004820152601060248201526f185b1c9958591e481c995919595b595960821b60448201526064016102e8565b61042760a08501358535610918565b6104645760405162461bcd60e51b815260206004820152600e60248201526d1a5b9d985b1a59081cd958dc995d60921b60448201526064016102e8565b600083815260208190526040902060a0850180359091556104889060808601610fde565b61049690633b9aca0061101e565b6104aa9067ffffffffffffffff168761104e565b95505050505080806104bb90611066565b9150506102f5565b5060408051336024820152604480820184905282518083039091018152606490910182526020810180516001600160e01b031663a9059cbb60e01b17905290516000916060916001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169161053d9161107f565b6000604051808303816000865af19150503d806000811461057a576040519150601f19603f3d011682016040523d82523d6000602084013e61057f565b606091505b5090925090508180156105aa5750805115806105aa5750808060200190518101906105aa91906110ba565b6105e85760405162461bcd60e51b815260206004820152600f60248201526e1d1c985b9cd9995c8819985a5b1959608a1b60448201526064016102e8565b5050505050565b32331461060e5760405162461bcd60e51b81526004016102e890610f6e565b6000805b828110156107e8573684848381811061062d5761062d610f98565b905060a002019050600081608001602081019061064a9190610fde565b67ffffffffffffffff16116106895760405162461bcd60e51b81526020600482015260056024820152640c081d985b60da1b60448201526064016102e8565b600061069b6060830160408401610fde565b67ffffffffffffffff16116106e65760405162461bcd60e51b815260206004820152601160248201527003020726566756e6454696d657374616d7607c1b60448201526064016102e8565b60006106f182610cc5565b60008181526020819052604090205490915080156107425760405162461bcd60e51b815260206004820152600e60248201526d73776170206e6f7420656d70747960901b60448201526064016102e8565b504361074f818435610918565b1561078d5760405162461bcd60e51b815260206004820152600e60248201526d3430b9b41031b7b63634b9b4b7b760911b60448201526064016102e8565b60008281526020819052604090208190556107ae60a0840160808501610fde565b6107bc90633b9aca0061101e565b6107d09067ffffffffffffffff168661104e565b945050505080806107e090611066565b915050610612565b5060408051336024820152306044820152606480820184905282518083039091018152608490910182526020810180516001600160e01b03166323b872dd60e01b17905290516000916060916001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016916108689161107f565b6000604051808303816000865af19150503d80600081146108a5576040519150601f19603f3d011682016040523d82523d6000602084013e6108aa565b606091505b5090925090508180156108d55750805115806108d55750808060200190518101906108d591906110ba565b6105e85760405162461bcd60e51b81526020600482015260146024820152731d1c985b9cd9995c88199c9bdb4819985a5b195960621b60448201526064016102e8565b60008160028460405160200161093091815260200190565b60408051601f198184030181529082905261094a9161107f565b602060405180830381855afa158015610967573d6000803e3d6000fd5b5050506040513d601f19601f8201168201806040525081019061098a91906110dc565b149392505050565b60008060006109a084610dbe565b9250925050806000141580156102c157506109bc828535610918565b15949350505050565b3233146109e45760405162461bcd60e51b81526004016102e890610f6e565b6109f46060820160408301610fde565b67ffffffffffffffff16421015610a445760405162461bcd60e51b81526020600482015260146024820152731b1bd8dadd1a5b59481b9bdd08195e1c1a5c995960621b60448201526064016102e8565b6000806000610a5284610dbe565b925092509250600081118015610a685750438111155b610aa65760405162461bcd60e51b815260206004820152600f60248201526e73776170206e6f742061637469766560881b60448201526064016102e8565b610ab1828535610918565b15610af65760405162461bcd60e51b81526020600482015260156024820152741cddd85c08185b1c9958591e481c995919595b5959605a1b60448201526064016102e8565b60018201610b3e5760405162461bcd60e51b81526020600482015260156024820152741cddd85c08185b1c9958591e481c99599d5b991959605a1b60448201526064016102e8565b6000838152602081905260408120600019905560606001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000167fa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b33610baf60a08a0160808b01610fde565b6040516001600160a01b03909216602483015267ffffffffffffffff16604482015260640160408051601f198184030181529181526020820180516001600160e01b03166001600160e01b0319909416939093179092529051610c12919061107f565b6000604051808303816000865af19150503d8060008114610c4f576040519150601f19603f3d011682016040523d82523d6000602084013e610c54565b606091505b509092509050818015610c7f575080511580610c7f575080806020019051810190610c7f91906110ba565b610cbd5760405162461bcd60e51b815260206004820152600f60248201526e1d1c985b9cd9995c8819985a5b1959608a1b60448201526064016102e8565b505050505050565b600060028235610cdb6040850160208601610fae565b60601b846060016020810190610cf19190610fae565b60601b610d0460a0870160808801610fde565b60c01b610d176060880160408901610fde565b6040805160208101969096526bffffffffffffffffffffffff19948516908601529190921660548401526001600160c01b0319918216606884015260c01b16607082015260780160408051601f1981840301815290829052610d789161107f565b602060405180830381855afa158015610d95573d6000803e3d6000fd5b5050506040513d601f19601f82011682018060405250810190610db891906110dc565b92915050565b600080600080610dcd85610cc5565b600081815260208190526040902054909690955085945092505050565b600060a08284031215610dfc57600080fd5b50919050565b634e487b7160e01b600052602160045260246000fd5b8151606082019060048110610e3d57634e487b7160e01b600052602160045260246000fd5b80835250602083015160208301526040830151604083015292915050565b60008060208385031215610e6e57600080fd5b823567ffffffffffffffff80821115610e8657600080fd5b818501915085601f830112610e9a57600080fd5b813581811115610ea957600080fd5b86602060c083028501011115610ebe57600080fd5b60209290920196919550909350505050565b60008060208385031215610ee357600080fd5b823567ffffffffffffffff80821115610efb57600080fd5b818501915085601f830112610f0f57600080fd5b813581811115610f1e57600080fd5b86602060a083028501011115610ebe57600080fd5b60008060408385031215610f4657600080fd5b50508035926020909101359150565b600060208284031215610f6757600080fd5b5035919050565b60208082526010908201526f39b2b73232b910109e9037b934b3b4b760811b604082015260600190565b634e487b7160e01b600052603260045260246000fd5b600060208284031215610fc057600080fd5b81356001600160a01b0381168114610fd757600080fd5b9392505050565b600060208284031215610ff057600080fd5b813567ffffffffffffffff81168114610fd757600080fd5b634e487b7160e01b600052601160045260246000fd5b600067ffffffffffffffff8083168185168183048111821515161561104557611045611008565b02949350505050565b6000821982111561106157611061611008565b500190565b60006001820161107857611078611008565b5060010190565b6000825160005b818110156110a05760208186018101518583015201611086565b818111156110af576000828501525b509190910192915050565b6000602082840312156110cc57600080fd5b81518015158114610fd757600080fd5b6000602082840312156110ee57600080fd5b505191905056fea2646970667358221220b11370b262d8a8823e931b54ec9699764ab88c2b1a67a8ea6ac515fb264bfffa64736f6c634300080f0033", +} + +// ERC20SwapABI is the input ABI used to generate the binding from. +// Deprecated: Use ERC20SwapMetaData.ABI instead. +var ERC20SwapABI = ERC20SwapMetaData.ABI + +// ERC20SwapBin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use ERC20SwapMetaData.Bin instead. +var ERC20SwapBin = ERC20SwapMetaData.Bin + +// DeployERC20Swap deploys a new Ethereum contract, binding an instance of ERC20Swap to it. +func DeployERC20Swap(auth *bind.TransactOpts, backend bind.ContractBackend, token common.Address) (common.Address, *types.Transaction, *ERC20Swap, error) { + parsed, err := ERC20SwapMetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(ERC20SwapBin), backend, token) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, &ERC20Swap{ERC20SwapCaller: ERC20SwapCaller{contract: contract}, ERC20SwapTransactor: ERC20SwapTransactor{contract: contract}, ERC20SwapFilterer: ERC20SwapFilterer{contract: contract}}, nil +} + +// ERC20Swap is an auto generated Go binding around an Ethereum contract. +type ERC20Swap struct { + ERC20SwapCaller // Read-only binding to the contract + ERC20SwapTransactor // Write-only binding to the contract + ERC20SwapFilterer // Log filterer for contract events +} + +// ERC20SwapCaller is an auto generated read-only Go binding around an Ethereum contract. +type ERC20SwapCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ERC20SwapTransactor is an auto generated write-only Go binding around an Ethereum contract. +type ERC20SwapTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ERC20SwapFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type ERC20SwapFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ERC20SwapSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type ERC20SwapSession struct { + Contract *ERC20Swap // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// ERC20SwapCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type ERC20SwapCallerSession struct { + Contract *ERC20SwapCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// ERC20SwapTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type ERC20SwapTransactorSession struct { + Contract *ERC20SwapTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// ERC20SwapRaw is an auto generated low-level Go binding around an Ethereum contract. +type ERC20SwapRaw struct { + Contract *ERC20Swap // Generic contract binding to access the raw methods on +} + +// ERC20SwapCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type ERC20SwapCallerRaw struct { + Contract *ERC20SwapCaller // Generic read-only contract binding to access the raw methods on +} + +// ERC20SwapTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type ERC20SwapTransactorRaw struct { + Contract *ERC20SwapTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewERC20Swap creates a new instance of ERC20Swap, bound to a specific deployed contract. +func NewERC20Swap(address common.Address, backend bind.ContractBackend) (*ERC20Swap, error) { + contract, err := bindERC20Swap(address, backend, backend, backend) + if err != nil { + return nil, err + } + return &ERC20Swap{ERC20SwapCaller: ERC20SwapCaller{contract: contract}, ERC20SwapTransactor: ERC20SwapTransactor{contract: contract}, ERC20SwapFilterer: ERC20SwapFilterer{contract: contract}}, nil +} + +// NewERC20SwapCaller creates a new read-only instance of ERC20Swap, bound to a specific deployed contract. +func NewERC20SwapCaller(address common.Address, caller bind.ContractCaller) (*ERC20SwapCaller, error) { + contract, err := bindERC20Swap(address, caller, nil, nil) + if err != nil { + return nil, err + } + return &ERC20SwapCaller{contract: contract}, nil +} + +// NewERC20SwapTransactor creates a new write-only instance of ERC20Swap, bound to a specific deployed contract. +func NewERC20SwapTransactor(address common.Address, transactor bind.ContractTransactor) (*ERC20SwapTransactor, error) { + contract, err := bindERC20Swap(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return &ERC20SwapTransactor{contract: contract}, nil +} + +// NewERC20SwapFilterer creates a new log filterer instance of ERC20Swap, bound to a specific deployed contract. +func NewERC20SwapFilterer(address common.Address, filterer bind.ContractFilterer) (*ERC20SwapFilterer, error) { + contract, err := bindERC20Swap(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return &ERC20SwapFilterer{contract: contract}, nil +} + +// bindERC20Swap binds a generic wrapper to an already deployed contract. +func bindERC20Swap(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := abi.JSON(strings.NewReader(ERC20SwapABI)) + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_ERC20Swap *ERC20SwapRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _ERC20Swap.Contract.ERC20SwapCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_ERC20Swap *ERC20SwapRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _ERC20Swap.Contract.ERC20SwapTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_ERC20Swap *ERC20SwapRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _ERC20Swap.Contract.ERC20SwapTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_ERC20Swap *ERC20SwapCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _ERC20Swap.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_ERC20Swap *ERC20SwapTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _ERC20Swap.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_ERC20Swap *ERC20SwapTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _ERC20Swap.Contract.contract.Transact(opts, method, params...) +} + +// ContractKey is a free data retrieval call binding the contract method 0xed7cbed7. +// +// Solidity: function contractKey((bytes32,address,uint64,address,uint64) c) pure returns(bytes32) +func (_ERC20Swap *ERC20SwapCaller) ContractKey(opts *bind.CallOpts, c ethv1.ETHSwapContract) ([32]byte, error) { + var out []interface{} + err := _ERC20Swap.contract.Call(opts, &out, "contractKey", c) + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// ContractKey is a free data retrieval call binding the contract method 0xed7cbed7. +// +// Solidity: function contractKey((bytes32,address,uint64,address,uint64) c) pure returns(bytes32) +func (_ERC20Swap *ERC20SwapSession) ContractKey(c ethv1.ETHSwapContract) ([32]byte, error) { + return _ERC20Swap.Contract.ContractKey(&_ERC20Swap.CallOpts, c) +} + +// ContractKey is a free data retrieval call binding the contract method 0xed7cbed7. +// +// Solidity: function contractKey((bytes32,address,uint64,address,uint64) c) pure returns(bytes32) +func (_ERC20Swap *ERC20SwapCallerSession) ContractKey(c ethv1.ETHSwapContract) ([32]byte, error) { + return _ERC20Swap.Contract.ContractKey(&_ERC20Swap.CallOpts, c) +} + +// IsRedeemable is a free data retrieval call binding the contract method 0x7802689d. +// +// Solidity: function isRedeemable((bytes32,address,uint64,address,uint64) c) view returns(bool) +func (_ERC20Swap *ERC20SwapCaller) IsRedeemable(opts *bind.CallOpts, c ethv1.ETHSwapContract) (bool, error) { + var out []interface{} + err := _ERC20Swap.contract.Call(opts, &out, "isRedeemable", c) + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// IsRedeemable is a free data retrieval call binding the contract method 0x7802689d. +// +// Solidity: function isRedeemable((bytes32,address,uint64,address,uint64) c) view returns(bool) +func (_ERC20Swap *ERC20SwapSession) IsRedeemable(c ethv1.ETHSwapContract) (bool, error) { + return _ERC20Swap.Contract.IsRedeemable(&_ERC20Swap.CallOpts, c) +} + +// IsRedeemable is a free data retrieval call binding the contract method 0x7802689d. +// +// Solidity: function isRedeemable((bytes32,address,uint64,address,uint64) c) view returns(bool) +func (_ERC20Swap *ERC20SwapCallerSession) IsRedeemable(c ethv1.ETHSwapContract) (bool, error) { + return _ERC20Swap.Contract.IsRedeemable(&_ERC20Swap.CallOpts, c) +} + +// SecretValidates is a free data retrieval call binding the contract method 0x77d7e031. +// +// Solidity: function secretValidates(bytes32 secret, bytes32 secretHash) pure returns(bool) +func (_ERC20Swap *ERC20SwapCaller) SecretValidates(opts *bind.CallOpts, secret [32]byte, secretHash [32]byte) (bool, error) { + var out []interface{} + err := _ERC20Swap.contract.Call(opts, &out, "secretValidates", secret, secretHash) + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// SecretValidates is a free data retrieval call binding the contract method 0x77d7e031. +// +// Solidity: function secretValidates(bytes32 secret, bytes32 secretHash) pure returns(bool) +func (_ERC20Swap *ERC20SwapSession) SecretValidates(secret [32]byte, secretHash [32]byte) (bool, error) { + return _ERC20Swap.Contract.SecretValidates(&_ERC20Swap.CallOpts, secret, secretHash) +} + +// SecretValidates is a free data retrieval call binding the contract method 0x77d7e031. +// +// Solidity: function secretValidates(bytes32 secret, bytes32 secretHash) pure returns(bool) +func (_ERC20Swap *ERC20SwapCallerSession) SecretValidates(secret [32]byte, secretHash [32]byte) (bool, error) { + return _ERC20Swap.Contract.SecretValidates(&_ERC20Swap.CallOpts, secret, secretHash) +} + +// State is a free data retrieval call binding the contract method 0x38ec1768. +// +// Solidity: function state((bytes32,address,uint64,address,uint64) c) view returns((uint8,bytes32,uint256)) +func (_ERC20Swap *ERC20SwapCaller) State(opts *bind.CallOpts, c ethv1.ETHSwapContract) (ethv1.ETHSwapRecord, error) { + var out []interface{} + err := _ERC20Swap.contract.Call(opts, &out, "state", c) + + if err != nil { + return *new(ethv1.ETHSwapRecord), err + } + + out0 := *abi.ConvertType(out[0], new(ethv1.ETHSwapRecord)).(*ethv1.ETHSwapRecord) + + return out0, err + +} + +// State is a free data retrieval call binding the contract method 0x38ec1768. +// +// Solidity: function state((bytes32,address,uint64,address,uint64) c) view returns((uint8,bytes32,uint256)) +func (_ERC20Swap *ERC20SwapSession) State(c ethv1.ETHSwapContract) (ethv1.ETHSwapRecord, error) { + return _ERC20Swap.Contract.State(&_ERC20Swap.CallOpts, c) +} + +// State is a free data retrieval call binding the contract method 0x38ec1768. +// +// Solidity: function state((bytes32,address,uint64,address,uint64) c) view returns((uint8,bytes32,uint256)) +func (_ERC20Swap *ERC20SwapCallerSession) State(c ethv1.ETHSwapContract) (ethv1.ETHSwapRecord, error) { + return _ERC20Swap.Contract.State(&_ERC20Swap.CallOpts, c) +} + +// Swaps is a free data retrieval call binding the contract method 0xeb84e7f2. +// +// Solidity: function swaps(bytes32 ) view returns(bytes32) +func (_ERC20Swap *ERC20SwapCaller) Swaps(opts *bind.CallOpts, arg0 [32]byte) ([32]byte, error) { + var out []interface{} + err := _ERC20Swap.contract.Call(opts, &out, "swaps", arg0) + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// Swaps is a free data retrieval call binding the contract method 0xeb84e7f2. +// +// Solidity: function swaps(bytes32 ) view returns(bytes32) +func (_ERC20Swap *ERC20SwapSession) Swaps(arg0 [32]byte) ([32]byte, error) { + return _ERC20Swap.Contract.Swaps(&_ERC20Swap.CallOpts, arg0) +} + +// Swaps is a free data retrieval call binding the contract method 0xeb84e7f2. +// +// Solidity: function swaps(bytes32 ) view returns(bytes32) +func (_ERC20Swap *ERC20SwapCallerSession) Swaps(arg0 [32]byte) ([32]byte, error) { + return _ERC20Swap.Contract.Swaps(&_ERC20Swap.CallOpts, arg0) +} + +// TokenAddress is a free data retrieval call binding the contract method 0x8c8e8fee. +// +// Solidity: function token_address() view returns(address) +func (_ERC20Swap *ERC20SwapCaller) TokenAddress(opts *bind.CallOpts) (common.Address, error) { + var out []interface{} + err := _ERC20Swap.contract.Call(opts, &out, "token_address") + + if err != nil { + return *new(common.Address), err + } + + out0 := *abi.ConvertType(out[0], new(common.Address)).(*common.Address) + + return out0, err + +} + +// TokenAddress is a free data retrieval call binding the contract method 0x8c8e8fee. +// +// Solidity: function token_address() view returns(address) +func (_ERC20Swap *ERC20SwapSession) TokenAddress() (common.Address, error) { + return _ERC20Swap.Contract.TokenAddress(&_ERC20Swap.CallOpts) +} + +// TokenAddress is a free data retrieval call binding the contract method 0x8c8e8fee. +// +// Solidity: function token_address() view returns(address) +func (_ERC20Swap *ERC20SwapCallerSession) TokenAddress() (common.Address, error) { + return _ERC20Swap.Contract.TokenAddress(&_ERC20Swap.CallOpts) +} + +// Initiate is a paid mutator transaction binding the contract method 0x64a97bff. +// +// Solidity: function initiate((bytes32,address,uint64,address,uint64)[] contracts) payable returns() +func (_ERC20Swap *ERC20SwapTransactor) Initiate(opts *bind.TransactOpts, contracts []ethv1.ETHSwapContract) (*types.Transaction, error) { + return _ERC20Swap.contract.Transact(opts, "initiate", contracts) +} + +// Initiate is a paid mutator transaction binding the contract method 0x64a97bff. +// +// Solidity: function initiate((bytes32,address,uint64,address,uint64)[] contracts) payable returns() +func (_ERC20Swap *ERC20SwapSession) Initiate(contracts []ethv1.ETHSwapContract) (*types.Transaction, error) { + return _ERC20Swap.Contract.Initiate(&_ERC20Swap.TransactOpts, contracts) +} + +// Initiate is a paid mutator transaction binding the contract method 0x64a97bff. +// +// Solidity: function initiate((bytes32,address,uint64,address,uint64)[] contracts) payable returns() +func (_ERC20Swap *ERC20SwapTransactorSession) Initiate(contracts []ethv1.ETHSwapContract) (*types.Transaction, error) { + return _ERC20Swap.Contract.Initiate(&_ERC20Swap.TransactOpts, contracts) +} + +// Redeem is a paid mutator transaction binding the contract method 0x428b16e1. +// +// Solidity: function redeem(((bytes32,address,uint64,address,uint64),bytes32)[] redemptions) returns() +func (_ERC20Swap *ERC20SwapTransactor) Redeem(opts *bind.TransactOpts, redemptions []ethv1.ETHSwapRedemption) (*types.Transaction, error) { + return _ERC20Swap.contract.Transact(opts, "redeem", redemptions) +} + +// Redeem is a paid mutator transaction binding the contract method 0x428b16e1. +// +// Solidity: function redeem(((bytes32,address,uint64,address,uint64),bytes32)[] redemptions) returns() +func (_ERC20Swap *ERC20SwapSession) Redeem(redemptions []ethv1.ETHSwapRedemption) (*types.Transaction, error) { + return _ERC20Swap.Contract.Redeem(&_ERC20Swap.TransactOpts, redemptions) +} + +// Redeem is a paid mutator transaction binding the contract method 0x428b16e1. +// +// Solidity: function redeem(((bytes32,address,uint64,address,uint64),bytes32)[] redemptions) returns() +func (_ERC20Swap *ERC20SwapTransactorSession) Redeem(redemptions []ethv1.ETHSwapRedemption) (*types.Transaction, error) { + return _ERC20Swap.Contract.Redeem(&_ERC20Swap.TransactOpts, redemptions) +} + +// Refund is a paid mutator transaction binding the contract method 0xd2544c06. +// +// Solidity: function refund((bytes32,address,uint64,address,uint64) c) returns() +func (_ERC20Swap *ERC20SwapTransactor) Refund(opts *bind.TransactOpts, c ethv1.ETHSwapContract) (*types.Transaction, error) { + return _ERC20Swap.contract.Transact(opts, "refund", c) +} + +// Refund is a paid mutator transaction binding the contract method 0xd2544c06. +// +// Solidity: function refund((bytes32,address,uint64,address,uint64) c) returns() +func (_ERC20Swap *ERC20SwapSession) Refund(c ethv1.ETHSwapContract) (*types.Transaction, error) { + return _ERC20Swap.Contract.Refund(&_ERC20Swap.TransactOpts, c) +} + +// Refund is a paid mutator transaction binding the contract method 0xd2544c06. +// +// Solidity: function refund((bytes32,address,uint64,address,uint64) c) returns() +func (_ERC20Swap *ERC20SwapTransactorSession) Refund(c ethv1.ETHSwapContract) (*types.Transaction, error) { + return _ERC20Swap.Contract.Refund(&_ERC20Swap.TransactOpts, c) +} diff --git a/dex/networks/eth/contracts/ETHSwapV1.sol b/dex/networks/eth/contracts/ETHSwapV1.sol new file mode 100644 index 0000000000..05039fcf68 --- /dev/null +++ b/dex/networks/eth/contracts/ETHSwapV1.sol @@ -0,0 +1,226 @@ +// SPDX-License-Identifier: BlueOak-1.0.0 +// pragma should be as specific as possible to allow easier validation. +pragma solidity = 0.8.15; + +// ETHSwap creates a contract to be deployed on an ethereum network. After +// deployed, it keeps a record of the state of a contract and enables +// redemption and refund of the contract when conditions are met. +// +// ETHSwap accomplishes this by holding funds sent to ETHSwap until certain +// conditions are met. An initiator sends a tx with the Contract(s) to fund and +// the requisite value to transfer to ETHSwap. At +// this point the funds belong to the contract, and cannot be accessed by +// anyone else, not even the contract's deployer. The swap Contract specifies +// the conditions necessary for refund and redeem. +// +// ETHSwap has no limits on gas used for any transactions. +// +// ETHSwap cannot be used by other contracts or by a third party mediating +// the swap or multisig wallets. +// +// This code should be verifiable as resulting in a certain on-chain contract +// by compiling with the correct version of solidity and comparing the +// resulting byte code to the data in the original transaction. +contract ETHSwap { + // State is a type that hold's a contract's state. Empty is the uninitiated + // or null value. + enum State { Empty, Filled, Redeemed, Refunded } + + struct Record { + State state; + bytes32 secret; + uint256 blockNumber; + } + + bytes32 constant RefundRecord = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; + + // swaps is a map of contract hashes to the "swap record". The swap record + // has the following interpretation. + // if (record == bytes32(0x00)): contract is uninitiated + // else if (uint256(record) < block.number && sha256(record) != contract.secretHash): + // contract is initiated and redeemable by the participant with the secret. + // else if (sha256(record) == contract.secretHash): contract has been redeemed + // else if (record == RefundRecord): contract has been refunded + // else: invalid record. Should be impossible by construction + mapping(bytes32 => bytes32) public swaps; + + // Contract is the information necessary for initialization and redemption + // or refund. The Contract itself is not stored on-chain. Instead, a key + // unique to the Contract is generated from the Contract data and keys + // the swap record. + struct Contract { + bytes32 secretHash; + address initiator; + uint64 refundTimestamp; + address participant; + uint64 value; + } + + // contractKey generates a key hash which commits to the contract data. The + // generated hash is used as a key in the swaps map. + function contractKey(Contract calldata c) public pure returns (bytes32) { + return sha256(bytes.concat(c.secretHash, bytes20(c.initiator), bytes20(c.participant), bytes8(c.value), bytes8(c.refundTimestamp))); + } + + // Redemption is the information necessary to redeem a Contract. Since we + // don't store the Contract itself, it must be provided as part of the + // redemption. + struct Redemption { + Contract c; + bytes32 secret; + } + + function secretValidates(bytes32 secret, bytes32 secretHash) public pure returns (bool) { + return sha256(bytes.concat(secret)) == secretHash; + } + + // constructor is empty. This contract has no connection to the original + // sender after deployed. It can only be interacted with by users + // initiating, redeeming, and refunding swaps. + constructor() {} + + // senderIsOrigin ensures that this contract cannot be used by other + // contracts, which reduces possible attack vectors. + modifier senderIsOrigin() { + require(tx.origin == msg.sender, "sender != origin"); + _; + } + + // retrieveRecord retrieves the current swap record for the contract. + function retrieveRecord(Contract calldata c) + private view returns (bytes32, bytes32, uint256) + { + bytes32 k = contractKey(c); + bytes32 record = swaps[k]; + return (k, record, uint256(record)); + } + + // state returns the current state of the swap. + function state(Contract calldata c) + public view returns(Record memory) + { + (, bytes32 record, uint256 blockNum) = retrieveRecord(c); + Record memory r; + if (blockNum == 0) { + r.state = State.Empty; + } else if (record == RefundRecord) { + r.state = State.Refunded; + } else if (secretValidates(record, c.secretHash)) { + r.state = State.Redeemed; + r.secret = record; + } else { + r.state = State.Filled; + r.blockNumber = blockNum; + } + return r; + } + + // initiate initiates an array of Contracts. + function initiate(Contract[] calldata contracts) + public + payable + senderIsOrigin() + { + uint initVal = 0; + for (uint i = 0; i < contracts.length; i++) { + Contract calldata c = contracts[i]; + + require(c.value > 0, "0 val"); + require(c.refundTimestamp > 0, "0 refundTimestamp"); + + bytes32 k = contractKey(c); + bytes32 record = swaps[k]; + require(record == bytes32(0), "swap not empty"); + + record = bytes32(block.number); + require(!secretValidates(record, c.secretHash), "hash collision"); + + swaps[k] = record; + + initVal += c.value * 1 gwei; + } + + require(initVal == msg.value, "bad val"); + } + + // isRedeemable returns whether or not a swap identified by secretHash + // can be redeemed using secret. + function isRedeemable(Contract calldata c) + public + view + returns (bool) + { + (, bytes32 record, uint256 blockNum) = retrieveRecord(c); + return blockNum != 0 && !secretValidates(record, c.secretHash); + } + + // redeem redeems a Contract. It checks that the sender is not a contract, + // and that the secret hash hashes to secretHash. msg.value is tranfered + // from ETHSwap to the sender. + // + // To prevent reentry attack, it is very important to check the state of the + // contract first, and change the state before proceeding to send. That way, + // the nested attacking function will throw upon trying to call redeem a + // second time. Currently, reentry is also not possible because contracts + // cannot use this contract. + function redeem(Redemption[] calldata redemptions) + public + senderIsOrigin() + { + uint amountToRedeem = 0; + for (uint i = 0; i < redemptions.length; i++) { + Redemption calldata r = redemptions[i]; + + require(r.c.participant == msg.sender, "not authed"); + + (bytes32 k, bytes32 record, uint256 blockNum) = retrieveRecord(r.c); + + // To be redeemable, the record needs to represent a valid block + // number. + require(blockNum > 0 && blockNum < block.number, "unfilled swap"); + + // Can't already be redeemed. + require(!secretValidates(record, r.c.secretHash), "already redeemed"); + + // Are they presenting the correct secret? + require(secretValidates(r.secret, r.c.secretHash), "invalid secret"); + + swaps[k] = r.secret; + amountToRedeem += r.c.value * 1 gwei; + } + + (bool ok, ) = payable(msg.sender).call{value: amountToRedeem}(""); + require(ok == true, "transfer failed"); + } + + // refund refunds a Contract. It checks that the sender is not a contract + // and that the refund time has passed. msg.value is transfered from the + // contract to the sender = Contract.participant. + // + // It is important to note that this also uses call.value which comes with + // no restrictions on gas used. See redeem for more info. + function refund(Contract calldata c) + public + senderIsOrigin() + { + // Is this contract even in a refundable state? + require(block.timestamp >= c.refundTimestamp, "locktime not expired"); + + // Retrieve the record. + (bytes32 k, bytes32 record, uint256 blockNum) = retrieveRecord(c); + + // Is this swap initialized? + require(blockNum > 0 && blockNum <= block.number, "swap not active"); + + // Is it already redeemed? + require(!secretValidates(record, c.secretHash), "swap already redeemed"); + + // Is it already refunded? + require(record != RefundRecord, "swap already refunded"); + + swaps[k] = RefundRecord; + + (bool ok, ) = payable(c.initiator).call{value: c.value * 1 gwei}(""); + require(ok == true, "transfer failed"); + } +} diff --git a/dex/networks/eth/contracts/updatecontract.sh b/dex/networks/eth/contracts/updatecontract.sh index 5397986ddb..8f1214a459 100755 --- a/dex/networks/eth/contracts/updatecontract.sh +++ b/dex/networks/eth/contracts/updatecontract.sh @@ -23,6 +23,7 @@ then fi mkdir temp +mkdir -p ${PKG_NAME} solc --abi --bin --bin-runtime --overwrite --optimize ${SOLIDITY_FILE} -o ./temp/ BYTECODE=$(<./temp/${CONTRACT_NAME}.bin-runtime) diff --git a/dex/networks/eth/contracts/v1/BinRuntimeV1.go b/dex/networks/eth/contracts/v1/BinRuntimeV1.go new file mode 100644 index 0000000000..60cf7a60c6 --- /dev/null +++ b/dex/networks/eth/contracts/v1/BinRuntimeV1.go @@ -0,0 +1,6 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package v1 + +const ETHSwapRuntimeBin = "60806040526004361061007b5760003560e01c80637802689d1161004e5780637802689d1461011b578063d2544c061461013b578063eb84e7f21461015b578063ed7cbed71461019657600080fd5b806338ec176814610080578063428b16e1146100b657806364a97bff146100d857806377d7e031146100eb575b600080fd5b34801561008c57600080fd5b506100a061009b366004610b6f565b6101b6565b6040516100ad9190610b9d565b60405180910390f35b3480156100c257600080fd5b506100d66100d1366004610be0565b610272565b005b6100d66100e6366004610c55565b610503565b3480156100f757600080fd5b5061010b610106366004610cb8565b61073b565b60405190151581526020016100ad565b34801561012757600080fd5b5061010b610136366004610b6f565b6107b5565b34801561014757600080fd5b506100d6610156366004610b6f565b6107e8565b34801561016757600080fd5b50610188610176366004610cda565b60006020819052908152604090205481565b6040519081526020016100ad565b3480156101a257600080fd5b506101886101b1366004610b6f565b610a4a565b6040805160608101825260008082526020820181905291810182905290806101dd84610b43565b92509250506102076040805160608101909152806000815260006020820181905260409091015290565b8160000361022e578060005b9081600381111561022657610226610b87565b90525061026a565b6001830161023e57806003610213565b61024983863561073b565b1561025e57600281526020810183905261026a565b60018152604081018290525b949350505050565b32331461029a5760405162461bcd60e51b815260040161029190610cf3565b60405180910390fd5b6000805b8281101561046c57368484838181106102b9576102b9610d1d565b60c0029190910191503390506102d56080830160608401610d33565b6001600160a01b0316146103185760405162461bcd60e51b815260206004820152600a6024820152691b9bdd08185d5d1a195960b21b6044820152606401610291565b6000808061032584610b43565b92509250925060008111801561033a57504381105b6103765760405162461bcd60e51b815260206004820152600d60248201526c0756e66696c6c6564207377617609c1b6044820152606401610291565b61038182853561073b565b156103c15760405162461bcd60e51b815260206004820152601060248201526f185b1c9958591e481c995919595b595960821b6044820152606401610291565b6103d060a0850135853561073b565b61040d5760405162461bcd60e51b815260206004820152600e60248201526d1a5b9d985b1a59081cd958dc995d60921b6044820152606401610291565b600083815260208190526040902060a0850180359091556104319060808601610d63565b61043f90633b9aca00610da3565b6104539067ffffffffffffffff1687610dd3565b955050505050808061046490610deb565b91505061029e565b50604051600090339083908381818185875af1925050503d80600081146104af576040519150601f19603f3d011682016040523d82523d6000602084013e6104b4565b606091505b50909150506001811515146104fd5760405162461bcd60e51b815260206004820152600f60248201526e1d1c985b9cd9995c8819985a5b1959608a1b6044820152606401610291565b50505050565b3233146105225760405162461bcd60e51b815260040161029190610cf3565b6000805b828110156106fc573684848381811061054157610541610d1d565b905060a002019050600081608001602081019061055e9190610d63565b67ffffffffffffffff161161059d5760405162461bcd60e51b81526020600482015260056024820152640c081d985b60da1b6044820152606401610291565b60006105af6060830160408401610d63565b67ffffffffffffffff16116105fa5760405162461bcd60e51b815260206004820152601160248201527003020726566756e6454696d657374616d7607c1b6044820152606401610291565b600061060582610a4a565b60008181526020819052604090205490915080156106565760405162461bcd60e51b815260206004820152600e60248201526d73776170206e6f7420656d70747960901b6044820152606401610291565b504361066381843561073b565b156106a15760405162461bcd60e51b815260206004820152600e60248201526d3430b9b41031b7b63634b9b4b7b760911b6044820152606401610291565b60008281526020819052604090208190556106c260a0840160808501610d63565b6106d090633b9aca00610da3565b6106e49067ffffffffffffffff1686610dd3565b945050505080806106f490610deb565b915050610526565b503481146107365760405162461bcd60e51b8152602060048201526007602482015266189859081d985b60ca1b6044820152606401610291565b505050565b60008160028460405160200161075391815260200190565b60408051601f198184030181529082905261076d91610e04565b602060405180830381855afa15801561078a573d6000803e3d6000fd5b5050506040513d601f19601f820116820180604052508101906107ad9190610e3f565b149392505050565b60008060006107c384610b43565b92509250508060001415801561026a57506107df82853561073b565b15949350505050565b3233146108075760405162461bcd60e51b815260040161029190610cf3565b6108176060820160408301610d63565b67ffffffffffffffff164210156108675760405162461bcd60e51b81526020600482015260146024820152731b1bd8dadd1a5b59481b9bdd08195e1c1a5c995960621b6044820152606401610291565b600080600061087584610b43565b92509250925060008111801561088b5750438111155b6108c95760405162461bcd60e51b815260206004820152600f60248201526e73776170206e6f742061637469766560881b6044820152606401610291565b6108d482853561073b565b156109195760405162461bcd60e51b81526020600482015260156024820152741cddd85c08185b1c9958591e481c995919595b5959605a1b6044820152606401610291565b600182016109615760405162461bcd60e51b81526020600482015260156024820152741cddd85c08185b1c9958591e481c99599d5b991959605a1b6044820152606401610291565b600083815260208181526040808320600019905561098491908701908701610d33565b6001600160a01b031661099d60a0870160808801610d63565b6109ab90633b9aca00610da3565b67ffffffffffffffff1660405160006040518083038185875af1925050503d80600081146109f5576040519150601f19603f3d011682016040523d82523d6000602084013e6109fa565b606091505b5090915050600181151514610a435760405162461bcd60e51b815260206004820152600f60248201526e1d1c985b9cd9995c8819985a5b1959608a1b6044820152606401610291565b5050505050565b600060028235610a606040850160208601610d33565b60601b846060016020810190610a769190610d33565b60601b610a8960a0870160808801610d63565b60c01b610a9c6060880160408901610d63565b6040805160208101969096526bffffffffffffffffffffffff19948516908601529190921660548401526001600160c01b0319918216606884015260c01b16607082015260780160408051601f1981840301815290829052610afd91610e04565b602060405180830381855afa158015610b1a573d6000803e3d6000fd5b5050506040513d601f19601f82011682018060405250810190610b3d9190610e3f565b92915050565b600080600080610b5285610a4a565b600081815260208190526040902054909690955085945092505050565b600060a08284031215610b8157600080fd5b50919050565b634e487b7160e01b600052602160045260246000fd5b8151606082019060048110610bc257634e487b7160e01b600052602160045260246000fd5b80835250602083015160208301526040830151604083015292915050565b60008060208385031215610bf357600080fd5b823567ffffffffffffffff80821115610c0b57600080fd5b818501915085601f830112610c1f57600080fd5b813581811115610c2e57600080fd5b86602060c083028501011115610c4357600080fd5b60209290920196919550909350505050565b60008060208385031215610c6857600080fd5b823567ffffffffffffffff80821115610c8057600080fd5b818501915085601f830112610c9457600080fd5b813581811115610ca357600080fd5b86602060a083028501011115610c4357600080fd5b60008060408385031215610ccb57600080fd5b50508035926020909101359150565b600060208284031215610cec57600080fd5b5035919050565b60208082526010908201526f39b2b73232b910109e9037b934b3b4b760811b604082015260600190565b634e487b7160e01b600052603260045260246000fd5b600060208284031215610d4557600080fd5b81356001600160a01b0381168114610d5c57600080fd5b9392505050565b600060208284031215610d7557600080fd5b813567ffffffffffffffff81168114610d5c57600080fd5b634e487b7160e01b600052601160045260246000fd5b600067ffffffffffffffff80831681851681830481118215151615610dca57610dca610d8d565b02949350505050565b60008219821115610de657610de6610d8d565b500190565b600060018201610dfd57610dfd610d8d565b5060010190565b6000825160005b81811015610e255760208186018101518583015201610e0b565b81811115610e34576000828501525b509190910192915050565b600060208284031215610e5157600080fd5b505191905056fea2646970667358221220ff4aa3b731ad448a2ec9f367bc9ddb0be755eebc0f8e3159b55e2113545da62764736f6c634300080f0033" diff --git a/dex/networks/eth/contracts/v1/contract.go b/dex/networks/eth/contracts/v1/contract.go new file mode 100644 index 0000000000..e742fa49b3 --- /dev/null +++ b/dex/networks/eth/contracts/v1/contract.go @@ -0,0 +1,442 @@ +// Code generated - DO NOT EDIT. +// This file is a generated binding and any manual changes will be lost. + +package v1 + +import ( + "errors" + "math/big" + "strings" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Reference imports to suppress errors if they are not otherwise used. +var ( + _ = errors.New + _ = big.NewInt + _ = strings.NewReader + _ = ethereum.NotFound + _ = bind.Bind + _ = common.Big1 + _ = types.BloomLookup + _ = event.NewSubscription +) + +// ETHSwapContract is an auto generated low-level Go binding around an user-defined struct. +type ETHSwapContract struct { + SecretHash [32]byte + Initiator common.Address + RefundTimestamp uint64 + Participant common.Address + Value uint64 +} + +// ETHSwapRecord is an auto generated low-level Go binding around an user-defined struct. +type ETHSwapRecord struct { + State uint8 + Secret [32]byte + BlockNumber *big.Int +} + +// ETHSwapRedemption is an auto generated low-level Go binding around an user-defined struct. +type ETHSwapRedemption struct { + C ETHSwapContract + Secret [32]byte +} + +// ETHSwapMetaData contains all meta data concerning the ETHSwap contract. +var ETHSwapMetaData = &bind.MetaData{ + ABI: "[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"initiator\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"refundTimestamp\",\"type\":\"uint64\"},{\"internalType\":\"address\",\"name\":\"participant\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"value\",\"type\":\"uint64\"}],\"internalType\":\"structETHSwap.Contract\",\"name\":\"c\",\"type\":\"tuple\"}],\"name\":\"contractKey\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"initiator\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"refundTimestamp\",\"type\":\"uint64\"},{\"internalType\":\"address\",\"name\":\"participant\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"value\",\"type\":\"uint64\"}],\"internalType\":\"structETHSwap.Contract[]\",\"name\":\"contracts\",\"type\":\"tuple[]\"}],\"name\":\"initiate\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"initiator\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"refundTimestamp\",\"type\":\"uint64\"},{\"internalType\":\"address\",\"name\":\"participant\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"value\",\"type\":\"uint64\"}],\"internalType\":\"structETHSwap.Contract\",\"name\":\"c\",\"type\":\"tuple\"}],\"name\":\"isRedeemable\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"initiator\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"refundTimestamp\",\"type\":\"uint64\"},{\"internalType\":\"address\",\"name\":\"participant\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"value\",\"type\":\"uint64\"}],\"internalType\":\"structETHSwap.Contract\",\"name\":\"c\",\"type\":\"tuple\"},{\"internalType\":\"bytes32\",\"name\":\"secret\",\"type\":\"bytes32\"}],\"internalType\":\"structETHSwap.Redemption[]\",\"name\":\"redemptions\",\"type\":\"tuple[]\"}],\"name\":\"redeem\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"initiator\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"refundTimestamp\",\"type\":\"uint64\"},{\"internalType\":\"address\",\"name\":\"participant\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"value\",\"type\":\"uint64\"}],\"internalType\":\"structETHSwap.Contract\",\"name\":\"c\",\"type\":\"tuple\"}],\"name\":\"refund\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"secret\",\"type\":\"bytes32\"},{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"}],\"name\":\"secretValidates\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"pure\",\"type\":\"function\"},{\"inputs\":[{\"components\":[{\"internalType\":\"bytes32\",\"name\":\"secretHash\",\"type\":\"bytes32\"},{\"internalType\":\"address\",\"name\":\"initiator\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"refundTimestamp\",\"type\":\"uint64\"},{\"internalType\":\"address\",\"name\":\"participant\",\"type\":\"address\"},{\"internalType\":\"uint64\",\"name\":\"value\",\"type\":\"uint64\"}],\"internalType\":\"structETHSwap.Contract\",\"name\":\"c\",\"type\":\"tuple\"}],\"name\":\"state\",\"outputs\":[{\"components\":[{\"internalType\":\"enumETHSwap.State\",\"name\":\"state\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"secret\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"blockNumber\",\"type\":\"uint256\"}],\"internalType\":\"structETHSwap.Record\",\"name\":\"\",\"type\":\"tuple\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"name\":\"swaps\",\"outputs\":[{\"internalType\":\"bytes32\",\"name\":\"\",\"type\":\"bytes32\"}],\"stateMutability\":\"view\",\"type\":\"function\"}]", + Bin: "0x608060405234801561001057600080fd5b50610e8e806100206000396000f3fe60806040526004361061007b5760003560e01c80637802689d1161004e5780637802689d1461011b578063d2544c061461013b578063eb84e7f21461015b578063ed7cbed71461019657600080fd5b806338ec176814610080578063428b16e1146100b657806364a97bff146100d857806377d7e031146100eb575b600080fd5b34801561008c57600080fd5b506100a061009b366004610b6f565b6101b6565b6040516100ad9190610b9d565b60405180910390f35b3480156100c257600080fd5b506100d66100d1366004610be0565b610272565b005b6100d66100e6366004610c55565b610503565b3480156100f757600080fd5b5061010b610106366004610cb8565b61073b565b60405190151581526020016100ad565b34801561012757600080fd5b5061010b610136366004610b6f565b6107b5565b34801561014757600080fd5b506100d6610156366004610b6f565b6107e8565b34801561016757600080fd5b50610188610176366004610cda565b60006020819052908152604090205481565b6040519081526020016100ad565b3480156101a257600080fd5b506101886101b1366004610b6f565b610a4a565b6040805160608101825260008082526020820181905291810182905290806101dd84610b43565b92509250506102076040805160608101909152806000815260006020820181905260409091015290565b8160000361022e578060005b9081600381111561022657610226610b87565b90525061026a565b6001830161023e57806003610213565b61024983863561073b565b1561025e57600281526020810183905261026a565b60018152604081018290525b949350505050565b32331461029a5760405162461bcd60e51b815260040161029190610cf3565b60405180910390fd5b6000805b8281101561046c57368484838181106102b9576102b9610d1d565b60c0029190910191503390506102d56080830160608401610d33565b6001600160a01b0316146103185760405162461bcd60e51b815260206004820152600a6024820152691b9bdd08185d5d1a195960b21b6044820152606401610291565b6000808061032584610b43565b92509250925060008111801561033a57504381105b6103765760405162461bcd60e51b815260206004820152600d60248201526c0756e66696c6c6564207377617609c1b6044820152606401610291565b61038182853561073b565b156103c15760405162461bcd60e51b815260206004820152601060248201526f185b1c9958591e481c995919595b595960821b6044820152606401610291565b6103d060a0850135853561073b565b61040d5760405162461bcd60e51b815260206004820152600e60248201526d1a5b9d985b1a59081cd958dc995d60921b6044820152606401610291565b600083815260208190526040902060a0850180359091556104319060808601610d63565b61043f90633b9aca00610da3565b6104539067ffffffffffffffff1687610dd3565b955050505050808061046490610deb565b91505061029e565b50604051600090339083908381818185875af1925050503d80600081146104af576040519150601f19603f3d011682016040523d82523d6000602084013e6104b4565b606091505b50909150506001811515146104fd5760405162461bcd60e51b815260206004820152600f60248201526e1d1c985b9cd9995c8819985a5b1959608a1b6044820152606401610291565b50505050565b3233146105225760405162461bcd60e51b815260040161029190610cf3565b6000805b828110156106fc573684848381811061054157610541610d1d565b905060a002019050600081608001602081019061055e9190610d63565b67ffffffffffffffff161161059d5760405162461bcd60e51b81526020600482015260056024820152640c081d985b60da1b6044820152606401610291565b60006105af6060830160408401610d63565b67ffffffffffffffff16116105fa5760405162461bcd60e51b815260206004820152601160248201527003020726566756e6454696d657374616d7607c1b6044820152606401610291565b600061060582610a4a565b60008181526020819052604090205490915080156106565760405162461bcd60e51b815260206004820152600e60248201526d73776170206e6f7420656d70747960901b6044820152606401610291565b504361066381843561073b565b156106a15760405162461bcd60e51b815260206004820152600e60248201526d3430b9b41031b7b63634b9b4b7b760911b6044820152606401610291565b60008281526020819052604090208190556106c260a0840160808501610d63565b6106d090633b9aca00610da3565b6106e49067ffffffffffffffff1686610dd3565b945050505080806106f490610deb565b915050610526565b503481146107365760405162461bcd60e51b8152602060048201526007602482015266189859081d985b60ca1b6044820152606401610291565b505050565b60008160028460405160200161075391815260200190565b60408051601f198184030181529082905261076d91610e04565b602060405180830381855afa15801561078a573d6000803e3d6000fd5b5050506040513d601f19601f820116820180604052508101906107ad9190610e3f565b149392505050565b60008060006107c384610b43565b92509250508060001415801561026a57506107df82853561073b565b15949350505050565b3233146108075760405162461bcd60e51b815260040161029190610cf3565b6108176060820160408301610d63565b67ffffffffffffffff164210156108675760405162461bcd60e51b81526020600482015260146024820152731b1bd8dadd1a5b59481b9bdd08195e1c1a5c995960621b6044820152606401610291565b600080600061087584610b43565b92509250925060008111801561088b5750438111155b6108c95760405162461bcd60e51b815260206004820152600f60248201526e73776170206e6f742061637469766560881b6044820152606401610291565b6108d482853561073b565b156109195760405162461bcd60e51b81526020600482015260156024820152741cddd85c08185b1c9958591e481c995919595b5959605a1b6044820152606401610291565b600182016109615760405162461bcd60e51b81526020600482015260156024820152741cddd85c08185b1c9958591e481c99599d5b991959605a1b6044820152606401610291565b600083815260208181526040808320600019905561098491908701908701610d33565b6001600160a01b031661099d60a0870160808801610d63565b6109ab90633b9aca00610da3565b67ffffffffffffffff1660405160006040518083038185875af1925050503d80600081146109f5576040519150601f19603f3d011682016040523d82523d6000602084013e6109fa565b606091505b5090915050600181151514610a435760405162461bcd60e51b815260206004820152600f60248201526e1d1c985b9cd9995c8819985a5b1959608a1b6044820152606401610291565b5050505050565b600060028235610a606040850160208601610d33565b60601b846060016020810190610a769190610d33565b60601b610a8960a0870160808801610d63565b60c01b610a9c6060880160408901610d63565b6040805160208101969096526bffffffffffffffffffffffff19948516908601529190921660548401526001600160c01b0319918216606884015260c01b16607082015260780160408051601f1981840301815290829052610afd91610e04565b602060405180830381855afa158015610b1a573d6000803e3d6000fd5b5050506040513d601f19601f82011682018060405250810190610b3d9190610e3f565b92915050565b600080600080610b5285610a4a565b600081815260208190526040902054909690955085945092505050565b600060a08284031215610b8157600080fd5b50919050565b634e487b7160e01b600052602160045260246000fd5b8151606082019060048110610bc257634e487b7160e01b600052602160045260246000fd5b80835250602083015160208301526040830151604083015292915050565b60008060208385031215610bf357600080fd5b823567ffffffffffffffff80821115610c0b57600080fd5b818501915085601f830112610c1f57600080fd5b813581811115610c2e57600080fd5b86602060c083028501011115610c4357600080fd5b60209290920196919550909350505050565b60008060208385031215610c6857600080fd5b823567ffffffffffffffff80821115610c8057600080fd5b818501915085601f830112610c9457600080fd5b813581811115610ca357600080fd5b86602060a083028501011115610c4357600080fd5b60008060408385031215610ccb57600080fd5b50508035926020909101359150565b600060208284031215610cec57600080fd5b5035919050565b60208082526010908201526f39b2b73232b910109e9037b934b3b4b760811b604082015260600190565b634e487b7160e01b600052603260045260246000fd5b600060208284031215610d4557600080fd5b81356001600160a01b0381168114610d5c57600080fd5b9392505050565b600060208284031215610d7557600080fd5b813567ffffffffffffffff81168114610d5c57600080fd5b634e487b7160e01b600052601160045260246000fd5b600067ffffffffffffffff80831681851681830481118215151615610dca57610dca610d8d565b02949350505050565b60008219821115610de657610de6610d8d565b500190565b600060018201610dfd57610dfd610d8d565b5060010190565b6000825160005b81811015610e255760208186018101518583015201610e0b565b81811115610e34576000828501525b509190910192915050565b600060208284031215610e5157600080fd5b505191905056fea2646970667358221220ff4aa3b731ad448a2ec9f367bc9ddb0be755eebc0f8e3159b55e2113545da62764736f6c634300080f0033", +} + +// ETHSwapABI is the input ABI used to generate the binding from. +// Deprecated: Use ETHSwapMetaData.ABI instead. +var ETHSwapABI = ETHSwapMetaData.ABI + +// ETHSwapBin is the compiled bytecode used for deploying new contracts. +// Deprecated: Use ETHSwapMetaData.Bin instead. +var ETHSwapBin = ETHSwapMetaData.Bin + +// DeployETHSwap deploys a new Ethereum contract, binding an instance of ETHSwap to it. +func DeployETHSwap(auth *bind.TransactOpts, backend bind.ContractBackend) (common.Address, *types.Transaction, *ETHSwap, error) { + parsed, err := ETHSwapMetaData.GetAbi() + if err != nil { + return common.Address{}, nil, nil, err + } + if parsed == nil { + return common.Address{}, nil, nil, errors.New("GetABI returned nil") + } + + address, tx, contract, err := bind.DeployContract(auth, *parsed, common.FromHex(ETHSwapBin), backend) + if err != nil { + return common.Address{}, nil, nil, err + } + return address, tx, ÐSwap{ETHSwapCaller: ETHSwapCaller{contract: contract}, ETHSwapTransactor: ETHSwapTransactor{contract: contract}, ETHSwapFilterer: ETHSwapFilterer{contract: contract}}, nil +} + +// ETHSwap is an auto generated Go binding around an Ethereum contract. +type ETHSwap struct { + ETHSwapCaller // Read-only binding to the contract + ETHSwapTransactor // Write-only binding to the contract + ETHSwapFilterer // Log filterer for contract events +} + +// ETHSwapCaller is an auto generated read-only Go binding around an Ethereum contract. +type ETHSwapCaller struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ETHSwapTransactor is an auto generated write-only Go binding around an Ethereum contract. +type ETHSwapTransactor struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ETHSwapFilterer is an auto generated log filtering Go binding around an Ethereum contract events. +type ETHSwapFilterer struct { + contract *bind.BoundContract // Generic contract wrapper for the low level calls +} + +// ETHSwapSession is an auto generated Go binding around an Ethereum contract, +// with pre-set call and transact options. +type ETHSwapSession struct { + Contract *ETHSwap // Generic contract binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// ETHSwapCallerSession is an auto generated read-only Go binding around an Ethereum contract, +// with pre-set call options. +type ETHSwapCallerSession struct { + Contract *ETHSwapCaller // Generic contract caller binding to set the session for + CallOpts bind.CallOpts // Call options to use throughout this session +} + +// ETHSwapTransactorSession is an auto generated write-only Go binding around an Ethereum contract, +// with pre-set transact options. +type ETHSwapTransactorSession struct { + Contract *ETHSwapTransactor // Generic contract transactor binding to set the session for + TransactOpts bind.TransactOpts // Transaction auth options to use throughout this session +} + +// ETHSwapRaw is an auto generated low-level Go binding around an Ethereum contract. +type ETHSwapRaw struct { + Contract *ETHSwap // Generic contract binding to access the raw methods on +} + +// ETHSwapCallerRaw is an auto generated low-level read-only Go binding around an Ethereum contract. +type ETHSwapCallerRaw struct { + Contract *ETHSwapCaller // Generic read-only contract binding to access the raw methods on +} + +// ETHSwapTransactorRaw is an auto generated low-level write-only Go binding around an Ethereum contract. +type ETHSwapTransactorRaw struct { + Contract *ETHSwapTransactor // Generic write-only contract binding to access the raw methods on +} + +// NewETHSwap creates a new instance of ETHSwap, bound to a specific deployed contract. +func NewETHSwap(address common.Address, backend bind.ContractBackend) (*ETHSwap, error) { + contract, err := bindETHSwap(address, backend, backend, backend) + if err != nil { + return nil, err + } + return ÐSwap{ETHSwapCaller: ETHSwapCaller{contract: contract}, ETHSwapTransactor: ETHSwapTransactor{contract: contract}, ETHSwapFilterer: ETHSwapFilterer{contract: contract}}, nil +} + +// NewETHSwapCaller creates a new read-only instance of ETHSwap, bound to a specific deployed contract. +func NewETHSwapCaller(address common.Address, caller bind.ContractCaller) (*ETHSwapCaller, error) { + contract, err := bindETHSwap(address, caller, nil, nil) + if err != nil { + return nil, err + } + return ÐSwapCaller{contract: contract}, nil +} + +// NewETHSwapTransactor creates a new write-only instance of ETHSwap, bound to a specific deployed contract. +func NewETHSwapTransactor(address common.Address, transactor bind.ContractTransactor) (*ETHSwapTransactor, error) { + contract, err := bindETHSwap(address, nil, transactor, nil) + if err != nil { + return nil, err + } + return ÐSwapTransactor{contract: contract}, nil +} + +// NewETHSwapFilterer creates a new log filterer instance of ETHSwap, bound to a specific deployed contract. +func NewETHSwapFilterer(address common.Address, filterer bind.ContractFilterer) (*ETHSwapFilterer, error) { + contract, err := bindETHSwap(address, nil, nil, filterer) + if err != nil { + return nil, err + } + return ÐSwapFilterer{contract: contract}, nil +} + +// bindETHSwap binds a generic wrapper to an already deployed contract. +func bindETHSwap(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor, filterer bind.ContractFilterer) (*bind.BoundContract, error) { + parsed, err := abi.JSON(strings.NewReader(ETHSwapABI)) + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, parsed, caller, transactor, filterer), nil +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_ETHSwap *ETHSwapRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _ETHSwap.Contract.ETHSwapCaller.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_ETHSwap *ETHSwapRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _ETHSwap.Contract.ETHSwapTransactor.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_ETHSwap *ETHSwapRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _ETHSwap.Contract.ETHSwapTransactor.contract.Transact(opts, method, params...) +} + +// Call invokes the (constant) contract method with params as input values and +// sets the output to result. The result type might be a single field for simple +// returns, a slice of interfaces for anonymous returns and a struct for named +// returns. +func (_ETHSwap *ETHSwapCallerRaw) Call(opts *bind.CallOpts, result *[]interface{}, method string, params ...interface{}) error { + return _ETHSwap.Contract.contract.Call(opts, result, method, params...) +} + +// Transfer initiates a plain transaction to move funds to the contract, calling +// its default method if one is available. +func (_ETHSwap *ETHSwapTransactorRaw) Transfer(opts *bind.TransactOpts) (*types.Transaction, error) { + return _ETHSwap.Contract.contract.Transfer(opts) +} + +// Transact invokes the (paid) contract method with params as input values. +func (_ETHSwap *ETHSwapTransactorRaw) Transact(opts *bind.TransactOpts, method string, params ...interface{}) (*types.Transaction, error) { + return _ETHSwap.Contract.contract.Transact(opts, method, params...) +} + +// ContractKey is a free data retrieval call binding the contract method 0xed7cbed7. +// +// Solidity: function contractKey((bytes32,address,uint64,address,uint64) c) pure returns(bytes32) +func (_ETHSwap *ETHSwapCaller) ContractKey(opts *bind.CallOpts, c ETHSwapContract) ([32]byte, error) { + var out []interface{} + err := _ETHSwap.contract.Call(opts, &out, "contractKey", c) + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// ContractKey is a free data retrieval call binding the contract method 0xed7cbed7. +// +// Solidity: function contractKey((bytes32,address,uint64,address,uint64) c) pure returns(bytes32) +func (_ETHSwap *ETHSwapSession) ContractKey(c ETHSwapContract) ([32]byte, error) { + return _ETHSwap.Contract.ContractKey(&_ETHSwap.CallOpts, c) +} + +// ContractKey is a free data retrieval call binding the contract method 0xed7cbed7. +// +// Solidity: function contractKey((bytes32,address,uint64,address,uint64) c) pure returns(bytes32) +func (_ETHSwap *ETHSwapCallerSession) ContractKey(c ETHSwapContract) ([32]byte, error) { + return _ETHSwap.Contract.ContractKey(&_ETHSwap.CallOpts, c) +} + +// IsRedeemable is a free data retrieval call binding the contract method 0x7802689d. +// +// Solidity: function isRedeemable((bytes32,address,uint64,address,uint64) c) view returns(bool) +func (_ETHSwap *ETHSwapCaller) IsRedeemable(opts *bind.CallOpts, c ETHSwapContract) (bool, error) { + var out []interface{} + err := _ETHSwap.contract.Call(opts, &out, "isRedeemable", c) + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// IsRedeemable is a free data retrieval call binding the contract method 0x7802689d. +// +// Solidity: function isRedeemable((bytes32,address,uint64,address,uint64) c) view returns(bool) +func (_ETHSwap *ETHSwapSession) IsRedeemable(c ETHSwapContract) (bool, error) { + return _ETHSwap.Contract.IsRedeemable(&_ETHSwap.CallOpts, c) +} + +// IsRedeemable is a free data retrieval call binding the contract method 0x7802689d. +// +// Solidity: function isRedeemable((bytes32,address,uint64,address,uint64) c) view returns(bool) +func (_ETHSwap *ETHSwapCallerSession) IsRedeemable(c ETHSwapContract) (bool, error) { + return _ETHSwap.Contract.IsRedeemable(&_ETHSwap.CallOpts, c) +} + +// SecretValidates is a free data retrieval call binding the contract method 0x77d7e031. +// +// Solidity: function secretValidates(bytes32 secret, bytes32 secretHash) pure returns(bool) +func (_ETHSwap *ETHSwapCaller) SecretValidates(opts *bind.CallOpts, secret [32]byte, secretHash [32]byte) (bool, error) { + var out []interface{} + err := _ETHSwap.contract.Call(opts, &out, "secretValidates", secret, secretHash) + + if err != nil { + return *new(bool), err + } + + out0 := *abi.ConvertType(out[0], new(bool)).(*bool) + + return out0, err + +} + +// SecretValidates is a free data retrieval call binding the contract method 0x77d7e031. +// +// Solidity: function secretValidates(bytes32 secret, bytes32 secretHash) pure returns(bool) +func (_ETHSwap *ETHSwapSession) SecretValidates(secret [32]byte, secretHash [32]byte) (bool, error) { + return _ETHSwap.Contract.SecretValidates(&_ETHSwap.CallOpts, secret, secretHash) +} + +// SecretValidates is a free data retrieval call binding the contract method 0x77d7e031. +// +// Solidity: function secretValidates(bytes32 secret, bytes32 secretHash) pure returns(bool) +func (_ETHSwap *ETHSwapCallerSession) SecretValidates(secret [32]byte, secretHash [32]byte) (bool, error) { + return _ETHSwap.Contract.SecretValidates(&_ETHSwap.CallOpts, secret, secretHash) +} + +// State is a free data retrieval call binding the contract method 0x38ec1768. +// +// Solidity: function state((bytes32,address,uint64,address,uint64) c) view returns((uint8,bytes32,uint256)) +func (_ETHSwap *ETHSwapCaller) State(opts *bind.CallOpts, c ETHSwapContract) (ETHSwapRecord, error) { + var out []interface{} + err := _ETHSwap.contract.Call(opts, &out, "state", c) + + if err != nil { + return *new(ETHSwapRecord), err + } + + out0 := *abi.ConvertType(out[0], new(ETHSwapRecord)).(*ETHSwapRecord) + + return out0, err + +} + +// State is a free data retrieval call binding the contract method 0x38ec1768. +// +// Solidity: function state((bytes32,address,uint64,address,uint64) c) view returns((uint8,bytes32,uint256)) +func (_ETHSwap *ETHSwapSession) State(c ETHSwapContract) (ETHSwapRecord, error) { + return _ETHSwap.Contract.State(&_ETHSwap.CallOpts, c) +} + +// State is a free data retrieval call binding the contract method 0x38ec1768. +// +// Solidity: function state((bytes32,address,uint64,address,uint64) c) view returns((uint8,bytes32,uint256)) +func (_ETHSwap *ETHSwapCallerSession) State(c ETHSwapContract) (ETHSwapRecord, error) { + return _ETHSwap.Contract.State(&_ETHSwap.CallOpts, c) +} + +// Swaps is a free data retrieval call binding the contract method 0xeb84e7f2. +// +// Solidity: function swaps(bytes32 ) view returns(bytes32) +func (_ETHSwap *ETHSwapCaller) Swaps(opts *bind.CallOpts, arg0 [32]byte) ([32]byte, error) { + var out []interface{} + err := _ETHSwap.contract.Call(opts, &out, "swaps", arg0) + + if err != nil { + return *new([32]byte), err + } + + out0 := *abi.ConvertType(out[0], new([32]byte)).(*[32]byte) + + return out0, err + +} + +// Swaps is a free data retrieval call binding the contract method 0xeb84e7f2. +// +// Solidity: function swaps(bytes32 ) view returns(bytes32) +func (_ETHSwap *ETHSwapSession) Swaps(arg0 [32]byte) ([32]byte, error) { + return _ETHSwap.Contract.Swaps(&_ETHSwap.CallOpts, arg0) +} + +// Swaps is a free data retrieval call binding the contract method 0xeb84e7f2. +// +// Solidity: function swaps(bytes32 ) view returns(bytes32) +func (_ETHSwap *ETHSwapCallerSession) Swaps(arg0 [32]byte) ([32]byte, error) { + return _ETHSwap.Contract.Swaps(&_ETHSwap.CallOpts, arg0) +} + +// Initiate is a paid mutator transaction binding the contract method 0x64a97bff. +// +// Solidity: function initiate((bytes32,address,uint64,address,uint64)[] contracts) payable returns() +func (_ETHSwap *ETHSwapTransactor) Initiate(opts *bind.TransactOpts, contracts []ETHSwapContract) (*types.Transaction, error) { + return _ETHSwap.contract.Transact(opts, "initiate", contracts) +} + +// Initiate is a paid mutator transaction binding the contract method 0x64a97bff. +// +// Solidity: function initiate((bytes32,address,uint64,address,uint64)[] contracts) payable returns() +func (_ETHSwap *ETHSwapSession) Initiate(contracts []ETHSwapContract) (*types.Transaction, error) { + return _ETHSwap.Contract.Initiate(&_ETHSwap.TransactOpts, contracts) +} + +// Initiate is a paid mutator transaction binding the contract method 0x64a97bff. +// +// Solidity: function initiate((bytes32,address,uint64,address,uint64)[] contracts) payable returns() +func (_ETHSwap *ETHSwapTransactorSession) Initiate(contracts []ETHSwapContract) (*types.Transaction, error) { + return _ETHSwap.Contract.Initiate(&_ETHSwap.TransactOpts, contracts) +} + +// Redeem is a paid mutator transaction binding the contract method 0x428b16e1. +// +// Solidity: function redeem(((bytes32,address,uint64,address,uint64),bytes32)[] redemptions) returns() +func (_ETHSwap *ETHSwapTransactor) Redeem(opts *bind.TransactOpts, redemptions []ETHSwapRedemption) (*types.Transaction, error) { + return _ETHSwap.contract.Transact(opts, "redeem", redemptions) +} + +// Redeem is a paid mutator transaction binding the contract method 0x428b16e1. +// +// Solidity: function redeem(((bytes32,address,uint64,address,uint64),bytes32)[] redemptions) returns() +func (_ETHSwap *ETHSwapSession) Redeem(redemptions []ETHSwapRedemption) (*types.Transaction, error) { + return _ETHSwap.Contract.Redeem(&_ETHSwap.TransactOpts, redemptions) +} + +// Redeem is a paid mutator transaction binding the contract method 0x428b16e1. +// +// Solidity: function redeem(((bytes32,address,uint64,address,uint64),bytes32)[] redemptions) returns() +func (_ETHSwap *ETHSwapTransactorSession) Redeem(redemptions []ETHSwapRedemption) (*types.Transaction, error) { + return _ETHSwap.Contract.Redeem(&_ETHSwap.TransactOpts, redemptions) +} + +// Refund is a paid mutator transaction binding the contract method 0xd2544c06. +// +// Solidity: function refund((bytes32,address,uint64,address,uint64) c) returns() +func (_ETHSwap *ETHSwapTransactor) Refund(opts *bind.TransactOpts, c ETHSwapContract) (*types.Transaction, error) { + return _ETHSwap.contract.Transact(opts, "refund", c) +} + +// Refund is a paid mutator transaction binding the contract method 0xd2544c06. +// +// Solidity: function refund((bytes32,address,uint64,address,uint64) c) returns() +func (_ETHSwap *ETHSwapSession) Refund(c ETHSwapContract) (*types.Transaction, error) { + return _ETHSwap.Contract.Refund(&_ETHSwap.TransactOpts, c) +} + +// Refund is a paid mutator transaction binding the contract method 0xd2544c06. +// +// Solidity: function refund((bytes32,address,uint64,address,uint64) c) returns() +func (_ETHSwap *ETHSwapTransactorSession) Refund(c ETHSwapContract) (*types.Transaction, error) { + return _ETHSwap.Contract.Refund(&_ETHSwap.TransactOpts, c) +} diff --git a/dex/networks/eth/params.go b/dex/networks/eth/params.go index 0248eb6e1b..781c1156ec 100644 --- a/dex/networks/eth/params.go +++ b/dex/networks/eth/params.go @@ -17,6 +17,7 @@ import ( "decred.org/dcrdex/dex" v0 "decred.org/dcrdex/dex/networks/eth/contracts/v0" + swapv1 "decred.org/dcrdex/dex/networks/eth/contracts/v1" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" ) @@ -42,23 +43,37 @@ var ( VersionedGases = map[uint32]*Gases{ 0: v0Gases, + 1: v1Gases, } ContractAddresses = map[uint32]map[dex.Network]common.Address{ 0: { dex.Mainnet: common.Address{}, - dex.Simnet: common.HexToAddress("0x2f68e723b8989ba1c6a9f03e42f33cb7dc9d606f"), dex.Testnet: common.HexToAddress("0xa483b6166dA8Da6748B29Af35f96C4F9388c456C"), + dex.Simnet: common.Address{}, + }, + 1: { + dex.Mainnet: common.Address{}, + dex.Testnet: common.Address{}, + dex.Simnet: common.Address{}, }, } ) var v0Gases = &Gases{ - Swap: 135000, - SwapAdd: 113000, - Redeem: 63000, - RedeemAdd: 32000, - Refund: 43000, + Swap: 135_000, + SwapAdd: 113_000, + Redeem: 63_000, + RedeemAdd: 32_000, + Refund: 43_000, +} + +var v1Gases = &Gases{ + Swap: 52_000, // [48072 74276 100477 126682 152874] + SwapAdd: 30_000, + Redeem: 50_000, // [39832 50894 61956 73016 84067] + RedeemAdd: 14_000, + Refund: 45_000, // [37961 37937 37961 37961] } // LoadGenesisFile loads a Genesis config from a json file. @@ -77,7 +92,7 @@ func LoadGenesisFile(genesisFile string) (*core.Genesis, error) { return &genesis, nil } -// EncodeContractData packs the contract version and the secret hash into a byte +// EncodeContractData packs the server version and the secret hash into a byte // slice for communicating a swap's identity. func EncodeContractData(contractVersion uint32, swapKey [SecretHashSize]byte) []byte { b := make([]byte, SecretHashSize+4) @@ -86,7 +101,7 @@ func EncodeContractData(contractVersion uint32, swapKey [SecretHashSize]byte) [] return b } -// DecodeContractData unpacks the contract version and secret hash. +// DecodeContractData unpacks the server version and secret hash. func DecodeContractData(data []byte) (contractVersion uint32, swapKey [SecretHashSize]byte, err error) { if len(data) != SecretHashSize+4 { err = errors.New("invalid swap data") @@ -228,8 +243,10 @@ func SwapStateFromV0(state *v0.ETHSwapSwap) *SwapState { type Initiation struct { LockTime time.Time SecretHash [32]byte + Initiator common.Address // v0 only Participant common.Address Value *big.Int + ValueGwei uint64 } // Redemption is the data used to redeem a swap. @@ -279,3 +296,15 @@ func (g *Gases) RedeemN(n int) uint64 { } return g.Redeem + g.RedeemAdd*(uint64(n)-1) } + +func SwapToV1(c *dex.SwapContractDetails) swapv1.ETHSwapContract { + var secretHash [32]byte + copy(secretHash[:], c.SecretHash) + return swapv1.ETHSwapContract{ + SecretHash: secretHash, + Initiator: common.HexToAddress(c.From), + RefundTimestamp: c.LockTime, + Participant: common.HexToAddress(c.To), + Value: c.Value, + } +} diff --git a/dex/networks/eth/params_test.go b/dex/networks/eth/params_test.go index c40e142c6f..1a905bb684 100644 --- a/dex/networks/eth/params_test.go +++ b/dex/networks/eth/params_test.go @@ -71,7 +71,7 @@ func TestVersionedGases(t *testing.T) { expRefundGas: v0Gases.Refund, }, { - ver: 1, + ver: 2, expInitGases: []uint64{0, math.MaxUint64}, expRedeemGases: []uint64{0, math.MaxUint64}, expRefundGas: math.MaxUint64, diff --git a/dex/networks/eth/tokens.go b/dex/networks/eth/tokens.go index a4e52c28cf..934cd89ec1 100644 --- a/dex/networks/eth/tokens.go +++ b/dex/networks/eth/tokens.go @@ -118,7 +118,28 @@ var Tokens = map[uint32]*Token{ // first approvals on the same contract, so it's not // just the global first. Approve: 46_000, // [44465 27365 27365 27365 27365] - Transfer: 27_000, // [24964 24964 24964 24964 24964] + Transfer: 28_000, // [24964 24964 24964 24964 24964] + }, + }, + 1: { + // Swap contract address. The simnet harness writes this + // address to file. Live tests must populate this field. + Address: common.Address{}, + Gas: Gases{ + Swap: 95_000, // [86009 112920 139831 166742 193651] + SwapAdd: 30_000, // avg SwapAdd 26910 + Redeem: 50_000, // [42569 53614 64646 75703 86734] + RedeemAdd: 14_000, // avg RedeemAdd 11038 + Refund: 50_000, // [45306 45306 45306 45306 45294] avg: 45303 + // Approve is the gas used to call the approve + // method of the contract. For Approve transactions, + // the very first approval for an account-spender + // pair takes more than subsequent approvals. The + // results are repeated for a different account's + // first approvals on the same contract, so it's not + // just the global first. + Approve: 46_000, + Transfer: 28_000, }, }, }, @@ -165,14 +186,18 @@ func MaybeReadSimnetAddrs() { return } - ethSwapContractAddrFile := filepath.Join(ethPath, "eth_swap_contract_address.txt") - tokenSwapContractAddrFile := filepath.Join(ethPath, "erc20_swap_contract_address.txt") + ethSwapContractAddrFileV0 := filepath.Join(ethPath, "eth_swap_contract_address.txt") + tokenSwapContractAddrFileV0 := filepath.Join(ethPath, "erc20_swap_contract_address.txt") + ethSwapContractAddrFileV1 := filepath.Join(ethPath, "eth_swap_contract_address_v1.txt") + tokenSwapContractAddrFileV1 := filepath.Join(ethPath, "erc20_swap_contract_address_v1.txt") testTokenContractAddrFile := filepath.Join(ethPath, "test_token_contract_address.txt") - ContractAddresses[0][dex.Simnet] = getContractAddrFromFile(ethSwapContractAddrFile) + ContractAddresses[0][dex.Simnet] = getContractAddrFromFile(ethSwapContractAddrFileV0) + ContractAddresses[1][dex.Simnet] = getContractAddrFromFile(ethSwapContractAddrFileV1) token := Tokens[testTokenID].NetTokens[dex.Simnet] - token.SwapContracts[0].Address = getContractAddrFromFile(tokenSwapContractAddrFile) + token.SwapContracts[0].Address = getContractAddrFromFile(tokenSwapContractAddrFileV0) + token.SwapContracts[1].Address = getContractAddrFromFile(tokenSwapContractAddrFileV1) token.Address = getContractAddrFromFile(testTokenContractAddrFile) } diff --git a/dex/networks/eth/txdata.go b/dex/networks/eth/txdata.go index 0855ddb996..247f5a9bac 100644 --- a/dex/networks/eth/txdata.go +++ b/dex/networks/eth/txdata.go @@ -11,48 +11,18 @@ import ( "strings" "time" + "decred.org/dcrdex/dex" 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" "github.com/ethereum/go-ethereum/common" ) -// ParseInitiateData parses the calldata used to call the initiate function of a -// specific version of the swap contract. It returns the the list of initiations -// done in the call and errors if the call data does not call initiate initiate -// with expected argument types. -func ParseInitiateData(calldata []byte, contractVersion uint32) (map[[SecretHashSize]byte]*Initiation, error) { - txDataHandler, ok := txDataHandlers[contractVersion] - if !ok { - return nil, fmt.Errorf("contract version %v does not exist", contractVersion) - } - - return txDataHandler.parseInitiateData(calldata) -} - -// ParseRedeemData parses the calldata used to call the redeem function of a -// specific version of the swap contract. It returns the the list of redemptions -// done in the call and errors if the call data does not call redeem with expected -// argument types. -func ParseRedeemData(calldata []byte, contractVersion uint32) (map[[SecretHashSize]byte]*Redemption, error) { - txDataHandler, ok := txDataHandlers[contractVersion] - if !ok { - return nil, fmt.Errorf("contract version %v does not exist", contractVersion) - } - - return txDataHandler.parseRedeemData(calldata) -} - -// ParseRefundData parses the calldata used to call the refund function of a -// specific version of the swap contract. It returns the secret hash and errors -// if the call data does not call refund with expected argument types. -func ParseRefundData(calldata []byte, contractVersion uint32) ([32]byte, error) { - txDataHandler, ok := txDataHandlers[contractVersion] - if !ok { - return [32]byte{}, fmt.Errorf("contract version %v does not exist", contractVersion) - } - - return txDataHandler.parseRefundData(calldata) -} +const ( + initiateFuncName = "initiate" + redeemFuncName = "redeem" + refundFuncName = "refund" +) // ABIs maps each swap contract's version to that version's parsed ABI. var ABIs = initAbis() @@ -60,45 +30,31 @@ var ABIs = initAbis() func initAbis() map[uint32]*abi.ABI { v0ABI, err := abi.JSON(strings.NewReader(swapv0.ETHSwapABI)) if err != nil { - panic(fmt.Sprintf("failed to parse abi: %v", err)) + panic(fmt.Sprintf("failed to parse v0 abi: %v", err)) } - return map[uint32]*abi.ABI{ - 0: &v0ABI, + v1ABI, err := abi.JSON(strings.NewReader(swapv1.ETHSwapABI)) + if err != nil { + panic(fmt.Sprintf("failed to parse v1 abi: %v", err)) } -} - -type txDataHandler interface { - parseInitiateData([]byte) (map[[SecretHashSize]byte]*Initiation, error) - parseRedeemData([]byte) (map[[SecretHashSize]byte]*Redemption, error) - parseRefundData([]byte) ([32]byte, error) -} - -var txDataHandlers = map[uint32]txDataHandler{ - 0: newTxDataV0(), -} -type txDataHandlerV0 struct { - initiateFuncName string - redeemFuncName string - refundFuncName string -} - -func newTxDataV0() *txDataHandlerV0 { - return &txDataHandlerV0{ - initiateFuncName: "initiate", - redeemFuncName: "redeem", - refundFuncName: "refund", + return map[uint32]*abi.ABI{ + 0: &v0ABI, + 1: &v1ABI, } } -func (t *txDataHandlerV0) parseInitiateData(calldata []byte) (map[[SecretHashSize]byte]*Initiation, error) { +// ParseInitiateData parses the calldata used to call the initiate function of a +// specific version of the swap contract. It returns the the list of initiations +// done in the call and errors if the call data does not call initiate initiate +// with expected argument types. +func ParseInitiateDataV0(calldata []byte) (map[[SecretHashSize]byte]*Initiation, error) { decoded, err := ParseCallData(calldata, ABIs[0]) if err != nil { return nil, fmt.Errorf("unable to parse call data: %v", err) } - if decoded.Name != t.initiateFuncName { - return nil, fmt.Errorf("expected %v function but got %v", t.initiateFuncName, decoded.Name) + if decoded.Name != initiateFuncName { + return nil, fmt.Errorf("expected %v function but got %v", initiateFuncName, decoded.Name) } args := decoded.inputs // Any difference in number of args and types than what we expect @@ -132,19 +88,24 @@ func (t *txDataHandlerV0) parseInitiateData(calldata []byte) (map[[SecretHashSiz SecretHash: init.SecretHash, Participant: init.Participant, Value: init.Value, + ValueGwei: WeiToGwei(init.Value), } } return toReturn, nil } -func (t *txDataHandlerV0) parseRedeemData(calldata []byte) (map[[SecretHashSize]byte]*Redemption, error) { +// ParseRedeemData parses the calldata used to call the redeem function of a +// specific version of the swap contract. It returns the the list of redemptions +// done in the call and errors if the call data does not call redeem with expected +// argument types. +func ParseRedeemDataV0(calldata []byte) (map[[SecretHashSize]byte]*Redemption, error) { decoded, err := ParseCallData(calldata, ABIs[0]) if err != nil { return nil, fmt.Errorf("unable to parse call data: %v", err) } - if decoded.Name != t.redeemFuncName { - return nil, fmt.Errorf("expected %v function but got %v", t.redeemFuncName, decoded.Name) + if decoded.Name != redeemFuncName { + return nil, fmt.Errorf("expected %v function but got %v", redeemFuncName, decoded.Name) } args := decoded.inputs // Any difference in number of args and types than what we expect @@ -180,15 +141,18 @@ func (t *txDataHandlerV0) parseRedeemData(calldata []byte) (map[[SecretHashSize] return toReturn, nil } -func (t *txDataHandlerV0) parseRefundData(calldata []byte) ([32]byte, error) { +// ParseRefundData parses the calldata used to call the refund function of a +// specific version of the swap contract. It returns the secret hash and errors +// if the call data does not call refund with expected argument types. +func ParseRefundDataV0(calldata []byte) ([32]byte, error) { var secretHash [32]byte decoded, err := ParseCallData(calldata, ABIs[0]) if err != nil { return secretHash, fmt.Errorf("unable to parse call data: %v", err) } - if decoded.Name != t.refundFuncName { - return secretHash, fmt.Errorf("expected %v function but got %v", t.refundFuncName, decoded.Name) + if decoded.Name != refundFuncName { + return secretHash, fmt.Errorf("expected %v function but got %v", refundFuncName, decoded.Name) } args := decoded.inputs // Any difference in number of args and types than what we expect @@ -206,3 +170,149 @@ func (t *txDataHandlerV0) parseRefundData(calldata []byte) ([32]byte, error) { return secretHash, nil } + +type RedemptionV1 struct { + Secret [32]byte + Contract *dex.SwapContractDetails +} + +func ParseInitiateDataV1(calldata []byte) (map[[SecretHashSize]byte]*dex.SwapContractDetails, error) { + decoded, err := ParseCallData(calldata, ABIs[1]) + if err != nil { + return nil, fmt.Errorf("unable to parse call data: %v", err) + } + if decoded.Name != initiateFuncName { + return nil, fmt.Errorf("expected %v function but got %v", initiateFuncName, decoded.Name) + } + args := decoded.inputs + // Any difference in number of args and types than what we expect + // should be caught by parseCallData, but checking again anyway. + // + // TODO: If any of the checks prove redundant, remove them. + const numArgs = 1 + if len(args) != numArgs { + return nil, fmt.Errorf("expected %v input args but got %v", numArgs, len(args)) + } + initiations, ok := args[0].value.([]struct { + SecretHash [32]byte `json:"secretHash"` + Initiator common.Address `json:"initiator"` + RefundTimestamp uint64 `json:"refundTimestamp"` + Participant common.Address `json:"participant"` + Value uint64 `json:"value"` + }) + if !ok { + return nil, fmt.Errorf("expected first arg of type []swapv1.ETHSwapContract but got %T", args[0].value) + } + + // This is done for the compiler to ensure that the type defined above and + // swapv0.ETHSwapInitiation are the same, other than the tags. + if len(initiations) > 0 { + _ = swapv1.ETHSwapContract(initiations[0]) + } + + toReturn := make(map[[SecretHashSize]byte]*dex.SwapContractDetails, len(initiations)) + for _, init := range initiations { + toReturn[init.SecretHash] = &dex.SwapContractDetails{ + From: init.Initiator.String(), + To: init.Participant.String(), + Value: init.Value, + SecretHash: init.SecretHash[:], + LockTime: init.RefundTimestamp, + } + } + + return toReturn, nil +} + +func ParseRedeemDataV1(calldata []byte) (map[[SecretHashSize]byte]*RedemptionV1, error) { + decoded, err := ParseCallData(calldata, ABIs[1]) + if err != nil { + return nil, fmt.Errorf("unable to parse call data: %v", err) + } + if decoded.Name != redeemFuncName { + return nil, fmt.Errorf("expected %v function but got %v", redeemFuncName, decoded.Name) + } + args := decoded.inputs + // Any difference in number of args and types than what we expect + // should be caught by parseCallData, but checking again anyway. + // + // TODO: If any of the checks prove redundant, remove them. + const numArgs = 1 + if len(args) != numArgs { + return nil, fmt.Errorf("expected %v redeem args but got %v", numArgs, len(args)) + } + + redemptions, ok := args[0].value.([]struct { + C struct { + SecretHash [32]uint8 `json:"secretHash"` + Initiator common.Address `json:"initiator"` + RefundTimestamp uint64 `json:"refundTimestamp"` + Participant common.Address `json:"participant"` + Value uint64 `json:"value"` + } `json:"c"` + Secret [32]uint8 `json:"secret"` + }) + if !ok { + return nil, fmt.Errorf("expected first arg of type []swapv0.ETHSwapRedemption but got %T", args[0].value) + } + + // This is done for the compiler to ensure that the type defined above and + // swapv0.ETHSwapRedemption are the same, other than the tags. + if len(redemptions) > 0 { + // Why can't I do ETHSwapRedemption directly? + _ = swapv1.ETHSwapContract(redemptions[0].C) + } + + toReturn := make(map[[SecretHashSize]byte]*RedemptionV1, len(redemptions)) + for _, r := range redemptions { + toReturn[r.C.SecretHash] = &RedemptionV1{ + Contract: &dex.SwapContractDetails{ + From: r.C.Initiator.String(), + To: r.C.Participant.String(), + Value: r.C.Value, + SecretHash: r.C.SecretHash[:], + LockTime: r.C.RefundTimestamp, + }, + Secret: r.Secret, + } + } + + return toReturn, nil +} + +func ParseRefundDataV1(calldata []byte) (*dex.SwapContractDetails, error) { + decoded, err := ParseCallData(calldata, ABIs[1]) + if err != nil { + return nil, fmt.Errorf("unable to parse call data: %v", err) + } + if decoded.Name != refundFuncName { + return nil, fmt.Errorf("expected %v function but got %v", refundFuncName, decoded.Name) + } + args := decoded.inputs + // Any difference in number of args and types than what we expect + // should be caught by parseCallData, but checking again anyway. + // + // TODO: If any of the checks prove redundant, remove them. + const numArgs = 1 + if len(args) != numArgs { + return nil, fmt.Errorf("expected %v redeem args but got %v", numArgs, len(args)) + } + contract, ok := args[0].value.(struct { + SecretHash [32]byte `json:"secretHash"` + Initiator common.Address `json:"initiator"` + RefundTimestamp uint64 `json:"refundTimestamp"` + Participant common.Address `json:"participant"` + Value uint64 `json:"value"` + }) + if !ok { + return nil, fmt.Errorf("expected first arg of type [32]byte but got %T", args[0].value) + } + + return &dex.SwapContractDetails{ + From: contract.Initiator.String(), + To: contract.Participant.String(), + Value: contract.Value, + LockTime: contract.RefundTimestamp, + SecretHash: contract.SecretHash[:], + }, nil +} diff --git a/dex/networks/eth/txdata_test.go b/dex/networks/eth/txdata_test.go index 73b6c3d22c..cdd23e1e7b 100644 --- a/dex/networks/eth/txdata_test.go +++ b/dex/networks/eth/txdata_test.go @@ -126,7 +126,7 @@ func TestParseInitiateDataV0(t *testing.T) { }} for _, test := range tests { - parsedInitiations, err := ParseInitiateData(test.calldata, 0) + parsedInitiations, err := ParseInitiateDataV0(test.calldata) if test.wantErr { if err == nil { t.Fatalf("expected error for test %q", test.name) @@ -219,7 +219,7 @@ func TestParseRedeemDataV0(t *testing.T) { }} for _, test := range tests { - parsedRedemptions, err := ParseRedeemData(test.calldata, 0) + parsedRedemptions, err := ParseRedeemDataV0(test.calldata) if test.wantErr { if err == nil { t.Fatalf("expected error for test %q", test.name) @@ -284,7 +284,7 @@ func TestParseRefundDataV0(t *testing.T) { }} for _, test := range tests { - parsedSecretHash, err := ParseRefundData(test.calldata, 0) + parsedSecretHash, err := ParseRefundDataV0(test.calldata) if test.wantErr { if err == nil { t.Fatalf("expected error for test %q", test.name) diff --git a/dex/testing/eth/harness.sh b/dex/testing/eth/harness.sh index 38afd5e99e..c72009b761 100755 --- a/dex/testing/eth/harness.sh +++ b/dex/testing/eth/harness.sh @@ -52,6 +52,9 @@ ETH_SWAP_V0="608060405234801561001057600080fd5b50610cb6806100206000396000f3fe608 ERC20_SWAP_V0="60a060405234801561001057600080fd5b50604051610fa7380380610fa783398101604081905261002f91610040565b6001600160a01b0316608052610070565b60006020828403121561005257600080fd5b81516001600160a01b038116811461006957600080fd5b9392505050565b608051610f0861009f6000396000818160d0015281816102b9015281816106890152610adb0152610f086000f3fe608060405234801561001057600080fd5b50600436106100885760003560e01c8063bfd2fd971161005b578063bfd2fd971461011d578063d0f761c014610140578063eb84e7f214610153578063f4fd17f9146101c257600080fd5b80637249fbb61461008d57806376467cbd146100a25780638c8e8fee146100cb578063a8793f941461010a575b600080fd5b6100a061009b366004610bb0565b6101d5565b005b6100b56100b0366004610bb0565b610394565b6040516100c29190610c01565b60405180910390f35b6100f27f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b0390911681526020016100c2565b6100a0610118366004610c66565b61046f565b61013061012b366004610cdb565b61076a565b60405190151581526020016100c2565b61013061014e366004610bb0565b610834565b6101af610161366004610bb0565b60006020819052908152604090208054600182015460028301546003840154600485015460059095015493949293919290916001600160a01b0391821691811690600160a01b900460ff1687565b6040516100c29796959493929190610cfd565b6100a06101d0366004610d49565b610894565b3233146101fd5760405162461bcd60e51b81526004016101f490610dac565b60405180910390fd5b61020681610834565b6102435760405162461bcd60e51b815260206004820152600e60248201526d6e6f7420726566756e6461626c6560901b60448201526064016101f4565b60008181526020818152604080832060058101805460ff60a01b1916600360a01b17905560018101548251336024820152604480820192909252835180820390920182526064018352928301805163a9059cbb60e01b6001600160e01b0390911617905290519092916060916001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016916102e391610dd6565b6000604051808303816000865af19150503d8060008114610320576040519150601f19603f3d011682016040523d82523d6000602084013e610325565b606091505b5090925090508180156103505750805115806103505750808060200190518101906103509190610e11565b61038e5760405162461bcd60e51b815260206004820152600f60248201526e1d1c985b9cd9995c8819985a5b1959608a1b60448201526064016101f4565b50505050565b6103d16040805160e081018252600080825260208201819052918101829052606081018290526080810182905260a081018290529060c082015290565b60008281526020818152604091829020825160e08101845281548152600182015492810192909252600281015492820192909252600380830154606083015260048301546001600160a01b039081166080840152600584015490811660a084015291929160c0840191600160a01b90910460ff169081111561045557610455610bc9565b600381111561046657610466610bc9565b90525092915050565b32331461048e5760405162461bcd60e51b81526004016101f490610dac565b6000805b8281101561063357368484838181106104ad576104ad610e33565b90506080020190506000806000836020013581526020019081526020016000209050600082606001351161050b5760405162461bcd60e51b81526020600482015260056024820152640c081d985b60da1b60448201526064016101f4565b813561054d5760405162461bcd60e51b815260206004820152601160248201527003020726566756e6454696d657374616d7607c1b60448201526064016101f4565b60006005820154600160a01b900460ff16600381111561056f5761056f610bc9565b146105ae5760405162461bcd60e51b815260206004820152600f60248201526e0c8eae040e6cac6e4cae840d0c2e6d608b1b60448201526064016101f4565b436002820155813560038201556004810180546001600160a01b031916331790556105df6060830160408401610e49565b6005820180546060850135600185018190556001600160a01b03939093166001600160a81b031990911617600160a01b17905561061c9085610e88565b93505050808061062b90610ea0565b915050610492565b5060408051336024820152306044820152606480820184905282518083039091018152608490910182526020810180516001600160e01b03166323b872dd60e01b17905290516000916060916001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016916106b391610dd6565b6000604051808303816000865af19150503d80600081146106f0576040519150601f19603f3d011682016040523d82523d6000602084013e6106f5565b606091505b5090925090508180156107205750805115806107205750808060200190518101906107209190610e11565b6107635760405162461bcd60e51b81526020600482015260146024820152731d1c985b9cd9995c88199c9bdb4819985a5b195960621b60448201526064016101f4565b5050505050565b600082815260208190526040812060016005820154600160a01b900460ff16600381111561079a5761079a610bc9565b1480156107b3575060058101546001600160a01b031633145b801561082c5750836002846040516020016107d091815260200190565b60408051601f19818403018152908290526107ea91610dd6565b602060405180830381855afa158015610807573d6000803e3d6000fd5b5050506040513d601f19601f8201168201806040525081019061082a9190610eb9565b145b949350505050565b600081815260208190526040812060016005820154600160a01b900460ff16600381111561086457610864610bc9565b14801561087d575060048101546001600160a01b031633145b801561088d575080600301544210155b9392505050565b3233146108b35760405162461bcd60e51b81526004016101f490610dac565b6000805b82811015610a8b57368484838181106108d2576108d2610e33565b6020604091820293909301838101356000908152938490529220919250600190506005820154600160a01b900460ff16600381111561091357610913610bc9565b1461094c5760405162461bcd60e51b815260206004820152600960248201526862616420737461746560b81b60448201526064016101f4565b60058101546001600160a01b0316331461099a5760405162461bcd60e51b815260206004820152600f60248201526e189859081c185c9d1a58da5c185b9d608a1b60448201526064016101f4565b8160200135600283600001356040516020016109b891815260200190565b60408051601f19818403018152908290526109d291610dd6565b602060405180830381855afa1580156109ef573d6000803e3d6000fd5b5050506040513d601f19601f82011682018060405250810190610a129190610eb9565b14610a4c5760405162461bcd60e51b815260206004820152600a602482015269189859081cd958dc995d60b21b60448201526064016101f4565b60058101805460ff60a01b1916600160a11b179055813581556001810154610a749085610e88565b935050508080610a8390610ea0565b9150506108b7565b5060408051336024820152604480820184905282518083039091018152606490910182526020810180516001600160e01b031663a9059cbb60e01b17905290516000916060916001600160a01b037f00000000000000000000000000000000000000000000000000000000000000001691610b0591610dd6565b6000604051808303816000865af19150503d8060008114610b42576040519150601f19603f3d011682016040523d82523d6000602084013e610b47565b606091505b509092509050818015610b72575080511580610b72575080806020019051810190610b729190610e11565b6107635760405162461bcd60e51b815260206004820152600f60248201526e1d1c985b9cd9995c8819985a5b1959608a1b60448201526064016101f4565b600060208284031215610bc257600080fd5b5035919050565b634e487b7160e01b600052602160045260246000fd5b60048110610bfd57634e487b7160e01b600052602160045260246000fd5b9052565b600060e08201905082518252602083015160208301526040830151604083015260608301516060830152608083015160018060a01b0380821660808501528060a08601511660a0850152505060c0830151610c5f60c0840182610bdf565b5092915050565b60008060208385031215610c7957600080fd5b823567ffffffffffffffff80821115610c9157600080fd5b818501915085601f830112610ca557600080fd5b813581811115610cb457600080fd5b8660208260071b8501011115610cc957600080fd5b60209290920196919550909350505050565b60008060408385031215610cee57600080fd5b50508035926020909101359150565b8781526020810187905260408101869052606081018590526001600160a01b038481166080830152831660a082015260e08101610d3d60c0830184610bdf565b98975050505050505050565b60008060208385031215610d5c57600080fd5b823567ffffffffffffffff80821115610d7457600080fd5b818501915085601f830112610d8857600080fd5b813581811115610d9757600080fd5b8660208260061b8501011115610cc957600080fd5b60208082526010908201526f39b2b73232b910109e9037b934b3b4b760811b604082015260600190565b6000825160005b81811015610df75760208186018101518583015201610ddd565b81811115610e06576000828501525b509190910192915050565b600060208284031215610e2357600080fd5b8151801515811461088d57600080fd5b634e487b7160e01b600052603260045260246000fd5b600060208284031215610e5b57600080fd5b81356001600160a01b038116811461088d57600080fd5b634e487b7160e01b600052601160045260246000fd5b60008219821115610e9b57610e9b610e72565b500190565b600060018201610eb257610eb2610e72565b5060010190565b600060208284031215610ecb57600080fd5b505191905056fea26469706673582212203e5af33d9672cc61834e91e1c391f408502e89feacb59762150c3e8ad4b8eb8764736f6c634300080f0033" TEST_TOKEN="608060405234801561001057600080fd5b506040805180820190915260098152682a32b9ba2a37b5b2b760b91b602082015260039061003e90826101d1565b506040805180820190915260038152621514d560ea1b602082015260049061006690826101d1565b506909513ea9de0243800000600255600060208190526902544faa778090e000007f7d4921c2bc32c0110a31d16f4efb43c7a1228f1df7af765f608241dee5c62ebc8190557f59603491850c7d11499afe95b334ccfd92b48b36a15df31ef59ff5813fe370828190557f963f2e057cac0b71a4b8cff76a0e66200ffc6cc5498c1198bc1df3cb2bf751dc819055731d4f2ee206474b136af4868b887c7b166693c1949091527fbc10d5a0a531ecf97938db2df6f3f5b59678ae655bd09be1d358f605f79153d455610290565b634e487b7160e01b600052604160045260246000fd5b600181811c9082168061015c57607f821691505b60208210810361017c57634e487b7160e01b600052602260045260246000fd5b50919050565b601f8211156101cc57600081815260208120601f850160051c810160208610156101a95750805b601f850160051c820191505b818110156101c8578281556001016101b5565b5050505b505050565b81516001600160401b038111156101ea576101ea610132565b6101fe816101f88454610148565b84610182565b602080601f831160018114610233576000841561021b5750858301515b600019600386901b1c1916600185901b1785556101c8565b600085815260208120601f198616915b8281101561026257888601518255948401946001909101908401610243565b50858210156102805787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b6107968061029f6000396000f3fe608060405234801561001057600080fd5b506004361061009e5760003560e01c806370a082311161006657806370a08231146101185780638ba4cc3c1461014157806395d89b4114610156578063a9059cbb1461015e578063dd62ed3e1461017157600080fd5b806306fdde03146100a3578063095ea7b3146100c157806318160ddd146100e457806323b872dd146100f6578063313ce56714610109575b600080fd5b6100ab6101aa565b6040516100b891906105d4565b60405180910390f35b6100d46100cf366004610645565b61023c565b60405190151581526020016100b8565b6002545b6040519081526020016100b8565b6100d461010436600461066f565b610252565b604051601281526020016100b8565b6100e86101263660046106ab565b6001600160a01b031660009081526020819052604090205490565b61015461014f366004610645565b610301565b005b6100ab610349565b6100d461016c366004610645565b610358565b6100e861017f3660046106cd565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b6060600380546101b990610700565b80601f01602080910402602001604051908101604052809291908181526020018280546101e590610700565b80156102325780601f1061020757610100808354040283529160200191610232565b820191906000526020600020905b81548152906001019060200180831161021557829003601f168201915b5050505050905090565b6000610249338484610365565b50600192915050565b600061025f848484610454565b6001600160a01b0384166000908152600160209081526040808320338452909152902054828110156102e95760405162461bcd60e51b815260206004820152602860248201527f45524332303a207472616e7366657220616d6f756e74206578636565647320616044820152676c6c6f77616e636560c01b60648201526084015b60405180910390fd5b6102f68533858403610365565b506001949350505050565b8060026000828254610313919061073a565b90915550506001600160a01b0382166000908152602081905260408120805483929061034090849061073a565b90915550505050565b6060600480546101b990610700565b6000610249338484610454565b6001600160a01b0383166103c75760405162461bcd60e51b8152602060048201526024808201527f45524332303a20617070726f76652066726f6d20746865207a65726f206164646044820152637265737360e01b60648201526084016102e0565b6001600160a01b0382166104285760405162461bcd60e51b815260206004820152602260248201527f45524332303a20617070726f766520746f20746865207a65726f206164647265604482015261737360f01b60648201526084016102e0565b6001600160a01b0392831660009081526001602090815260408083209490951682529290925291902055565b6001600160a01b0383166104b85760405162461bcd60e51b815260206004820152602560248201527f45524332303a207472616e736665722066726f6d20746865207a65726f206164604482015264647265737360d81b60648201526084016102e0565b6001600160a01b03821661051a5760405162461bcd60e51b815260206004820152602360248201527f45524332303a207472616e7366657220746f20746865207a65726f206164647260448201526265737360e81b60648201526084016102e0565b6001600160a01b038316600090815260208190526040902054818110156105925760405162461bcd60e51b815260206004820152602660248201527f45524332303a207472616e7366657220616d6f756e7420657863656564732062604482015265616c616e636560d01b60648201526084016102e0565b6001600160a01b038085166000908152602081905260408082208585039055918516815290812080548492906105c990849061073a565b909155505050505050565b600060208083528351808285015260005b81811015610601578581018301518582016040015282016105e5565b81811115610613576000604083870101525b50601f01601f1916929092016040019392505050565b80356001600160a01b038116811461064057600080fd5b919050565b6000806040838503121561065857600080fd5b61066183610629565b946020939093013593505050565b60008060006060848603121561068457600080fd5b61068d84610629565b925061069b60208501610629565b9150604084013590509250925092565b6000602082840312156106bd57600080fd5b6106c682610629565b9392505050565b600080604083850312156106e057600080fd5b6106e983610629565b91506106f760208401610629565b90509250929050565b600181811c9082168061071457607f821691505b60208210810361073457634e487b7160e01b600052602260045260246000fd5b50919050565b6000821982111561075b57634e487b7160e01b600052601160045260246000fd5b50019056fea26469706673582212201528eada32f708041b3309a694f6ffe74e05ebdb01eafc2fb478927580375b6d64736f6c634300080f0033" +ETH_SWAP_V1="608060405234801561001057600080fd5b50610e8e806100206000396000f3fe60806040526004361061007b5760003560e01c80637802689d1161004e5780637802689d1461011b578063d2544c061461013b578063eb84e7f21461015b578063ed7cbed71461019657600080fd5b806338ec176814610080578063428b16e1146100b657806364a97bff146100d857806377d7e031146100eb575b600080fd5b34801561008c57600080fd5b506100a061009b366004610b6f565b6101b6565b6040516100ad9190610b9d565b60405180910390f35b3480156100c257600080fd5b506100d66100d1366004610be0565b610272565b005b6100d66100e6366004610c55565b610503565b3480156100f757600080fd5b5061010b610106366004610cb8565b61073b565b60405190151581526020016100ad565b34801561012757600080fd5b5061010b610136366004610b6f565b6107b5565b34801561014757600080fd5b506100d6610156366004610b6f565b6107e8565b34801561016757600080fd5b50610188610176366004610cda565b60006020819052908152604090205481565b6040519081526020016100ad565b3480156101a257600080fd5b506101886101b1366004610b6f565b610a4a565b6040805160608101825260008082526020820181905291810182905290806101dd84610b43565b92509250506102076040805160608101909152806000815260006020820181905260409091015290565b8160000361022e578060005b9081600381111561022657610226610b87565b90525061026a565b6001830161023e57806003610213565b61024983863561073b565b1561025e57600281526020810183905261026a565b60018152604081018290525b949350505050565b32331461029a5760405162461bcd60e51b815260040161029190610cf3565b60405180910390fd5b6000805b8281101561046c57368484838181106102b9576102b9610d1d565b60c0029190910191503390506102d56080830160608401610d33565b6001600160a01b0316146103185760405162461bcd60e51b815260206004820152600a6024820152691b9bdd08185d5d1a195960b21b6044820152606401610291565b6000808061032584610b43565b92509250925060008111801561033a57504381105b6103765760405162461bcd60e51b815260206004820152600d60248201526c0756e66696c6c6564207377617609c1b6044820152606401610291565b61038182853561073b565b156103c15760405162461bcd60e51b815260206004820152601060248201526f185b1c9958591e481c995919595b595960821b6044820152606401610291565b6103d060a0850135853561073b565b61040d5760405162461bcd60e51b815260206004820152600e60248201526d1a5b9d985b1a59081cd958dc995d60921b6044820152606401610291565b600083815260208190526040902060a0850180359091556104319060808601610d63565b61043f90633b9aca00610da3565b6104539067ffffffffffffffff1687610dd3565b955050505050808061046490610deb565b91505061029e565b50604051600090339083908381818185875af1925050503d80600081146104af576040519150601f19603f3d011682016040523d82523d6000602084013e6104b4565b606091505b50909150506001811515146104fd5760405162461bcd60e51b815260206004820152600f60248201526e1d1c985b9cd9995c8819985a5b1959608a1b6044820152606401610291565b50505050565b3233146105225760405162461bcd60e51b815260040161029190610cf3565b6000805b828110156106fc573684848381811061054157610541610d1d565b905060a002019050600081608001602081019061055e9190610d63565b67ffffffffffffffff161161059d5760405162461bcd60e51b81526020600482015260056024820152640c081d985b60da1b6044820152606401610291565b60006105af6060830160408401610d63565b67ffffffffffffffff16116105fa5760405162461bcd60e51b815260206004820152601160248201527003020726566756e6454696d657374616d7607c1b6044820152606401610291565b600061060582610a4a565b60008181526020819052604090205490915080156106565760405162461bcd60e51b815260206004820152600e60248201526d73776170206e6f7420656d70747960901b6044820152606401610291565b504361066381843561073b565b156106a15760405162461bcd60e51b815260206004820152600e60248201526d3430b9b41031b7b63634b9b4b7b760911b6044820152606401610291565b60008281526020819052604090208190556106c260a0840160808501610d63565b6106d090633b9aca00610da3565b6106e49067ffffffffffffffff1686610dd3565b945050505080806106f490610deb565b915050610526565b503481146107365760405162461bcd60e51b8152602060048201526007602482015266189859081d985b60ca1b6044820152606401610291565b505050565b60008160028460405160200161075391815260200190565b60408051601f198184030181529082905261076d91610e04565b602060405180830381855afa15801561078a573d6000803e3d6000fd5b5050506040513d601f19601f820116820180604052508101906107ad9190610e3f565b149392505050565b60008060006107c384610b43565b92509250508060001415801561026a57506107df82853561073b565b15949350505050565b3233146108075760405162461bcd60e51b815260040161029190610cf3565b6108176060820160408301610d63565b67ffffffffffffffff164210156108675760405162461bcd60e51b81526020600482015260146024820152731b1bd8dadd1a5b59481b9bdd08195e1c1a5c995960621b6044820152606401610291565b600080600061087584610b43565b92509250925060008111801561088b5750438111155b6108c95760405162461bcd60e51b815260206004820152600f60248201526e73776170206e6f742061637469766560881b6044820152606401610291565b6108d482853561073b565b156109195760405162461bcd60e51b81526020600482015260156024820152741cddd85c08185b1c9958591e481c995919595b5959605a1b6044820152606401610291565b600182016109615760405162461bcd60e51b81526020600482015260156024820152741cddd85c08185b1c9958591e481c99599d5b991959605a1b6044820152606401610291565b600083815260208181526040808320600019905561098491908701908701610d33565b6001600160a01b031661099d60a0870160808801610d63565b6109ab90633b9aca00610da3565b67ffffffffffffffff1660405160006040518083038185875af1925050503d80600081146109f5576040519150601f19603f3d011682016040523d82523d6000602084013e6109fa565b606091505b5090915050600181151514610a435760405162461bcd60e51b815260206004820152600f60248201526e1d1c985b9cd9995c8819985a5b1959608a1b6044820152606401610291565b5050505050565b600060028235610a606040850160208601610d33565b60601b846060016020810190610a769190610d33565b60601b610a8960a0870160808801610d63565b60c01b610a9c6060880160408901610d63565b6040805160208101969096526bffffffffffffffffffffffff19948516908601529190921660548401526001600160c01b0319918216606884015260c01b16607082015260780160408051601f1981840301815290829052610afd91610e04565b602060405180830381855afa158015610b1a573d6000803e3d6000fd5b5050506040513d601f19601f82011682018060405250810190610b3d9190610e3f565b92915050565b600080600080610b5285610a4a565b600081815260208190526040902054909690955085945092505050565b600060a08284031215610b8157600080fd5b50919050565b634e487b7160e01b600052602160045260246000fd5b8151606082019060048110610bc257634e487b7160e01b600052602160045260246000fd5b80835250602083015160208301526040830151604083015292915050565b60008060208385031215610bf357600080fd5b823567ffffffffffffffff80821115610c0b57600080fd5b818501915085601f830112610c1f57600080fd5b813581811115610c2e57600080fd5b86602060c083028501011115610c4357600080fd5b60209290920196919550909350505050565b60008060208385031215610c6857600080fd5b823567ffffffffffffffff80821115610c8057600080fd5b818501915085601f830112610c9457600080fd5b813581811115610ca357600080fd5b86602060a083028501011115610c4357600080fd5b60008060408385031215610ccb57600080fd5b50508035926020909101359150565b600060208284031215610cec57600080fd5b5035919050565b60208082526010908201526f39b2b73232b910109e9037b934b3b4b760811b604082015260600190565b634e487b7160e01b600052603260045260246000fd5b600060208284031215610d4557600080fd5b81356001600160a01b0381168114610d5c57600080fd5b9392505050565b600060208284031215610d7557600080fd5b813567ffffffffffffffff81168114610d5c57600080fd5b634e487b7160e01b600052601160045260246000fd5b600067ffffffffffffffff80831681851681830481118215151615610dca57610dca610d8d565b02949350505050565b60008219821115610de657610de6610d8d565b500190565b600060018201610dfd57610dfd610d8d565b5060010190565b6000825160005b81811015610e255760208186018101518583015201610e0b565b81811115610e34576000828501525b509190910192915050565b600060208284031215610e5157600080fd5b505191905056fea2646970667358221220ff4aa3b731ad448a2ec9f367bc9ddb0be755eebc0f8e3159b55e2113545da62764736f6c634300080f0033" +ERC20_SWAP_V1="60a060405234801561001057600080fd5b506040516111cb3803806111cb83398101604081905261002f91610040565b6001600160a01b0316608052610070565b60006020828403121561005257600080fd5b81516001600160a01b038116811461006957600080fd5b9392505050565b60805161112b6100a060003960008181610158015281816105130152818161083e0152610b5d015261112b6000f3fe6080604052600436106100865760003560e01c80637802689d116100595780637802689d146101265780638c8e8fee14610146578063d2544c0614610192578063eb84e7f2146101b2578063ed7cbed7146101ed57600080fd5b806338ec17681461008b578063428b16e1146100c157806364a97bff146100e357806377d7e031146100f6575b600080fd5b34801561009757600080fd5b506100ab6100a6366004610dea565b61020d565b6040516100b89190610e18565b60405180910390f35b3480156100cd57600080fd5b506100e16100dc366004610e5b565b6102c9565b005b6100e16100f1366004610ed0565b6105ef565b34801561010257600080fd5b50610116610111366004610f33565b610918565b60405190151581526020016100b8565b34801561013257600080fd5b50610116610141366004610dea565b610992565b34801561015257600080fd5b5061017a7f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b0390911681526020016100b8565b34801561019e57600080fd5b506100e16101ad366004610dea565b6109c5565b3480156101be57600080fd5b506101df6101cd366004610f55565b60006020819052908152604090205481565b6040519081526020016100b8565b3480156101f957600080fd5b506101df610208366004610dea565b610cc5565b60408051606081018252600080825260208201819052918101829052908061023484610dbe565b925092505061025e6040805160608101909152806000815260006020820181905260409091015290565b81600003610285578060005b9081600381111561027d5761027d610e02565b9052506102c1565b600183016102955780600361026a565b6102a0838635610918565b156102b55760028152602081018390526102c1565b60018152604081018290525b949350505050565b3233146102f15760405162461bcd60e51b81526004016102e890610f6e565b60405180910390fd5b6000805b828110156104c3573684848381811061031057610310610f98565b60c00291909101915033905061032c6080830160608401610fae565b6001600160a01b03161461036f5760405162461bcd60e51b815260206004820152600a6024820152691b9bdd08185d5d1a195960b21b60448201526064016102e8565b6000808061037c84610dbe565b92509250925060008111801561039157504381105b6103cd5760405162461bcd60e51b815260206004820152600d60248201526c0756e66696c6c6564207377617609c1b60448201526064016102e8565b6103d8828535610918565b156104185760405162461bcd60e51b815260206004820152601060248201526f185b1c9958591e481c995919595b595960821b60448201526064016102e8565b61042760a08501358535610918565b6104645760405162461bcd60e51b815260206004820152600e60248201526d1a5b9d985b1a59081cd958dc995d60921b60448201526064016102e8565b600083815260208190526040902060a0850180359091556104889060808601610fde565b61049690633b9aca0061101e565b6104aa9067ffffffffffffffff168761104e565b95505050505080806104bb90611066565b9150506102f5565b5060408051336024820152604480820184905282518083039091018152606490910182526020810180516001600160e01b031663a9059cbb60e01b17905290516000916060916001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000169161053d9161107f565b6000604051808303816000865af19150503d806000811461057a576040519150601f19603f3d011682016040523d82523d6000602084013e61057f565b606091505b5090925090508180156105aa5750805115806105aa5750808060200190518101906105aa91906110ba565b6105e85760405162461bcd60e51b815260206004820152600f60248201526e1d1c985b9cd9995c8819985a5b1959608a1b60448201526064016102e8565b5050505050565b32331461060e5760405162461bcd60e51b81526004016102e890610f6e565b6000805b828110156107e8573684848381811061062d5761062d610f98565b905060a002019050600081608001602081019061064a9190610fde565b67ffffffffffffffff16116106895760405162461bcd60e51b81526020600482015260056024820152640c081d985b60da1b60448201526064016102e8565b600061069b6060830160408401610fde565b67ffffffffffffffff16116106e65760405162461bcd60e51b815260206004820152601160248201527003020726566756e6454696d657374616d7607c1b60448201526064016102e8565b60006106f182610cc5565b60008181526020819052604090205490915080156107425760405162461bcd60e51b815260206004820152600e60248201526d73776170206e6f7420656d70747960901b60448201526064016102e8565b504361074f818435610918565b1561078d5760405162461bcd60e51b815260206004820152600e60248201526d3430b9b41031b7b63634b9b4b7b760911b60448201526064016102e8565b60008281526020819052604090208190556107ae60a0840160808501610fde565b6107bc90633b9aca0061101e565b6107d09067ffffffffffffffff168661104e565b945050505080806107e090611066565b915050610612565b5060408051336024820152306044820152606480820184905282518083039091018152608490910182526020810180516001600160e01b03166323b872dd60e01b17905290516000916060916001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016916108689161107f565b6000604051808303816000865af19150503d80600081146108a5576040519150601f19603f3d011682016040523d82523d6000602084013e6108aa565b606091505b5090925090508180156108d55750805115806108d55750808060200190518101906108d591906110ba565b6105e85760405162461bcd60e51b81526020600482015260146024820152731d1c985b9cd9995c88199c9bdb4819985a5b195960621b60448201526064016102e8565b60008160028460405160200161093091815260200190565b60408051601f198184030181529082905261094a9161107f565b602060405180830381855afa158015610967573d6000803e3d6000fd5b5050506040513d601f19601f8201168201806040525081019061098a91906110dc565b149392505050565b60008060006109a084610dbe565b9250925050806000141580156102c157506109bc828535610918565b15949350505050565b3233146109e45760405162461bcd60e51b81526004016102e890610f6e565b6109f46060820160408301610fde565b67ffffffffffffffff16421015610a445760405162461bcd60e51b81526020600482015260146024820152731b1bd8dadd1a5b59481b9bdd08195e1c1a5c995960621b60448201526064016102e8565b6000806000610a5284610dbe565b925092509250600081118015610a685750438111155b610aa65760405162461bcd60e51b815260206004820152600f60248201526e73776170206e6f742061637469766560881b60448201526064016102e8565b610ab1828535610918565b15610af65760405162461bcd60e51b81526020600482015260156024820152741cddd85c08185b1c9958591e481c995919595b5959605a1b60448201526064016102e8565b60018201610b3e5760405162461bcd60e51b81526020600482015260156024820152741cddd85c08185b1c9958591e481c99599d5b991959605a1b60448201526064016102e8565b6000838152602081905260408120600019905560606001600160a01b037f0000000000000000000000000000000000000000000000000000000000000000167fa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b33610baf60a08a0160808b01610fde565b6040516001600160a01b03909216602483015267ffffffffffffffff16604482015260640160408051601f198184030181529181526020820180516001600160e01b03166001600160e01b0319909416939093179092529051610c12919061107f565b6000604051808303816000865af19150503d8060008114610c4f576040519150601f19603f3d011682016040523d82523d6000602084013e610c54565b606091505b509092509050818015610c7f575080511580610c7f575080806020019051810190610c7f91906110ba565b610cbd5760405162461bcd60e51b815260206004820152600f60248201526e1d1c985b9cd9995c8819985a5b1959608a1b60448201526064016102e8565b505050505050565b600060028235610cdb6040850160208601610fae565b60601b846060016020810190610cf19190610fae565b60601b610d0460a0870160808801610fde565b60c01b610d176060880160408901610fde565b6040805160208101969096526bffffffffffffffffffffffff19948516908601529190921660548401526001600160c01b0319918216606884015260c01b16607082015260780160408051601f1981840301815290829052610d789161107f565b602060405180830381855afa158015610d95573d6000803e3d6000fd5b5050506040513d601f19601f82011682018060405250810190610db891906110dc565b92915050565b600080600080610dcd85610cc5565b600081815260208190526040902054909690955085945092505050565b600060a08284031215610dfc57600080fd5b50919050565b634e487b7160e01b600052602160045260246000fd5b8151606082019060048110610e3d57634e487b7160e01b600052602160045260246000fd5b80835250602083015160208301526040830151604083015292915050565b60008060208385031215610e6e57600080fd5b823567ffffffffffffffff80821115610e8657600080fd5b818501915085601f830112610e9a57600080fd5b813581811115610ea957600080fd5b86602060c083028501011115610ebe57600080fd5b60209290920196919550909350505050565b60008060208385031215610ee357600080fd5b823567ffffffffffffffff80821115610efb57600080fd5b818501915085601f830112610f0f57600080fd5b813581811115610f1e57600080fd5b86602060a083028501011115610ebe57600080fd5b60008060408385031215610f4657600080fd5b50508035926020909101359150565b600060208284031215610f6757600080fd5b5035919050565b60208082526010908201526f39b2b73232b910109e9037b934b3b4b760811b604082015260600190565b634e487b7160e01b600052603260045260246000fd5b600060208284031215610fc057600080fd5b81356001600160a01b0381168114610fd757600080fd5b9392505050565b600060208284031215610ff057600080fd5b813567ffffffffffffffff81168114610fd757600080fd5b634e487b7160e01b600052601160045260246000fd5b600067ffffffffffffffff8083168185168183048111821515161561104557611045611008565b02949350505050565b6000821982111561106157611061611008565b500190565b60006001820161107857611078611008565b5060010190565b6000825160005b818110156110a05760208186018101518583015201611086565b818111156110af576000828501525b509190910192915050565b6000602082840312156110cc57600080fd5b81518015158114610fd757600080fd5b6000602082840312156110ee57600080fd5b505191905056fea2646970667358221220b11370b262d8a8823e931b54ec9699764ab88c2b1a67a8ea6ac515fb264bfffa64736f6c634300080f0033" + # PASSWORD is the password used to unlock all accounts/wallets/addresses. PASSWORD="abc" @@ -308,6 +311,9 @@ echo "Sending 5000 eth to delta and gamma and testing." echo "Deploying ETHSwapV0 contract." ETH_SWAP_CONTRACT_HASH=$("${NODES_ROOT}/harness-ctl/alpha" "attach --preload ${NODES_ROOT}/harness-ctl/deploy.js --exec deploy(\"${ALPHA_ADDRESS}\",\"${ETH_SWAP_V0}\")" | sed 's/"//g') +echo "Deploying ETHSwap1 contract." +ETH_SWAP_CONTRACT_HASH_V1=$("${NODES_ROOT}/harness-ctl/alpha" "attach --preload ${NODES_ROOT}/harness-ctl/deploy.js --exec deploy(\"${ALPHA_ADDRESS}\",\"${ETH_SWAP_V1}\")" | sed 's/"//g') + echo "Deploying TestToken contract." TEST_TOKEN_CONTRACT_HASH=$("${NODES_ROOT}/harness-ctl/alpha" "attach --preload ${NODES_ROOT}/harness-ctl/deploy.js --exec deploy(\"${ALPHA_ADDRESS}\",\"${TEST_TOKEN}\")" | sed 's/"//g') @@ -345,6 +351,12 @@ cat > "${NODES_ROOT}/eth_swap_contract_address.txt" < "${NODES_ROOT}/eth_swap_contract_address_v1.txt" < "${NODES_ROOT}/test_token_contract_address.txt" < "${NODES_ROOT}/erc20_swap_contract_address.txt" < "${NODES_ROOT}/erc20_swap_contract_address_v1.txt" < 1 { + return nil, fmt.Errorf("contract version %d not supported, only 0 and 1", contractVer) } contractAddr := tx.To() if *contractAddr != be.contractAddr { @@ -221,78 +192,11 @@ 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) { - swap, err := c.backend.node.swap(ctx, c.backend.assetID, c.secretHash) - if err != nil { - return -1, err - } - - // Uninitiated state is zero confs. It could still be in mempool. - // It is important to only trust confirmations according to the - // swap contract. Until there are confirmations we cannot be sure - // that initiation happened successfully. - if swap.State == dexeth.SSNone { - // 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. - if swap.Value.Cmp(c.init.Value) != 0 { - return -1, fmt.Errorf("tx data swap val (%d) does not match contract value (%d)", - c.init.Value, swap.Value) - } - if swap.Participant != c.init.Participant { - return -1, fmt.Errorf("tx data participant %q does not match contract value %q", - c.init.Participant, swap.Participant) - } - - // locktime := swap.RefundBlockTimestamp.Int64() - if !swap.LockTime.Equal(c.init.LockTime) { - return -1, fmt.Errorf("expected swap locktime (%s) does not match expected (%s)", - c.init.LockTime, swap.LockTime) - } - - bn, err := c.backend.node.blockNumber(ctx) - if err != nil { - return 0, fmt.Errorf("unable to fetch block number: %v", err) - } - return int64(bn - swap.BlockHeight + 1), nil + return c.backend.txConfirmations(ctx, c.txHash) } func (c *redeemCoin) Confirmations(ctx context.Context) (int64, error) { - swap, err := c.backend.node.swap(ctx, c.backend.assetID, c.secretHash) - if err != nil { - return -1, err - } - - // 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 swap.State == dexeth.SSRedeemed { - bn, err := c.backend.node.blockNumber(ctx) - if err != nil { - return 0, fmt.Errorf("unable to fetch block number: %v", err) - } - return int64(bn - swap.BlockHeight + 1), nil - } - // If swap is in the Initiated state, the redemption may be - // unmined. - if swap.State == dexeth.SSInitiated { - // Assume the tx still has a chance of being mined. - return 0, nil - } - // 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", swap.State) + return c.backend.txConfirmations(ctx, c.txHash) } func (c *redeemCoin) Value() uint64 { return 0 } @@ -320,5 +224,5 @@ func (c *baseCoin) FeeRate() uint64 { // Value returns the value of one swap in order to validate during processing. func (c *swapCoin) Value() uint64 { - return c.dexAtoms + return c.swap.Value } diff --git a/server/asset/eth/coiner_test.go b/server/asset/eth/coiner_test.go index 92013d929c..6dcb97b7ab 100644 --- a/server/asset/eth/coiner_test.go +++ b/server/asset/eth/coiner_test.go @@ -27,9 +27,9 @@ func TestNewRedeemCoin(t *testing.T) { contractAddr := randomAddress() var secret, secretHash, txHash [32]byte copy(txHash[:], encode.RandomBytes(32)) - copy(secret[:], redeemSecretB) - copy(secretHash[:], redeemSecretHashB) - contract := dexeth.EncodeContractData(0, secretHash) + copy(secret[:], tRedeem2.Secret[:]) + copy(secretHash[:], tRedeem2.C.SecretHash[:]) + contract := dexeth.EncodeContractData(version, secretHash) const gasPrice = 30 const gasTipCap = 2 const value = 5e9 @@ -61,23 +61,23 @@ func TestNewRedeemCoin(t *testing.T) { tx: tTx(gasPrice, gasTipCap, value, contractAddr, redeemCalldata), contract: encode.RandomBytes(32), wantErr: true, - }, { - name: "tx not found, redeemed", - txErr: ethereum.NotFound, - 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, secret, dexeth.SSInitiated, &initParticipantAddr), - contract: contract, - wantErr: true, - }, { - name: "tx not found, swap err", - txErr: ethereum.NotFound, - swpErr: errors.New("swap not found"), - contract: contract, - wantErr: true, + // }, { + // name: "tx not found, redeemed", + // txErr: ethereum.NotFound, + // 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, secret, dexeth.SSInitiated, &initParticipantAddr), + // contract: contract, + // wantErr: true, + // }, { + // name: "tx not found, swap err", + // txErr: ethereum.NotFound, + // swpErr: errors.New("swap not found"), + // contract: contract, + // wantErr: true, }} for _, test := range tests { node := &testNode{ @@ -92,7 +92,7 @@ func TestNewRedeemCoin(t *testing.T) { baseLogger: tLogger, }, contractAddr: *contractAddr, - initTxSize: uint32(dexeth.InitGas(1, 0)), + initTxSize: uint32(dexeth.InitGas(1, version)), assetID: BipID, log: tLogger, } @@ -108,7 +108,7 @@ func TestNewRedeemCoin(t *testing.T) { } if test.txErr == nil && (rc.secretHash != secretHash || - rc.secret != secret || + rc.redeem.Secret != secret || rc.value.Uint64() != 0 || rc.gasFeeCap != wantGas || rc.gasTipCap != wantGasTipCap) { @@ -121,21 +121,17 @@ func TestNewSwapCoin(t *testing.T) { contractAddr, randomAddr := randomAddress(), randomAddress() var secret, secretHash, txHash [32]byte copy(txHash[:], encode.RandomBytes(32)) - copy(secret[:], redeemSecretB) - copy(secretHash[:], redeemSecretHashB) + copy(secret[:], tRedeem2.Secret[:]) + copy(secretHash[:], tRedeem2.C.SecretHash[:]) txCoinIDBytes := txHash[:] badCoinIDBytes := encode.RandomBytes(39) const gasPrice = 30 - const value = 5e9 + value := tRedeem2.C.Value const gasTipCap = 2 wantGas, err := dexeth.WeiToGweiUint64(big.NewInt(3e10)) if err != nil { t.Fatal(err) } - wantVal, err := dexeth.WeiToGweiUint64(big.NewInt(5e18)) - if err != nil { - t.Fatal(err) - } wantGasTipCap, err := dexeth.WeiToGweiUint64(big.NewInt(2e9)) if err != nil { t.Fatal(err) @@ -233,13 +229,21 @@ func TestNewSwapCoin(t *testing.T) { t.Fatalf("unexpected error for test %q: %v", test.name, err) } - if sc.init.Participant != initParticipantAddr || + if sc.swap.To != tRedeem2.C.Participant.String() || sc.secretHash != secretHash || - dexeth.WeiToGwei(sc.value) != wantVal || + dexeth.WeiToGwei(sc.value) != tRedeem2.C.Value || sc.gasFeeCap != wantGas || sc.gasTipCap != wantGasTipCap || - sc.init.LockTime.Unix() != initLocktime { - t.Fatalf("returns do not match expected for test %q / %v", test.name, sc) + sc.swap.LockTime != tRedeem2.C.RefundTimestamp { + + t.Fatalf("returns do not match expected for test %q %t %t %t %t %t %t", test.name, + sc.swap.To != tRedeem2.C.Participant.String(), + sc.secretHash != secretHash, + dexeth.WeiToGwei(sc.value) != tRedeem2.C.Value, + sc.gasFeeCap != wantGas, + sc.gasTipCap != wantGasTipCap, + sc.swap.LockTime != tRedeem2.C.RefundTimestamp, + ) } } } @@ -254,24 +258,23 @@ func TestConfirmations(t *testing.T) { copy(contractAddr[:], encode.RandomBytes(20)) var secret, secretHash, txHash [32]byte copy(txHash[:], encode.RandomBytes(32)) - copy(secret[:], redeemSecretB) - copy(secretHash[:], redeemSecretHashB) + copy(secret[:], tRedeem2.Secret[:]) + copy(secretHash[:], tRedeem2.C.SecretHash[:]) const gasPrice = 30 const gasTipCap = 2 const swapVal = 25e8 const txVal = swapVal * 2 const oneGweiMore = swapVal + 1 + const blockNumber = 100 tests := []struct { name string swap *dexeth.SwapState - bn uint64 value uint64 wantConfs int64 swapErr, bnErr error wantErr, redeem bool }{{ name: "ok has confs value not verified", - bn: 100, swap: tSwap(97, initLocktime, swapVal, secret, dexeth.SSInitiated, &initParticipantAddr), value: txVal, wantConfs: 4, @@ -281,48 +284,10 @@ func TestConfirmations(t *testing.T) { value: txVal, }, { name: "ok redeem swap status redeemed", - bn: 97, - swap: tSwap(97, initLocktime, swapVal, secret, dexeth.SSRedeemed, &initParticipantAddr), + swap: tSwap(100, initLocktime, swapVal, secret, dexeth.SSRedeemed, &initParticipantAddr), value: 0, wantConfs: 1, redeem: true, - }, { - 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), - value: 0, - wantErr: true, - redeem: true, - }, { - name: "error getting swap", - swapErr: errors.New(""), - 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), @@ -334,7 +299,7 @@ func TestConfirmations(t *testing.T) { node := &testNode{ swp: test.swap, swpErr: test.swapErr, - blkNum: test.bn, + blkNum: blockNumber, blkNumErr: test.bnErr, } eth := &AssetBackend{ @@ -343,11 +308,15 @@ func TestConfirmations(t *testing.T) { baseLogger: tLogger, }, contractAddr: *contractAddr, - initTxSize: uint32(dexeth.InitGas(1, 0)), + initTxSize: uint32(dexeth.InitGas(1, version)), atomize: dexeth.WeiToGwei, } - swapData := dexeth.EncodeContractData(0, secretHash) + swapData := dexeth.EncodeContractData(version, secretHash) + + if test.swap != nil { + node.rcpt = &types.Receipt{BlockNumber: big.NewInt(int64(test.swap.BlockHeight))} + } var confirmer Confirmer var err error diff --git a/server/asset/eth/eth.go b/server/asset/eth/eth.go index 99d875787c..169f97cd37 100644 --- a/server/asset/eth/eth.go +++ b/server/asset/eth/eth.go @@ -52,15 +52,15 @@ func networkToken(assetID uint32, net dex.Network) (token *registeredToken, netT return } -func registerToken(assetID uint32, ver uint32) { +func registerToken(assetID uint32, contractVer uint32) { token, exists := dexeth.Tokens[assetID] if !exists { panic(fmt.Sprintf("no token constructor for asset ID %d", assetID)) } drv := &TokenDriver{ driverBase: driverBase{ - version: ver, - unitInfo: token.UnitInfo, + serverVersion: version, + unitInfo: token.UnitInfo, }, token: token.Token, } @@ -68,25 +68,26 @@ func registerToken(assetID uint32, ver uint32) { registeredTokens[assetID] = ®isteredToken{ Token: token, drv: drv, - ver: ver, + ver: contractVer, } } func init() { asset.Register(BipID, &Driver{ driverBase: driverBase{ - version: version, - unitInfo: dexeth.UnitInfo, + serverVersion: version, + unitInfo: dexeth.UnitInfo, }, }) - registerToken(testTokenID, 0) + registerToken(testTokenID, tokenContractVersion) } const ( - BipID = 60 - ethContractVersion = 0 - version = 0 + BipID = 60 + ethContractVersion = 1 + tokenContractVersion = 1 + version = 1 // The blockPollInterval is the delay between calls to bestBlockHash to // check for new blocks. blockPollInterval = time.Second @@ -104,13 +105,13 @@ var ( ) type driverBase struct { - unitInfo dex.UnitInfo - version uint32 + unitInfo dex.UnitInfo + serverVersion uint32 } // Version returns the Backend implementation's version number. func (d *driverBase) Version() uint32 { - return d.version + return d.serverVersion } // DecodeCoinID creates a human-readable representation of a coin ID for @@ -164,9 +165,9 @@ type ethFetcher interface { suggestGasTipCap(ctx context.Context) (*big.Int, error) syncProgress(ctx context.Context) (*ethereum.SyncProgress, error) transaction(ctx context.Context, hash common.Hash) (tx *types.Transaction, isMempool bool, err error) + receipt(context.Context, common.Hash) (*types.Receipt, error) // token- and asset-specific methods loadToken(ctx context.Context, assetID uint32) error - swap(ctx context.Context, assetID uint32, secretHash [32]byte) (*dexeth.SwapState, error) accountBalance(ctx context.Context, assetID uint32, addr common.Address) (*big.Int, error) } @@ -241,9 +242,9 @@ func unconnectedETH(logger dex.Logger, net dex.Network) (*ETHBackend, error) { // least for transitory periods when updating the contract, and // possibly a random contract setup, and so this section will need to // change to support multiple contracts. - contractAddr, exists := dexeth.ContractAddresses[0][net] + contractAddr, exists := dexeth.ContractAddresses[ethContractVersion][net] if !exists || contractAddr == (common.Address{}) { - return nil, fmt.Errorf("no eth contract for version 0, net %s", net) + return nil, fmt.Errorf("no eth contract for version %d, net %s", ethContractVersion, net) } return ÐBackend{&AssetBackend{ baseBackend: &baseBackend{ @@ -254,8 +255,8 @@ func unconnectedETH(logger dex.Logger, net dex.Network) (*ETHBackend, error) { log: logger.SubLogger("ETH"), contractAddr: contractAddr, blockChans: make(map[chan *asset.BlockUpdate]struct{}), - initTxSize: uint32(dexeth.InitGas(1, ethContractVersion)), - redeemSize: dexeth.RedeemGas(1, ethContractVersion), + initTxSize: uint32(dexeth.InitGas(1, 0)), + redeemSize: dexeth.RedeemGas(1, 0), assetID: BipID, atomize: dexeth.WeiToGwei, }}, nil @@ -528,7 +529,7 @@ func (be *AssetBackend) Contract(coinID, contractData []byte) (*asset.Contract, // counterparty address, and locktime. The supported version is enforced. sc, err := be.newSwapCoin(coinID, contractData) if err != nil { - return nil, fmt.Errorf("unable to create coiner: %w", err) + return nil, err } // Confirmations performs some extra swap status checks if the the tx is @@ -540,11 +541,12 @@ func (be *AssetBackend) Contract(coinID, contractData []byte) (*asset.Contract, } return &asset.Contract{ Coin: sc, - SwapAddress: sc.init.Participant.String(), + SwapAddress: sc.swap.To, + FromAccount: sc.swap.From, ContractData: contractData, SecretHash: sc.secretHash[:], TxData: sc.serializedTx, - LockTime: sc.init.LockTime, + LockTime: time.Unix(int64(sc.swap.LockTime), 0), }, nil } @@ -755,3 +757,28 @@ 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 a304f2d61b..333131dcff 100644 --- a/server/asset/eth/eth_test.go +++ b/server/asset/eth/eth_test.go @@ -21,6 +21,7 @@ import ( "decred.org/dcrdex/dex/calc" "decred.org/dcrdex/dex/encode" dexeth "decred.org/dcrdex/dex/networks/eth" + swapv1 "decred.org/dcrdex/dex/networks/eth/contracts/v1" "decred.org/dcrdex/server/asset" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" @@ -30,56 +31,61 @@ import ( const initLocktime = 1632112916 var ( - _ ethFetcher = (*testNode)(nil) - tLogger = dex.StdOutLogger("ETHTEST", dex.LevelTrace) - tCtx context.Context - initCalldata = mustParseHex("a8793f94000000000000000000000000000" + - "0000000000000000000000000000000000020000000000000000000000000000000000" + - "0000000000000000000000000000002000000000000000000000000000000000000000" + - "00000000000000000614811148b3e4acc53b664f9cf6fcac0adcd328e95d62ba1f4379" + - "650ae3e1460a0f9d1a1000000000000000000000000345853e21b1d475582e71cc2691" + - "24ed5e2dd342200000000000000000000000000000000000000000000000022b1c8c12" + - "27a0000000000000000000000000000000000000000000000000000000000006148111" + - "4ebdc4c31b88d0c8f4d644591a8e00e92b607f920ad8050deb7c7469767d9c56100000" + - "0000000000000000000345853e21b1d475582e71cc269124ed5e2dd342200000000000" + - "000000000000000000000000000000000000022b1c8c1227a0000") - /* initCallData parses to: - [ETHSwapInitiation { - RefundTimestamp: 1632112916 - SecretHash: 8b3e4acc53b664f9cf6fcac0adcd328e95d62ba1f4379650ae3e1460a0f9d1a1 - Value: 5e9 gwei - Participant: 0x345853e21b1d475582e71cc269124ed5e2dd3422 - }, - ETHSwapInitiation { - RefundTimestamp: 1632112916 - SecretHash: ebdc4c31b88d0c8f4d644591a8e00e92b607f920ad8050deb7c7469767d9c561 - Value: 5e9 gwei - Participant: 0x345853e21b1d475582e71cc269124ed5e2dd3422 - }] - */ + _ ethFetcher = (*testNode)(nil) + tLogger = dex.StdOutLogger("ETHTEST", dex.LevelTrace) + tCtx context.Context + tSwap1 = &swapv1.ETHSwapContract{ + SecretHash: bytesToArray([]byte("1")), + Initiator: common.BytesToAddress([]byte("initiator1")), + RefundTimestamp: 1, + Participant: common.BytesToAddress([]byte("participant1")), + Value: 1, + } + tSwap2 = &swapv1.ETHSwapContract{ + 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") initSecretHashA = mustParseHex("8b3e4acc53b664f9cf6fcac0adcd328e95d62ba1f4379650ae3e1460a0f9d1a1") initSecretHashB = mustParseHex("ebdc4c31b88d0c8f4d644591a8e00e92b607f920ad8050deb7c7469767d9c561") initParticipantAddr = common.HexToAddress("345853e21b1d475582E71cC269124eD5e2dD3422") - 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") + tRedeem1 = &swapv1.ETHSwapRedemption{ + C: *tSwap1, + Secret: bytesToArray([]byte("1")), + } + tRedeem2 = &swapv1.ETHSwapRedemption{ + C: *tSwap2, + Secret: bytesToArray([]byte("2")), + } + // redeemCalldata encodes [tRedeem1, tRedeem2] + redeemCalldata = mustParseHex("428b16e100000000000000000000000000000000000" + + "0000000000000000000000000002000000000000000000000000000000000000000000" + + "0000000000000000000000231000000000000000000000000000000000000000000000" + + "0000000000000000000000000000000000000000000000000000000000000696e69746" + + "961746f723100000000000000000000000000000000000000000000000000000000000" + + "0000100000000000000000000000000000000000000007061727469636970616e74310" + + "0000000000000000000000000000000000000000000000000000000000000013100000" + + "0000000000000000000000000000000000000000000000000000000003200000000000" + + "0000000000000000000000000000000000000000000000000000000000000000000000" + + "0000000000000000000000000696e69746961746f72320000000000000000000000000" + + "0000000000000000000000000000000000000020000000000000000000000000000000" + + "0000000007061727469636970616e74320000000000000000000000000000000000000" + + "0000000000000000000000000023200000000000000000000000000000000000000000" + + "000000000000000000000") ) func mustParseHex(s string) []byte { @@ -107,6 +113,8 @@ type testNode struct { tx *types.Transaction txIsMempool bool txErr error + rcpt *types.Receipt + rcptErr error acctBal *big.Int acctBalErr error } @@ -141,14 +149,18 @@ func (n *testNode) suggestGasTipCap(ctx context.Context) (*big.Int, error) { return n.suggGasTipCap, n.suggGasTipCapErr } -func (n *testNode) swap(ctx context.Context, assetID uint32, secretHash [32]byte) (*dexeth.SwapState, error) { - return n.swp, n.swpErr +func (n *testNode) status(ctx context.Context, assetID uint32, contract *dex.SwapContractDetails) (dexeth.SwapStep, [32]byte, uint32, error) { + return n.swp.State, n.swp.Secret, uint32(n.swp.BlockHeight), n.swpErr } func (n *testNode) transaction(ctx context.Context, hash 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) accountBalance(ctx context.Context, assetID uint32, addr common.Address) (*big.Int, error) { return n.acctBal, n.acctBalErr } @@ -185,7 +197,10 @@ func TestMain(m *testing.M) { tCtx, shutdown = context.WithCancel(context.Background()) doIt := func() int { defer shutdown() + dexeth.ContractAddresses[0][dex.Simnet] = common.BytesToAddress(encode.RandomBytes(20)) + dexeth.ContractAddresses[1][dex.Simnet] = common.BytesToAddress(encode.RandomBytes(20)) dexeth.Tokens[testTokenID].NetTokens[dex.Simnet].SwapContracts[0].Address = common.BytesToAddress(encode.RandomBytes(20)) + dexeth.Tokens[testTokenID].NetTokens[dex.Simnet].SwapContracts[1].Address = common.BytesToAddress(encode.RandomBytes(20)) return m.Run() } os.Exit(doIt()) @@ -440,8 +455,8 @@ func TestContract(t *testing.T) { const swapVal = 25e8 const txVal = 5e9 var secret, secretHash [32]byte - copy(secret[:], redeemSecretB) - copy(secretHash[:], redeemSecretHashB) + copy(secret[:], tRedeem2.Secret[:]) + copy(secretHash[:], tRedeem2.C.SecretHash[:]) tests := []struct { name string coinID []byte @@ -463,13 +478,6 @@ func TestContract(t *testing.T) { 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(0, secretHash), - coinID: txHash[:], - swapErr: errors.New(""), - wantErr: true, }} for _, test := range tests { eth, node := tNewBackend(BipID) @@ -479,6 +487,10 @@ func TestContract(t *testing.T) { node.swpErr = test.swapErr eth.contractAddr = *contractAddr + if test.swap != nil { + node.rcpt = &types.Receipt{BlockNumber: big.NewInt(int64(test.swap.BlockHeight))} + } + contractData := dexeth.EncodeContractData(0, secretHash) // matches initCalldata contract, err := eth.Contract(test.coinID, contractData) if test.wantErr { @@ -490,8 +502,8 @@ func TestContract(t *testing.T) { if err != nil { t.Fatalf("unexpected error for test %q: %v", test.name, err) } - if contract.SwapAddress != initParticipantAddr.String() || - contract.LockTime.Unix() != initLocktime { + if contract.SwapAddress != tRedeem2.C.Participant.String() || + contract.LockTime.Unix() != int64(tRedeem2.C.RefundTimestamp) { t.Fatalf("returns do not match expected for test %q", test.name) } } @@ -557,8 +569,8 @@ func TestRedemption(t *testing.T) { copy(receiverAddr[:], encode.RandomBytes(20)) copy(contractAddr[:], encode.RandomBytes(20)) var secret, secretHash, txHash [32]byte - copy(secret[:], redeemSecretB) - copy(secretHash[:], redeemSecretHashB) + copy(secret[:], tRedeem2.Secret[:]) + copy(secretHash[:], tRedeem2.C.SecretHash[:]) copy(txHash[:], encode.RandomBytes(32)) const gasPrice = 30 const gasTipCap = 2 @@ -573,22 +585,15 @@ func TestRedemption(t *testing.T) { }{{ name: "ok", tx: tTx(gasPrice, gasTipCap, 0, contractAddr, redeemCalldata), - contractID: dexeth.EncodeContractData(0, secretHash), + contractID: dexeth.EncodeContractData(version, 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: dexeth.EncodeContractData(0, secretHash), + contractID: dexeth.EncodeContractData(version, secretHash), coinID: txHash[1:], wantErr: true, - }, { - name: "confirmations error, swap wrong state", - tx: tTx(gasPrice, gasTipCap, 0, contractAddr, redeemCalldata), - contractID: dexeth.EncodeContractData(0, secretHash), - swp: tSwap(0, 0, 0, secret, dexeth.SSRefunded, receiverAddr), - coinID: txHash[:], - wantErr: true, }, { name: "validate redeem error", tx: tTx(gasPrice, gasTipCap, 0, contractAddr, redeemCalldata), @@ -604,6 +609,7 @@ func TestRedemption(t *testing.T) { node.txErr = test.txErr node.swp = test.swp node.swpErr = test.swpErr + node.rcpt = &types.Receipt{BlockNumber: new(big.Int)} eth.contractAddr = *contractAddr _, err := eth.Redemption(test.coinID, nil, test.contractID) @@ -682,14 +688,16 @@ func testValidateContract(t *testing.T, assetID uint32) { wantErr bool }{{ name: "ok", + ver: 1, secretHash: make([]byte, dexeth.SecretHashSize), }, { name: "wrong size", + ver: 1, secretHash: make([]byte, dexeth.SecretHashSize-1), wantErr: true, }, { name: "wrong version", - ver: 1, + ver: 0, secretHash: make([]byte, dexeth.SecretHashSize), wantErr: true, }} @@ -921,3 +929,26 @@ func TestValidateSignature(t *testing.T) { } } } + +func bytesToArray(b []byte) (a [32]byte) { + copy(a[:], b) + return +} + +// func TestGenerateV1RedeemData(t *testing.T) { +// redeemData, err := dexeth.ABIs[1].Pack("redeem", []swapv1.ETHSwapRedemption{*tRedeem1, *tRedeem2}) +// if err != nil { +// t.Fatalf("error packing redeem data: %v", err) +// } +// fmt.Println("redeem data:", hex.EncodeToString(redeemData)) +// t.Fatalf("") +// } + +// func TestGenerateV1ContractData(t *testing.T) { +// swapData, err := dexeth.ABIs[1].Pack("initiate", []swapv1.ETHSwapContract{*tSwap1, *tSwap2}) +// if err != nil { +// t.Fatalf("error packing swap data: %v", err) +// } +// fmt.Println("swap data:", hex.EncodeToString(swapData)) +// t.Fatalf("") +// } diff --git a/server/asset/eth/rpcclient.go b/server/asset/eth/rpcclient.go index 873ab2adfd..213c7af6d1 100644 --- a/server/asset/eth/rpcclient.go +++ b/server/asset/eth/rpcclient.go @@ -12,7 +12,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" "github.com/ethereum/go-ethereum/common/hexutil" @@ -65,20 +65,20 @@ func (c *rpcclient) connect(ctx context.Context) error { } c.ec = ethclient.NewClient(client) - netAddrs, found := dexeth.ContractAddresses[ethContractVersion] + netAddrs, found := dexeth.ContractAddresses[0] if !found { - return fmt.Errorf("no contract address for eth version %d", ethContractVersion) + return fmt.Errorf("no contract address for eth version %d", 0) } contractAddr, found := netAddrs[c.net] if !found { - return fmt.Errorf("no contract address for eth version %d on %s", ethContractVersion, c.net) + return fmt.Errorf("no contract address for eth version %d on %s", 0, c.net) } - es, err := swapv0.NewETHSwap(contractAddr, c.ec) + es, err := swapv1.NewETHSwap(contractAddr, c.ec) if err != nil { return fmt.Errorf("unable to find swap contract: %v", err) } - c.swapContract = &swapSourceV0{es} + c.swapContract = &swapSourceV1{es} c.caller = client return nil } @@ -139,12 +139,14 @@ func (c *rpcclient) blockNumber(ctx context.Context) (uint64, error) { } // swap gets a swap keyed by secretHash in the contract. -func (c *rpcclient) swap(ctx context.Context, assetID uint32, secretHash [32]byte) (state *dexeth.SwapState, err error) { +func (c *rpcclient) status(ctx context.Context, assetID uint32, deets *dex.SwapContractDetails) (step dexeth.SwapStep, secret [32]byte, blockNumber uint32, err error) { + var secretHash [32]byte + copy(secretHash[:], deets.SecretHash) if assetID == BipID { - return c.swapContract.Swap(ctx, secretHash) + return c.swapContract.Status(ctx, deets) } - return state, c.withTokener(assetID, func(tkn *tokener) error { - state, err = tkn.Swap(ctx, secretHash) + return step, secret, blockNumber, c.withTokener(assetID, func(tkn *tokener) error { + step, secret, blockNumber, err = tkn.Status(ctx, deets) return err }) } @@ -155,6 +157,10 @@ func (c *rpcclient) transaction(ctx context.Context, hash common.Hash) (tx *type return c.ec.TransactionByHash(ctx, hash) } +func (c *rpcclient) receipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) { + return c.ec.TransactionReceipt(ctx, txHash) +} + // accountBalance gets the account balance, including the effects of known // unmined transactions. func (c *rpcclient) accountBalance(ctx context.Context, assetID uint32, addr common.Address) (*big.Int, error) { diff --git a/server/asset/eth/rpcclient_harness_test.go b/server/asset/eth/rpcclient_harness_test.go index ad800e939d..9c697bef60 100644 --- a/server/asset/eth/rpcclient_harness_test.go +++ b/server/asset/eth/rpcclient_harness_test.go @@ -105,10 +105,14 @@ func TestSuggestGasTipCap(t *testing.T) { } } -func TestSwap(t *testing.T) { - var secretHash [32]byte - copy(secretHash[:], encode.RandomBytes(32)) - _, err := ethClient.swap(ctx, BipID, secretHash) +func TestStatus(t *testing.T) { + _, _, _, err := ethClient.status(ctx, BipID, &dex.SwapContractDetails{ + To: common.Address{}.String(), + From: common.Address{}.String(), + Value: 1, + SecretHash: encode.RandomBytes(32), + LockTime: 1, + }) if err != nil { t.Fatal(err) } diff --git a/server/asset/eth/tokener.go b/server/asset/eth/tokener.go index 895be5904b..a3e552516f 100644 --- a/server/asset/eth/tokener.go +++ b/server/asset/eth/tokener.go @@ -12,16 +12,16 @@ 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" ) // swapContract is a generic source of swap contract data. type swapContract interface { - Swap(context.Context, [32]byte) (*dexeth.SwapState, error) + Status(context.Context, *dex.SwapContractDetails) (step dexeth.SwapStep, secret [32]byte, blockNum uint32, err error) } // erc2Contract exposes methods of a token's ERC20 contract. @@ -44,11 +44,11 @@ func newTokener(ctx context.Context, assetID uint32, net dex.Network, be bind.Co return nil, err } - if token.ver != 0 { + if token.ver != 1 { return nil, fmt.Errorf("only version 0 contracts supported") } - es, err := erc20v0.NewERC20Swap(swapContract.Address, be) + es, err := erc20v1.NewERC20Swap(swapContract.Address, be) if err != nil { return nil, err } @@ -71,7 +71,7 @@ func newTokener(ctx context.Context, assetID uint32, net dex.Network, be bind.Co tkn := &tokener{ registeredToken: token, - swapContract: &swapSourceV0{es}, + swapContract: &swapSourceV1{es}, erc20Contract: erc20, contractAddr: swapContract.Address, tokenAddr: netToken.Address, @@ -92,13 +92,13 @@ 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 { - inits, err := dexeth.ParseInitiateData(txData, t.ver) + contracts, err := dexeth.ParseInitiateDataV1(txData) if err != nil { return nil } v := new(big.Int) - for _, init := range inits { - v.Add(v, init.Value) + for _, c := range contracts { + v.Add(v, dexeth.GweiToWei(c.Value)) } return v } @@ -108,25 +108,25 @@ func (t *tokener) balanceOf(ctx context.Context, addr common.Address) (*big.Int, return t.BalanceOf(readOnlyCallOpts(ctx, false), addr) } -// 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) +// swapContractV1 represents a version 0 swap contract for ETH or a token. +type swapContractV1 interface { + State(*bind.CallOpts, swapv1.ETHSwapContract) (swapv1.ETHSwapRecord, error) } -// swapSourceV0 wraps a swapContractV0 and translates the swap data to satisfy +// swapSourceV1 wraps a swapContractV1 and translates the swap data to satisfy // swapSource. -type swapSourceV0 struct { - contract swapContractV0 // *swapv0.ETHSwap or *erc20v0.ERCSwap +type swapSourceV1 struct { + contract swapContractV1 // *swapv1.ETHSwap or *erc20v1.ERCSwap } -// Swap translates the version 0 swap data to the more general SwapState to +// Status translates the version 0 swap data to the more general SwapState to // satisfy the swapSource interface. -func (s *swapSourceV0) Swap(ctx context.Context, secretHash [32]byte) (*dexeth.SwapState, error) { - state, err := s.contract.Swap(readOnlyCallOpts(ctx, true), secretHash) +func (s *swapSourceV1) Status(ctx context.Context, deets *dex.SwapContractDetails) (step dexeth.SwapStep, secret [32]byte, blockNum uint32, err error) { + rec, err := s.contract.State(readOnlyCallOpts(ctx, true), dexeth.SwapToV1(deets)) if err != nil { - return nil, fmt.Errorf("Swap error: %w", err) + return } - return dexeth.SwapStateFromV0(&state), nil + return dexeth.SwapStep(rec.State), rec.Secret, uint32(rec.BlockNumber.Uint64()), nil } // readOnlyCallOpts is the CallOpts used for read-only contract method calls. diff --git a/server/swap/swap.go b/server/swap/swap.go index 1b872a190f..92dad5009e 100644 --- a/server/swap/swap.go +++ b/server/swap/swap.go @@ -1505,7 +1505,7 @@ func (s *Swapper) processInit(msg *msgjson.Message, params *msgjson.Init, stepIn stepInfo.match.ID(), actor, params.CoinID, params.Contract, err) actor.status.mtx.RUnlock() s.respondError(msg.ID, actor.user, msgjson.ContractError, - "redemption error") + "init error") return wait.DontTryAgain } @@ -1541,6 +1541,12 @@ func (s *Swapper) processInit(msg *msgjson.Message, params *msgjson.Init, stepIn contract.SwapAddress, counterParty.order.Trade().SwapAddress())) return wait.DontTryAgain } + if contract.FromAccount != "" && contract.FromAccount != actor.order.Trade().FromAccount() { + s.respondError(msg.ID, actor.user, msgjson.ContractError, + fmt.Sprintf("incorrect initiator. expected %s. got %s", + actor.order.Trade().SwapAddress(), contract.SwapAddress)) + return wait.DontTryAgain + } if contract.Value() != stepInfo.checkVal { s.respondError(msg.ID, actor.user, msgjson.ContractError, fmt.Sprintf("contract error. expected contract value to be %d, got %d", stepInfo.checkVal, contract.Value())) @@ -1559,7 +1565,7 @@ func (s *Swapper) processInit(msg *msgjson.Message, params *msgjson.Init, stepIn } if contract.LockTime.Before(reqLockTime) { s.respondError(msg.ID, actor.user, msgjson.ContractError, - fmt.Sprintf("contract error. expected lock time >= %s, got %s", reqLockTime, contract.LockTime)) + fmt.Sprintf("contract error. expected lock time >= %s, got %s", reqLockTime.UTC(), contract.LockTime.UTC())) return wait.DontTryAgain } else if remain := time.Until(contract.LockTime); remain < 0 { s.respondError(msg.ID, actor.user, msgjson.ContractError,