diff --git a/client/asset/eth/eth.go b/client/asset/eth/eth.go index c4db7307fc..1d1b9d8cc3 100644 --- a/client/asset/eth/eth.go +++ b/client/asset/eth/eth.go @@ -145,7 +145,7 @@ type ethFetcher interface { initiate(ctx context.Context, contracts []*asset.Contract, maxFeeRate uint64, contractVer uint32) (*types.Transaction, error) shutdown() syncProgress() ethereum.SyncProgress - redeem(txOpts *bind.TransactOpts, redemptions []*asset.Redemption, contractVer uint32) (*types.Transaction, error) + redeem(ctx context.Context, redemptions []*asset.Redemption, maxFeeRate uint64, contractVer uint32) (*types.Transaction, error) refund(txOpts *bind.TransactOpts, secretHash [32]byte, contractVer uint32) (*types.Transaction, error) swap(ctx context.Context, secretHash [32]byte, contractVer uint32) (*dexeth.SwapState, error) lock(ctx context.Context) error @@ -672,8 +672,38 @@ func (eth *ExchangeWallet) Swap(swaps *asset.Swaps) ([]asset.Receipt, asset.Coin // Redeem sends the redemption transaction, which may contain more than one // redemption. -func (*ExchangeWallet) Redeem(form *asset.RedeemForm) ([]dex.Bytes, asset.Coin, uint64, error) { - return nil, nil, 0, asset.ErrNotImplemented +func (eth *ExchangeWallet) Redeem(form *asset.RedeemForm) ([]dex.Bytes, asset.Coin, uint64, error) { + fail := func(err error) ([]dex.Bytes, asset.Coin, uint64, error) { + return nil, nil, 0, err + } + + if len(form.Redemptions) == 0 { + return fail(errors.New("Redeem: must be called with at least 1 redemption")) + } + + inputs := make([]dex.Bytes, 0, len(form.Redemptions)) + var redeemedValue uint64 + for _, redemption := range form.Redemptions { + var secretHash [32]byte + copy(secretHash[:], redemption.Spends.SecretHash) + swapData, err := eth.node.swap(eth.ctx, secretHash, form.AssetVersion) + if err != nil { + return nil, nil, 0, fmt.Errorf("Redeem: error finding swap state: %w", err) + } + redeemedValue += swapData.Value + inputs = append(inputs, redemption.Spends.Coin.ID()) + } + outputCoin := eth.createAmountCoin(redeemedValue) + fundsRequired := dexeth.RedeemGas(len(form.Redemptions), form.AssetVersion) * form.FeeSuggestion + + // TODO: make sure the amount we locked for redemption is enough to cover the gas + // fees. + _, err := eth.node.redeem(eth.ctx, form.Redemptions, form.FeeSuggestion, form.AssetVersion) + if err != nil { + return fail(fmt.Errorf("Redeem: redeem error: %w", err)) + } + + return inputs, outputCoin, fundsRequired, nil } // SignMessage signs the message with the private key associated with the diff --git a/client/asset/eth/eth_test.go b/client/asset/eth/eth_test.go index b2e74aa5af..da55344f24 100644 --- a/client/asset/eth/eth_test.go +++ b/client/asset/eth/eth_test.go @@ -61,6 +61,7 @@ type testNode struct { swapMap map[[32]byte]*dexeth.SwapState swapErr error initErr error + redeemErr error nonce uint64 } @@ -134,10 +135,32 @@ func (n *testNode) initiate(ctx context.Context, contracts []*asset.Contract, ma Nonce: n.nonce, }), nil } -func (n *testNode) redeem(opts *bind.TransactOpts, redemptions []*asset.Redemption, contractVer uint32) (*types.Transaction, error) { - return nil, nil + +func (n *testNode) redeem(ctx context.Context, redemptions []*asset.Redemption, maxFeeRate uint64, contractVer uint32) (*types.Transaction, error) { + if n.redeemErr != nil { + return nil, n.redeemErr + } + /*baseTx := &types.DynamicFeeTx{ + Nonce: n.nonce, + GasFeeCap: opts.GasFeeCap, + GasTipCap: opts.GasTipCap, + Gas: opts.GasLimit, + Value: opts.Value, + Data: []byte{}, + }*/ + //tx := types.NewTx(baseTx) + n.nonce++ + // n.lastRedemption = redeemTx{ + // redemptions: redemptions, + // hash: tx.Hash(), + // opts: opts, + // tx: tx, + // } + return types.NewTx(&types.DynamicFeeTx{ + Nonce: n.nonce, + }), nil } -func (n *testNode) refund(opts *bind.TransactOpts, secretHash [32]byte, serverVer uint32) (*types.Transaction, error) { +func (n *testNode) refund(txOpts *bind.TransactOpts, secretHash [32]byte, contractVer uint32) (*types.Transaction, error) { return nil, nil } func (n *testNode) swap(ctx context.Context, secretHash [32]byte, contractVer uint32) (*dexeth.SwapState, error) { @@ -154,7 +177,6 @@ func (n *testNode) swap(ctx context.Context, secretHash [32]byte, contractVer ui } return swap, nil } - func (n *testNode) signData(addr common.Address, data []byte) ([]byte, error) { if n.signDataErr != nil { return nil, n.signDataErr @@ -1138,6 +1160,177 @@ func TestPreRedeem(t *testing.T) { } } +func TestRedeem(t *testing.T) { + node := &testNode{ + swapVers: map[uint32]struct{}{ + 0: {}, + }, + swapMap: make(map[[32]byte]*dexeth.SwapState), + } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + eth := &ExchangeWallet{ + node: node, + ctx: ctx, + log: tLogger, + } + addSwapToSwapMap := func(secretHash [32]byte, value uint64, step dexeth.SwapStep) { + swap := dexeth.SwapState{ + BlockHeight: 1, + LockTime: time.Now(), + Initiator: common.HexToAddress("0x2b84C791b79Ee37De042AD2ffF1A253c3ce9bc27"), + Participant: common.HexToAddress("B6De8BB5ed28E6bE6d671975cad20C03931bE981"), + Value: value, + State: step, + } + node.swapMap[secretHash] = &swap + } + + numSecrets := 3 + secrets := make([][32]byte, 0, numSecrets) + secretHashes := make([][32]byte, 0, numSecrets) + for i := 0; i < numSecrets; i++ { + var secret [32]byte + copy(secret[:], encode.RandomBytes(32)) + secretHash := sha256.Sum256(secret[:]) + secrets = append(secrets, secret) + secretHashes = append(secretHashes, secretHash) + } + + addSwapToSwapMap(secretHashes[0], 1e9, dexeth.SSInitiated) + addSwapToSwapMap(secretHashes[1], 1e9, dexeth.SSInitiated) + + tests := []struct { + name string + form asset.RedeemForm + redeemErr error + expectError bool + }{ + { + name: "ok", + expectError: false, + form: asset.RedeemForm{ + Redemptions: []*asset.Redemption{ + { + Spends: &asset.AuditInfo{ + SecretHash: secretHashes[0][:], + Coin: &coin{ + id: encode.RandomBytes(32), + }, + }, + Secret: secrets[0][:], + }, + { + Spends: &asset.AuditInfo{ + SecretHash: secretHashes[1][:], + Coin: &coin{ + id: encode.RandomBytes(32), + }, + }, + Secret: secrets[1][:], + }, + }, + FeeSuggestion: 100, + AssetVersion: 0, + }, + }, + { + name: "redeem error", + redeemErr: errors.New(""), + expectError: true, + form: asset.RedeemForm{ + Redemptions: []*asset.Redemption{ + { + Spends: &asset.AuditInfo{ + SecretHash: secretHashes[0][:], + Coin: &coin{ + id: encode.RandomBytes(32), + }, + }, + Secret: secrets[0][:], + }, + }, + FeeSuggestion: 200, + AssetVersion: 0, + }, + }, + { + name: "swap not found in contract", + expectError: true, + form: asset.RedeemForm{ + Redemptions: []*asset.Redemption{ + { + Spends: &asset.AuditInfo{ + SecretHash: secretHashes[2][:], + Coin: &coin{ + id: encode.RandomBytes(32), + }, + }, + Secret: secrets[2][:], + }, + }, + FeeSuggestion: 100, + AssetVersion: 0, + }, + }, + { + name: "empty redemptions slice error", + expectError: true, + form: asset.RedeemForm{ + Redemptions: []*asset.Redemption{}, + FeeSuggestion: 100, + AssetVersion: 0, + }, + }, + } + + for _, test := range tests { + node.redeemErr = test.redeemErr + + ins, out, fees, err := eth.Redeem(&test.form) + if test.expectError { + if err == nil { + t.Fatalf("%v: expected error", test.name) + } + continue + } + if err != nil { + t.Fatalf("%v: unexpected error: %v", test.name, err) + } + + if len(ins) != len(test.form.Redemptions) { + t.Fatalf("%v: expected %d inputs but got %d", + test.name, len(test.form.Redemptions), len(ins)) + } + + // Check fees returned from Redeem are as expected + expectedGas := dexeth.RedeemGas(len(test.form.Redemptions), 0) + expectedFees := expectedGas * test.form.FeeSuggestion + if fees != expectedFees { + t.Fatalf("%v: expected fees %d, but got %d", test.name, expectedFees, fees) + } + + var totalSwapValue uint64 + for i, redemption := range test.form.Redemptions { + coinID := redemption.Spends.Coin.ID() + if !bytes.Equal(coinID, ins[i]) { + t.Fatalf("%v: expected input %x to equal coin id %x", + test.name, coinID, ins[i]) + } + + var secretHash [32]byte + copy(secretHash[:], redemption.Spends.SecretHash) + swap := node.swapMap[secretHash] + totalSwapValue += swap.Value + } + + if out.Value() != totalSwapValue { + t.Fatalf("expected coin value to be %d but got %d", + totalSwapValue, out.Value()) + } + } +} + func TestMaxOrder(t *testing.T) { gases := dexeth.VersionedGases[0] diff --git a/client/asset/eth/nodeclient.go b/client/asset/eth/nodeclient.go index d7ea4ddbcb..15560dd8e7 100644 --- a/client/asset/eth/nodeclient.go +++ b/client/asset/eth/nodeclient.go @@ -377,7 +377,7 @@ func (n *nodeClient) addSignerToOpts(txOpts *bind.TransactOpts) error { // initiate initiates multiple swaps in the same transaction. func (n *nodeClient) initiate(ctx context.Context, contracts []*asset.Contract, maxFeeRate uint64, contractVer uint32) (tx *types.Transaction, err error) { - gas := dexeth.InitGas(len(contracts), 0) + gas := dexeth.InitGas(len(contracts), contractVer) var val uint64 for _, c := range contracts { val += c.Value @@ -417,12 +417,13 @@ func (n *nodeClient) estimateRefundGas(ctx context.Context, secretHash [32]byte, }) } -// redeem redeems a swap contract. The redeemer will be the account at txOpts.From. -// Any on-chain failure, such as this secret not matching the hash, will not cause -// this to error. -func (n *nodeClient) redeem(txOpts *bind.TransactOpts, redemptions []*asset.Redemption, contractVer uint32) (tx *types.Transaction, 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 (n *nodeClient) redeem(ctx context.Context, redemptions []*asset.Redemption, maxFeeRate uint64, contractVer uint32) (tx *types.Transaction, err error) { + gas := dexeth.RedeemGas(len(redemptions), contractVer) + txOpts, _ := n.txOpts(ctx, 0, gas, dexeth.GweiToWei(maxFeeRate)) if err := n.addSignerToOpts(txOpts); err != nil { - return nil, err + return nil, fmt.Errorf("addSignerToOpts error: %w", err) } return tx, n.withcontractor(contractVer, func(c contractor) error { tx, err = c.redeem(txOpts, redemptions) diff --git a/client/asset/eth/nodeclient_harness_test.go b/client/asset/eth/nodeclient_harness_test.go index 924d885593..ac09be2f8d 100644 --- a/client/asset/eth/nodeclient_harness_test.go +++ b/client/asset/eth/nodeclient_harness_test.go @@ -867,9 +867,6 @@ func testRedeem(t *testing.T) { } for _, test := range tests { - numSwaps := len(test.swaps) - txOpts, _ := ethClient.txOpts(ctx, uint64(numSwaps), dexeth.InitGas(numSwaps, 0), nil) - for i := range test.swaps { swap, err := ethClient.swap(ctx, bytesToArray(test.swaps[i].SecretHash), 0) if err != nil { @@ -907,8 +904,6 @@ func testRedeem(t *testing.T) { t.Fatalf("%s: balance error: %v", test.name, err) } baseFee, _, _ := test.redeemerClient.netFeeState(ctx) - txOpts, _ = test.redeemerClient.txOpts(ctx, 0, dexeth.RedeemGas(numSwaps, 0), nil) - for i, redemption := range test.redemptions { expected := test.isRedeemable[i] isRedeemable, err := test.redeemerClient.isRedeemable(bytesToArray(redemption.Spends.SecretHash), bytesToArray(redemption.Secret), 0) @@ -920,7 +915,7 @@ func testRedeem(t *testing.T) { } } - tx, err := test.redeemerClient.redeem(txOpts, test.redemptions, 0) + tx, err := test.redeemerClient.redeem(ctx, test.redemptions, maxFeeRate, 0) if err != nil { t.Fatalf("%s: redeem error: %v", test.name, err) } @@ -1085,8 +1080,7 @@ func testRefund(t *testing.T) { if err := waitForMined(t, time.Second*8, false); err != nil { t.Fatalf("%s: pre-redeem mining error: %v", test.name, err) } - txOpts, _ := participantEthClient.txOpts(ctx, 0, dexeth.RedeemGas(1, 0), nil) - _, err := participantEthClient.redeem(txOpts, []*asset.Redemption{newRedeem(secret, secretHash)}, 0) + _, err := participantEthClient.redeem(ctx, []*asset.Redemption{newRedeem(secret, secretHash)}, maxFeeRate, 0) if err != nil { t.Fatalf("%s: redeem error: %v", test.name, err) } diff --git a/client/asset/interface.go b/client/asset/interface.go index f7e1d4fa12..aaa2e642a1 100644 --- a/client/asset/interface.go +++ b/client/asset/interface.go @@ -335,6 +335,9 @@ type RedeemForm struct { // suggestion in any way, but obviously fees that are too low may result in // the redemption getting stuck in mempool. FeeSuggestion uint64 + // AssetVersion is the swap protocol version, which may indicate a specific + // contract or form of contract. + AssetVersion uint32 } // Order is order details needed for FundOrder. diff --git a/client/core/trade.go b/client/core/trade.go index a40e0927cd..fdec1ea0cc 100644 --- a/client/core/trade.go +++ b/client/core/trade.go @@ -1677,6 +1677,7 @@ func (c *Core) redeemMatchGroup(t *trackedTrade, matches []*matchTracker, errs * coinIDs, outCoin, fees, err := redeemWallet.Redeem(&asset.RedeemForm{ Redemptions: redemptions, FeeSuggestion: feeSuggestion, + AssetVersion: t.metaData.ToVersion, }) // If an error was encountered, fail all of the matches. A failed match will // not run again on during ticks. diff --git a/dex/networks/eth/txdata_test.go b/dex/networks/eth/txdata_test.go index c25238e277..b7abff2810 100644 --- a/dex/networks/eth/txdata_test.go +++ b/dex/networks/eth/txdata_test.go @@ -146,7 +146,7 @@ func TestParseRedeemData(t *testing.T) { } calldata, err := PackRedeemData(redemptions) if err != nil { - t.Fatalf("unale to pack abi: %v", err) + t.Fatalf("unable to pack abi: %v", err) } redeemCallData := mustParseHex("f4fd17f9000000000000000000000000000000000" + "000000000000000000000000000002000000000000000000000000000000000000" +