@agoric/cosmos 0.35.0-u18.4 → 0.35.0-u18a.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/ante/inbound_test.go +2 -2
- package/app/app.go +26 -9
- package/app/upgrade.go +47 -134
- package/daemon/cmd/root.go +48 -15
- package/git-revision.txt +1 -1
- package/go.mod +82 -71
- package/go.sum +200 -167
- package/package.json +2 -2
- package/proto/agoric/swingset/swingset.proto +25 -0
- package/proto/agoric/vbank/vbank.proto +7 -0
- package/types/address_hooks.go +242 -0
- package/types/address_hooks_test.go +221 -0
- package/x/swingset/genesis.go +99 -21
- package/x/swingset/keeper/keeper.go +16 -7
- package/x/swingset/module.go +17 -2
- package/x/swingset/testing/queue.go +8 -0
- package/x/swingset/types/default-params.go +31 -5
- package/x/swingset/types/expected_keepers.go +2 -2
- package/x/swingset/types/msgs.go +34 -12
- package/x/swingset/types/params.go +53 -43
- package/x/swingset/types/params_test.go +75 -9
- package/x/swingset/types/swingset.pb.go +386 -56
- package/x/vbank/README.md +6 -1
- package/x/vbank/keeper/keeper.go +4 -9
- package/x/vbank/keeper/migrations.go +30 -0
- package/x/vbank/module.go +8 -2
- package/x/vbank/types/params.go +43 -2
- package/x/vbank/types/vbank.pb.go +105 -36
- package/x/vbank/vbank_test.go +12 -7
- package/x/vibc/keeper/keeper.go +2 -5
- package/x/vibc/keeper/triggers.go +3 -6
- package/x/vibc/types/receiver.go +11 -5
- package/x/vstorage/testing/queue.go +10 -3
- package/x/vtransfer/ibc_middleware.go +5 -1
- package/x/vtransfer/ibc_middleware_test.go +511 -145
- package/x/vtransfer/keeper/keeper.go +215 -50
- package/x/vtransfer/types/genesis.pb.go +1 -0
- package/x/vtransfer/utils_test.go +111 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package vtransfer_test
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
+
"bytes"
|
|
4
5
|
"context"
|
|
5
6
|
"encoding/json"
|
|
6
7
|
"fmt"
|
|
@@ -11,26 +12,37 @@ import (
|
|
|
11
12
|
|
|
12
13
|
app "github.com/Agoric/agoric-sdk/golang/cosmos/app"
|
|
13
14
|
"github.com/Agoric/agoric-sdk/golang/cosmos/vm"
|
|
15
|
+
"github.com/cosmos/cosmos-sdk/codec"
|
|
14
16
|
"github.com/cosmos/cosmos-sdk/store"
|
|
17
|
+
"github.com/cosmos/cosmos-sdk/testutil/testdata"
|
|
18
|
+
"github.com/iancoleman/orderedmap"
|
|
15
19
|
"github.com/stretchr/testify/suite"
|
|
16
20
|
"github.com/tendermint/tendermint/libs/log"
|
|
17
21
|
dbm "github.com/tendermint/tm-db"
|
|
18
22
|
|
|
23
|
+
"github.com/Agoric/agoric-sdk/golang/cosmos/types"
|
|
19
24
|
swingsettesting "github.com/Agoric/agoric-sdk/golang/cosmos/x/swingset/testing"
|
|
20
25
|
swingsettypes "github.com/Agoric/agoric-sdk/golang/cosmos/x/swingset/types"
|
|
21
26
|
vibckeeper "github.com/Agoric/agoric-sdk/golang/cosmos/x/vibc/keeper"
|
|
27
|
+
vibctypes "github.com/Agoric/agoric-sdk/golang/cosmos/x/vibc/types"
|
|
22
28
|
|
|
23
29
|
"github.com/cosmos/cosmos-sdk/baseapp"
|
|
24
30
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
25
31
|
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
|
|
26
32
|
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
|
|
33
|
+
packetforwardtypes "github.com/cosmos/ibc-apps/middleware/packet-forward-middleware/v6/packetforward/types"
|
|
27
34
|
ibctransfertypes "github.com/cosmos/ibc-go/v6/modules/apps/transfer/types"
|
|
28
35
|
channeltypes "github.com/cosmos/ibc-go/v6/modules/core/04-channel/types"
|
|
36
|
+
ibcexported "github.com/cosmos/ibc-go/v6/modules/core/exported"
|
|
29
37
|
ibctesting "github.com/cosmos/ibc-go/v6/testing"
|
|
30
38
|
"github.com/cosmos/ibc-go/v6/testing/simapp"
|
|
31
39
|
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
|
|
32
40
|
)
|
|
33
41
|
|
|
42
|
+
const (
|
|
43
|
+
StorePacketData = true
|
|
44
|
+
)
|
|
45
|
+
|
|
34
46
|
type IntegrationTestSuite struct {
|
|
35
47
|
suite.Suite
|
|
36
48
|
|
|
@@ -39,17 +51,42 @@ type IntegrationTestSuite struct {
|
|
|
39
51
|
// testing chains used for convenience and readability
|
|
40
52
|
chainA *ibctesting.TestChain
|
|
41
53
|
chainB *ibctesting.TestChain
|
|
54
|
+
chainC *ibctesting.TestChain
|
|
55
|
+
|
|
56
|
+
lastChannelOffset map[int]int
|
|
57
|
+
endpoints map[int]map[int]*ibctesting.Endpoint
|
|
42
58
|
|
|
43
59
|
queryClient ibctransfertypes.QueryClient
|
|
44
60
|
}
|
|
45
61
|
|
|
62
|
+
type TestingAppMaker func() (ibctesting.TestingApp, map[string]json.RawMessage)
|
|
63
|
+
|
|
64
|
+
func TestTransferTestSuite(t *testing.T) {
|
|
65
|
+
s := new(IntegrationTestSuite)
|
|
66
|
+
suite.Run(t, s)
|
|
67
|
+
}
|
|
68
|
+
|
|
46
69
|
// interBlockCacheOpt returns a BaseApp option function that sets the persistent
|
|
47
70
|
// inter-block write-through cache.
|
|
48
71
|
func interBlockCacheOpt() func(*baseapp.BaseApp) {
|
|
49
72
|
return baseapp.SetInterBlockCache(store.NewCommitKVStoreCacheManager())
|
|
50
73
|
}
|
|
51
74
|
|
|
52
|
-
|
|
75
|
+
func (s *IntegrationTestSuite) getEndpoint(a, b int) *ibctesting.Endpoint {
|
|
76
|
+
amap := s.endpoints[a]
|
|
77
|
+
if amap == nil {
|
|
78
|
+
return nil
|
|
79
|
+
}
|
|
80
|
+
return amap[b]
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
func (s *IntegrationTestSuite) cacheEndpoint(a, b int, endpoint *ibctesting.Endpoint) {
|
|
84
|
+
amap := s.endpoints[a]
|
|
85
|
+
if amap == nil {
|
|
86
|
+
amap = make(map[int]*ibctesting.Endpoint)
|
|
87
|
+
}
|
|
88
|
+
amap[b] = endpoint
|
|
89
|
+
}
|
|
53
90
|
|
|
54
91
|
// Each instance has unique IBC genesis state with deterministic
|
|
55
92
|
// client/connection/channel initial sequence numbers
|
|
@@ -61,6 +98,15 @@ func computeSequences(instance int) (clientSeq, connectionSeq, channelSeq int) {
|
|
|
61
98
|
return baseSequence, baseSequence + 10, baseSequence + 50
|
|
62
99
|
}
|
|
63
100
|
|
|
101
|
+
func (s *IntegrationTestSuite) nextChannelOffset(instance int) int {
|
|
102
|
+
offset, ok := s.lastChannelOffset[instance]
|
|
103
|
+
if ok {
|
|
104
|
+
offset += 1
|
|
105
|
+
}
|
|
106
|
+
s.lastChannelOffset[instance] = offset
|
|
107
|
+
return offset
|
|
108
|
+
}
|
|
109
|
+
|
|
64
110
|
func SetupAgoricTestingApp(instance int) TestingAppMaker {
|
|
65
111
|
return func() (ibctesting.TestingApp, map[string]json.RawMessage) {
|
|
66
112
|
db := dbm.NewMemDB()
|
|
@@ -131,17 +177,15 @@ func SetupAgoricTestingApp(instance int) TestingAppMaker {
|
|
|
131
177
|
}
|
|
132
178
|
}
|
|
133
179
|
|
|
134
|
-
|
|
135
|
-
suite.Run(t, new(IntegrationTestSuite))
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// SetupTest initializes an IntegrationTestSuite with two similar chains, a
|
|
180
|
+
// SetupTest initializes an IntegrationTestSuite with three similar chains, a
|
|
139
181
|
// shared coordinator, and a query client that happens to point at chainA.
|
|
140
182
|
func (s *IntegrationTestSuite) SetupTest() {
|
|
183
|
+
s.lastChannelOffset = make(map[int]int)
|
|
184
|
+
s.endpoints = make(map[int]map[int]*ibctesting.Endpoint)
|
|
141
185
|
s.coordinator = ibctesting.NewCoordinator(s.T(), 0)
|
|
142
186
|
|
|
143
187
|
chains := make(map[string]*ibctesting.TestChain)
|
|
144
|
-
for i := 0; i <
|
|
188
|
+
for i := 0; i < 3; i++ {
|
|
145
189
|
ibctesting.DefaultTestingAppInit = SetupAgoricTestingApp(i)
|
|
146
190
|
|
|
147
191
|
chainID := ibctesting.GetChainID(i)
|
|
@@ -182,6 +226,7 @@ func (s *IntegrationTestSuite) SetupTest() {
|
|
|
182
226
|
s.coordinator.Chains = chains
|
|
183
227
|
s.chainA = s.coordinator.GetChain(ibctesting.GetChainID(0))
|
|
184
228
|
s.chainB = s.coordinator.GetChain(ibctesting.GetChainID(1))
|
|
229
|
+
s.chainC = s.coordinator.GetChain(ibctesting.GetChainID(2))
|
|
185
230
|
|
|
186
231
|
agoricApp := s.GetApp(s.chainA)
|
|
187
232
|
|
|
@@ -190,6 +235,10 @@ func (s *IntegrationTestSuite) SetupTest() {
|
|
|
190
235
|
s.queryClient = ibctransfertypes.NewQueryClient(queryHelper)
|
|
191
236
|
}
|
|
192
237
|
|
|
238
|
+
func (s *IntegrationTestSuite) GetChainByIndex(index int) *ibctesting.TestChain {
|
|
239
|
+
return s.coordinator.GetChain(ibctesting.GetChainID(index))
|
|
240
|
+
}
|
|
241
|
+
|
|
193
242
|
func (s *IntegrationTestSuite) GetApp(chain *ibctesting.TestChain) *app.GaiaApp {
|
|
194
243
|
app, ok := chain.App.(*app.GaiaApp)
|
|
195
244
|
if !ok {
|
|
@@ -199,30 +248,53 @@ func (s *IntegrationTestSuite) GetApp(chain *ibctesting.TestChain) *app.GaiaApp
|
|
|
199
248
|
return app
|
|
200
249
|
}
|
|
201
250
|
|
|
202
|
-
func (s *IntegrationTestSuite) NewTransferPath() *ibctesting.Path {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
251
|
+
func (s *IntegrationTestSuite) NewTransferPath(endpointAChainIdx, endpointBChainIdx int) *ibctesting.Path {
|
|
252
|
+
endpointAChain := s.coordinator.GetChain(ibctesting.GetChainID(endpointAChainIdx))
|
|
253
|
+
endpointBChain := s.coordinator.GetChain(ibctesting.GetChainID(endpointBChainIdx))
|
|
254
|
+
|
|
255
|
+
chAOffset := s.nextChannelOffset(endpointAChainIdx)
|
|
256
|
+
chBOffset := s.nextChannelOffset(endpointBChainIdx)
|
|
257
|
+
path := ibctesting.NewPath(endpointAChain, endpointBChain)
|
|
258
|
+
_, _, channelASeq := computeSequences(endpointAChainIdx)
|
|
259
|
+
_, _, channelBSeq := computeSequences(endpointBChainIdx)
|
|
260
|
+
path.EndpointA.ChannelID = fmt.Sprintf("channel-%d", channelASeq+chAOffset)
|
|
261
|
+
path.EndpointB.ChannelID = fmt.Sprintf("channel-%d", channelBSeq+chBOffset)
|
|
208
262
|
path.EndpointA.ChannelConfig.PortID = ibctesting.TransferPort
|
|
209
263
|
path.EndpointB.ChannelConfig.PortID = ibctesting.TransferPort
|
|
210
264
|
path.EndpointA.ChannelConfig.Version = "ics20-1"
|
|
211
265
|
path.EndpointB.ChannelConfig.Version = "ics20-1"
|
|
212
266
|
|
|
213
|
-
s.
|
|
267
|
+
endpoint := s.getEndpoint(endpointAChainIdx, endpointBChainIdx)
|
|
268
|
+
if endpoint == nil {
|
|
269
|
+
s.coordinator.SetupConnections(path)
|
|
270
|
+
s.cacheEndpoint(endpointAChainIdx, endpointBChainIdx, path.EndpointA)
|
|
271
|
+
s.cacheEndpoint(endpointBChainIdx, endpointAChainIdx, path.EndpointB)
|
|
272
|
+
} else {
|
|
273
|
+
path.EndpointA.ClientID = endpoint.ClientID
|
|
274
|
+
path.EndpointA.ConnectionID = endpoint.ConnectionID
|
|
275
|
+
|
|
276
|
+
path.EndpointB.ClientID = endpoint.Counterparty.ClientID
|
|
277
|
+
path.EndpointB.ConnectionID = endpoint.Counterparty.ConnectionID
|
|
278
|
+
}
|
|
279
|
+
s.coordinator.CreateChannels(path)
|
|
214
280
|
|
|
215
|
-
s.coordinator.CommitBlock(
|
|
281
|
+
s.coordinator.CommitBlock(endpointAChain, endpointBChain)
|
|
216
282
|
|
|
217
283
|
return path
|
|
218
284
|
}
|
|
219
285
|
|
|
286
|
+
func (s *IntegrationTestSuite) resetActionQueue(chain *ibctesting.TestChain) {
|
|
287
|
+
err := swingsettesting.ResetActionQueue(s.T(), chain.GetContext(), s.GetApp(chain).SwingSetKeeper)
|
|
288
|
+
s.Require().NoError(err)
|
|
289
|
+
}
|
|
290
|
+
|
|
220
291
|
func (s *IntegrationTestSuite) assertActionQueue(chain *ibctesting.TestChain, expectedRecords []swingsettypes.InboundQueueRecord) {
|
|
221
292
|
actualRecords, err := swingsettesting.GetActionQueueRecords(
|
|
222
293
|
s.T(),
|
|
223
294
|
chain.GetContext(),
|
|
224
295
|
s.GetApp(chain).SwingSetKeeper,
|
|
225
296
|
)
|
|
297
|
+
s.resetActionQueue(chain)
|
|
226
298
|
s.Require().NoError(err)
|
|
227
299
|
|
|
228
300
|
exLen := len(expectedRecords)
|
|
@@ -262,44 +334,86 @@ func (s *IntegrationTestSuite) RegisterBridgeTarget(chain *ibctesting.TestChain,
|
|
|
262
334
|
agdServer := s.GetApp(chain).AgdServer
|
|
263
335
|
defer agdServer.SetControllerContext(chain.GetContext())()
|
|
264
336
|
var reply string
|
|
265
|
-
err :=
|
|
337
|
+
bz, err := json.Marshal(struct {
|
|
338
|
+
Type string
|
|
339
|
+
Target string
|
|
340
|
+
}{"BRIDGE_TARGET_REGISTER", target})
|
|
341
|
+
s.Require().NoError(err)
|
|
342
|
+
err = agdServer.ReceiveMessage(
|
|
266
343
|
&vm.Message{
|
|
267
344
|
Port: agdServer.GetPort("vtransfer"),
|
|
268
|
-
Data:
|
|
345
|
+
Data: string(bz),
|
|
269
346
|
},
|
|
270
347
|
&reply,
|
|
271
348
|
)
|
|
272
349
|
s.Require().NoError(err)
|
|
273
|
-
s.Require().Equal(
|
|
350
|
+
s.Require().Equal("true", reply)
|
|
274
351
|
}
|
|
275
352
|
|
|
276
|
-
func (s *IntegrationTestSuite)
|
|
277
|
-
|
|
353
|
+
func (s *IntegrationTestSuite) TransferFromEndpoint(
|
|
354
|
+
srcContext sdk.Context,
|
|
355
|
+
src *ibctesting.Endpoint,
|
|
278
356
|
data ibctransfertypes.FungibleTokenPacketData,
|
|
279
|
-
|
|
280
|
-
) (channeltypes.Packet, error) {
|
|
357
|
+
) error {
|
|
281
358
|
tokenAmt, ok := sdk.NewIntFromString(data.Amount)
|
|
282
359
|
s.Require().True(ok)
|
|
283
360
|
|
|
284
|
-
timeoutHeight :=
|
|
285
|
-
packet := channeltypes.NewPacket(data.GetBytes(), 0, src.ChannelConfig.PortID, src.ChannelID, dst.ChannelConfig.PortID, dst.ChannelID, timeoutHeight, 0)
|
|
361
|
+
timeoutHeight := src.Counterparty.Chain.GetTimeoutHeight()
|
|
286
362
|
|
|
287
363
|
// send a transfer packet from src
|
|
288
|
-
imt := ibctransfertypes.
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
364
|
+
imt := ibctransfertypes.NewMsgTransfer(
|
|
365
|
+
src.ChannelConfig.PortID,
|
|
366
|
+
src.ChannelID,
|
|
367
|
+
sdk.NewCoin(data.Denom, tokenAmt),
|
|
368
|
+
data.Sender,
|
|
369
|
+
data.Receiver,
|
|
370
|
+
timeoutHeight,
|
|
371
|
+
0,
|
|
372
|
+
data.Memo,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
tk := s.GetApp(src.Chain).TransferKeeper
|
|
376
|
+
_, err := tk.Transfer(srcContext, imt)
|
|
377
|
+
return err
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
func (s *IntegrationTestSuite) prependDenomTrace(sender *ibctesting.Endpoint, trace string) string {
|
|
381
|
+
return fmt.Sprintf("%s/%s/%s", sender.ChannelConfig.PortID, sender.ChannelID, trace)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
func (s *IntegrationTestSuite) overrideSendPacketData(cdc codec.Codec, data []byte, hookedSender string) ([]byte, error) {
|
|
385
|
+
var ftpd ibctransfertypes.FungibleTokenPacketData
|
|
386
|
+
err := json.Unmarshal(data, &ftpd)
|
|
387
|
+
if err != nil {
|
|
388
|
+
return nil, err
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// XXX: This is a hack to get around the fact that `TransferKeeper.Transfer`
|
|
392
|
+
// doesn't understand hooked senders. We need to put the hooked sender back
|
|
393
|
+
// in so that the vtransfer keeper can strip it out as if it had been there
|
|
394
|
+
// all along.
|
|
395
|
+
newFtpd := ftpd
|
|
396
|
+
newFtpd.Sender = hookedSender
|
|
397
|
+
|
|
398
|
+
// Permute the encoded data to ensure that it is different that what the TransferKeeper.Transfer specified.
|
|
399
|
+
if bz := ftpd.GetBytes(); !bytes.Equal(data, bz) {
|
|
400
|
+
return newFtpd.GetBytes(), nil
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
bz, err := cdc.MarshalJSON(&ftpd)
|
|
404
|
+
if err != nil {
|
|
405
|
+
return nil, err
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if !bytes.Equal(data, bz) {
|
|
409
|
+
newBz, err := cdc.MarshalJSON(&newFtpd)
|
|
410
|
+
if err != nil {
|
|
411
|
+
return nil, err
|
|
412
|
+
}
|
|
413
|
+
return newBz, nil
|
|
297
414
|
}
|
|
298
|
-
imr, err := s.GetApp(srcChain).TransferKeeper.Transfer(srcChain.GetContext(), &imt)
|
|
299
|
-
s.Require().NoError(err)
|
|
300
|
-
packet.Sequence = imr.Sequence
|
|
301
415
|
|
|
302
|
-
return packet,
|
|
416
|
+
return nil, fmt.Errorf("failed to find a way to permute packet data: %s", string(data))
|
|
303
417
|
}
|
|
304
418
|
|
|
305
419
|
func (s *IntegrationTestSuite) mintToAddress(chain *ibctesting.TestChain, addr sdk.AccAddress, denom, amount string) {
|
|
@@ -313,136 +427,388 @@ func (s *IntegrationTestSuite) mintToAddress(chain *ibctesting.TestChain, addr s
|
|
|
313
427
|
s.Require().NoError(err)
|
|
314
428
|
err = app.BankKeeper.SendCoinsFromModuleToAccount(chain.GetContext(), ibctransfertypes.ModuleName, addr, coins)
|
|
315
429
|
s.Require().NoError(err)
|
|
316
|
-
|
|
317
|
-
// Verify success.
|
|
318
|
-
balances := app.BankKeeper.GetAllBalances(chain.GetContext(), addr)
|
|
319
|
-
s.Require().Equal(coins[0], balances[1])
|
|
320
430
|
}
|
|
321
431
|
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
)
|
|
432
|
+
// TestHops relays an IBC transfer initiated from a chain A to a chain B, via 0
|
|
433
|
+
// or more intermediate chains' PacketForwardMiddleware, and relays the chain
|
|
434
|
+
// B's resulting acknowledgement back through the intermediate chains to chain A
|
|
435
|
+
// in return. It verifies that the source and destination accounts' bridge
|
|
436
|
+
// targets are called by inspecting their resulting actionQueue records. By
|
|
437
|
+
// committing blocks between actions, the test verifies that the VM results are
|
|
438
|
+
// permitted to be async across blocks.
|
|
439
|
+
func (s *IntegrationTestSuite) TestHops() {
|
|
440
|
+
testCases := []struct {
|
|
441
|
+
name string
|
|
442
|
+
senderIsTarget bool
|
|
443
|
+
receiverIsTarget bool
|
|
444
|
+
senderHookData []byte
|
|
445
|
+
receiverHookData []byte
|
|
446
|
+
}{
|
|
447
|
+
{"NoTargetsNoHooks", false, false, nil, nil},
|
|
448
|
+
{"NoTargetsReceiverHook", false, false, nil, []byte("?what=arbitrary-data&why=to-test-bridge-targets")},
|
|
449
|
+
{"NoTargetsSenderHook", false, false, []byte("?name=alice&peer=bob"), nil},
|
|
450
|
+
{"NoTargetsBothHooks", false, false, []byte("?name=alice&peer=bob"), []byte("?what=arbitrary-data&why=to-test-bridge-targets")},
|
|
451
|
+
{"SenderTargetNoHooks", true, false, nil, nil},
|
|
452
|
+
{"SenderTargetReceiverHook", true, false, nil, []byte("?what=arbitrary-data&why=to-test-bridge-targets")},
|
|
453
|
+
{"SenderTargetSenderHook", true, false, []byte("?name=alice&peer=bob"), nil},
|
|
454
|
+
{"SenderTargetBothHooks", true, false, []byte("?name=alice&peer=bob"), []byte("?what=arbitrary-data&why=to-test-bridge-targets")},
|
|
455
|
+
{"ReceiverTargetNoHooks", false, true, nil, nil},
|
|
456
|
+
{"ReceiverTargetReceiverHook", false, true, nil, []byte("?what=arbitrary-data&why=to-test-bridge-targets")},
|
|
457
|
+
{"ReceiverTargetSenderHook", false, true, []byte("?name=alice&peer=bob"), nil},
|
|
458
|
+
{"ReceiverTargetBothHooks", false, true, []byte("?name=alice&peer=bob"), []byte("?what=arbitrary-data&why=to-test-bridge-targets")},
|
|
459
|
+
{"BothTargetsNoHooks", true, true, nil, nil},
|
|
460
|
+
{"BothTargetsReceiverHook", true, true, nil, []byte("?what=arbitrary-data&why=to-test-bridge-targets")},
|
|
461
|
+
{"BothTargetsSenderHook", true, true, []byte("?name=alice&peer=bob"), nil},
|
|
462
|
+
{"BothTargetsBothHooks", true, true, []byte("?name=alice&peer=bob"), []byte("?what=arbitrary-data&why=to-test-bridge-targets")},
|
|
463
|
+
}
|
|
341
464
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
465
|
+
for hops := 1; hops <= 2; hops += 1 {
|
|
466
|
+
for _, tc := range testCases {
|
|
467
|
+
tc := tc
|
|
468
|
+
name := fmt.Sprintf("%s_%dHop", tc.name, hops)
|
|
469
|
+
s.Run(name, func() {
|
|
470
|
+
_, _, baseSenderAddr := testdata.KeyTestPubAddr()
|
|
471
|
+
baseSender := baseSenderAddr.String()
|
|
472
|
+
|
|
473
|
+
_, _, baseReceiverAddr := testdata.KeyTestPubAddr()
|
|
474
|
+
baseReceiver := baseReceiverAddr.String()
|
|
475
|
+
|
|
476
|
+
var receiver, sender string
|
|
477
|
+
var err error
|
|
478
|
+
if tc.senderHookData != nil {
|
|
479
|
+
sender, err = types.JoinHookedAddress(baseSender, tc.senderHookData)
|
|
480
|
+
s.Require().NoError(err)
|
|
481
|
+
} else {
|
|
482
|
+
sender = baseSender
|
|
483
|
+
}
|
|
346
484
|
|
|
347
|
-
|
|
485
|
+
if tc.receiverHookData != nil {
|
|
486
|
+
receiver, err = types.JoinHookedAddress(baseReceiver, tc.receiverHookData)
|
|
487
|
+
s.Require().NoError(err)
|
|
488
|
+
} else {
|
|
489
|
+
receiver = baseReceiver
|
|
490
|
+
}
|
|
348
491
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
492
|
+
var overriddenPacketData []byte
|
|
493
|
+
overrideSendPacketData := func(ctx sdk.Context, cdc codec.Codec, data []byte) ([]byte, error) {
|
|
494
|
+
newData, err := s.overrideSendPacketData(cdc, data, sender)
|
|
495
|
+
overriddenPacketData = newData
|
|
496
|
+
return overriddenPacketData, err
|
|
497
|
+
}
|
|
498
|
+
// Reset the chain state.
|
|
499
|
+
for i := 0; i <= hops; i += 1 {
|
|
500
|
+
chain := s.GetChainByIndex(i)
|
|
501
|
+
s.resetActionQueue(chain)
|
|
502
|
+
s.GetApp(chain).VtransferKeeper.SetDebugging(StorePacketData, overrideSendPacketData)
|
|
503
|
+
|
|
504
|
+
// Only the first chain is the sender, so don't override any other packets.
|
|
505
|
+
overrideSendPacketData = nil
|
|
506
|
+
}
|
|
352
507
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
508
|
+
// Construct the transfer path from chainA=0, [chainC=2, [chainD=3...]], and finally chainB=1.
|
|
509
|
+
// This guarantees that the first and last chains are s.chainA and s.chainB.
|
|
510
|
+
paths := make([]*ibctesting.Path, hops)
|
|
511
|
+
{
|
|
512
|
+
endpointAChainIndex := 0 // s.chainA
|
|
513
|
+
endpointBChainIndex := 2 // s.chainC
|
|
514
|
+
for i := 0; i < hops; i += 1 {
|
|
515
|
+
if i == hops-1 {
|
|
516
|
+
// Final path's endpointB is s.chainB=1.
|
|
517
|
+
endpointBChainIndex = 1
|
|
518
|
+
}
|
|
519
|
+
// Each path is an endpointA->endpointB pair. We specify them by index.
|
|
520
|
+
paths[i] = s.NewTransferPath(endpointAChainIndex, endpointBChainIndex)
|
|
521
|
+
// The next path's A is the current path's B...
|
|
522
|
+
endpointAChainIndex = endpointBChainIndex
|
|
523
|
+
// and the next path's B is the next chain in the sequence.
|
|
524
|
+
endpointBChainIndex += 1
|
|
525
|
+
}
|
|
526
|
+
}
|
|
358
527
|
|
|
359
|
-
|
|
360
|
-
|
|
528
|
+
// create a transfer packet's data contents
|
|
529
|
+
hopReceiver := receiver
|
|
530
|
+
var memoBytes []byte
|
|
531
|
+
for pathIdx := hops - 1; pathIdx > 0; pathIdx -= 1 {
|
|
532
|
+
m := struct {
|
|
533
|
+
Forward packetforwardtypes.ForwardMetadata `json:"forward"`
|
|
534
|
+
}{}
|
|
535
|
+
if memoBytes != nil {
|
|
536
|
+
m.Forward.Next = packetforwardtypes.NewJSONObject(false, memoBytes, *orderedmap.New())
|
|
537
|
+
}
|
|
538
|
+
m.Forward.Receiver = hopReceiver
|
|
539
|
+
|
|
540
|
+
// Previous hops should not have a bech32 address in the receiver field,
|
|
541
|
+
// or tokens may get stuck en route rather than returned on error.
|
|
542
|
+
hopReceiver = "pfm"
|
|
543
|
+
m.Forward.Port = paths[pathIdx].EndpointA.ChannelConfig.PortID
|
|
544
|
+
m.Forward.Channel = paths[pathIdx].EndpointA.ChannelID
|
|
545
|
+
|
|
546
|
+
memoBytes, err = json.Marshal(m)
|
|
547
|
+
s.Require().NoError(err)
|
|
548
|
+
}
|
|
361
549
|
|
|
362
|
-
|
|
363
|
-
|
|
550
|
+
var memo string
|
|
551
|
+
if memoBytes != nil {
|
|
552
|
+
memo = string(memoBytes)
|
|
553
|
+
} else {
|
|
554
|
+
memo = `This is not a JSON memo`
|
|
555
|
+
}
|
|
364
556
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
557
|
+
denomTrace := "uosmo"
|
|
558
|
+
transferData := ibctransfertypes.NewFungibleTokenPacketData(
|
|
559
|
+
denomTrace,
|
|
560
|
+
"1000000",
|
|
561
|
+
baseSender, // TODO: ideally this would just be sender, and `TransferKeeper.Transfer` would accept address hooks.
|
|
562
|
+
hopReceiver,
|
|
563
|
+
memo,
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
// Register the sender and receiver as bridge targets on their specific
|
|
567
|
+
// chain.
|
|
568
|
+
if tc.senderIsTarget {
|
|
569
|
+
s.RegisterBridgeTarget(s.chainA, baseSender)
|
|
570
|
+
}
|
|
571
|
+
if tc.receiverIsTarget {
|
|
572
|
+
s.RegisterBridgeTarget(s.chainB, baseReceiver)
|
|
573
|
+
}
|
|
369
574
|
|
|
370
|
-
|
|
575
|
+
s.mintToAddress(s.chainA, baseSenderAddr, transferData.Denom, transferData.Amount)
|
|
371
576
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
577
|
+
// Initiate the transfer
|
|
578
|
+
sendContext := s.chainA.GetContext()
|
|
579
|
+
err = s.TransferFromEndpoint(sendContext, paths[0].EndpointA, transferData)
|
|
580
|
+
s.Require().NoError(err)
|
|
581
|
+
|
|
582
|
+
sendPacket, err := ParsePacketFromEvents(sendContext.EventManager().Events())
|
|
583
|
+
s.Require().NoError(err)
|
|
584
|
+
|
|
585
|
+
s.coordinator.CommitBlock(s.chainA)
|
|
586
|
+
|
|
587
|
+
// Relay the packet through the intermediaries to the final destination.
|
|
588
|
+
var packetRes *sdk.Result
|
|
589
|
+
var writeAcknowledgementHeight, writeAcknowledgementTime int64
|
|
590
|
+
for pathIdx := 0; pathIdx < hops; pathIdx += 1 {
|
|
591
|
+
nextPath := paths[pathIdx]
|
|
592
|
+
err = nextPath.EndpointB.UpdateClient()
|
|
593
|
+
s.Require().NoError(err)
|
|
594
|
+
s.coordinator.CommitBlock(nextPath.EndpointB.Chain)
|
|
595
|
+
|
|
596
|
+
writeAcknowledgementHeight = nextPath.EndpointB.Chain.CurrentHeader.Height
|
|
597
|
+
writeAcknowledgementTime = nextPath.EndpointB.Chain.CurrentHeader.Time.Unix()
|
|
598
|
+
|
|
599
|
+
packetRes, err = nextPath.EndpointB.RecvPacketWithResult(sendPacket)
|
|
600
|
+
s.Require().NoError(err)
|
|
601
|
+
|
|
602
|
+
s.coordinator.CommitBlock(nextPath.EndpointA.Chain, nextPath.EndpointB.Chain)
|
|
603
|
+
|
|
604
|
+
denomTrace = s.prependDenomTrace(nextPath.EndpointB, denomTrace)
|
|
605
|
+
|
|
606
|
+
{
|
|
607
|
+
expectedRecords := []swingsettypes.InboundQueueRecord{}
|
|
608
|
+
s.assertActionQueue(nextPath.EndpointA.Chain, expectedRecords)
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if pathIdx >= hops-1 {
|
|
612
|
+
break
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// The PFM should have received the packet and advertised a send toward the last path.
|
|
616
|
+
sendPacket, err = ParsePacketFromEvents(packetRes.GetEvents())
|
|
617
|
+
s.Require().NoError(err)
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
var ack ibcexported.Acknowledgement
|
|
621
|
+
var ackedPacket channeltypes.Packet
|
|
622
|
+
|
|
623
|
+
expectedAck := channeltypes.NewResultAcknowledgement([]byte{1})
|
|
376
624
|
|
|
377
|
-
{
|
|
378
|
-
expectedRecords := []swingsettypes.InboundQueueRecord{
|
|
379
625
|
{
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
626
|
+
var events sdk.Events
|
|
627
|
+
var ackData []byte
|
|
628
|
+
if packetRes != nil {
|
|
629
|
+
events = packetRes.GetEvents()
|
|
630
|
+
ackData, err = ParseAckFromEvents(events)
|
|
631
|
+
}
|
|
632
|
+
if tc.receiverIsTarget {
|
|
633
|
+
s.Require().Nil(ackData)
|
|
634
|
+
// The packet was not yet acknowledged, so write out an ack from the VM, one block later
|
|
635
|
+
s.coordinator.CommitBlock(s.chainB)
|
|
636
|
+
|
|
637
|
+
vmAckContext := s.chainB.GetContext()
|
|
638
|
+
err = s.GetApp(s.chainB).VtransferKeeper.ReceiveWriteAcknowledgement(vmAckContext, sendPacket, expectedAck)
|
|
639
|
+
s.Require().NoError(err)
|
|
640
|
+
|
|
641
|
+
events = vmAckContext.EventManager().Events()
|
|
642
|
+
ackData, err = ParseAckFromEvents(events)
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
s.Require().NoError(err)
|
|
646
|
+
|
|
647
|
+
ackedPacket, err = ParsePacketFromFilteredEvents(events, channeltypes.EventTypeWriteAck)
|
|
648
|
+
s.Require().NoError(err)
|
|
649
|
+
ack = vibctypes.NewRawAcknowledgement(ackData)
|
|
650
|
+
|
|
651
|
+
s.coordinator.CommitBlock(s.chainB)
|
|
652
|
+
|
|
653
|
+
expectedRecords := []swingsettypes.InboundQueueRecord{}
|
|
654
|
+
if tc.receiverIsTarget {
|
|
655
|
+
expectedRecords = append(expectedRecords, swingsettypes.InboundQueueRecord{
|
|
656
|
+
Action: &vibckeeper.WriteAcknowledgementEvent{
|
|
657
|
+
ActionHeader: &vm.ActionHeader{
|
|
658
|
+
Type: "VTRANSFER_IBC_EVENT",
|
|
659
|
+
BlockHeight: writeAcknowledgementHeight,
|
|
660
|
+
BlockTime: writeAcknowledgementTime,
|
|
661
|
+
},
|
|
662
|
+
Event: "writeAcknowledgement",
|
|
663
|
+
Target: baseReceiver,
|
|
664
|
+
Packet: sendPacket,
|
|
665
|
+
Acknowledgement: expectedAck.Acknowledgement(),
|
|
666
|
+
},
|
|
667
|
+
Context: swingsettypes.ActionContext{
|
|
668
|
+
BlockHeight: writeAcknowledgementHeight,
|
|
669
|
+
// TxHash is filled in below
|
|
670
|
+
MsgIdx: 0,
|
|
671
|
+
},
|
|
672
|
+
})
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
s.assertActionQueue(s.chainB, expectedRecords)
|
|
676
|
+
}
|
|
398
677
|
|
|
399
|
-
|
|
678
|
+
// Send the acks back.
|
|
679
|
+
for pathIdx := hops - 1; pathIdx > 0; pathIdx -= 1 {
|
|
680
|
+
priorPath := paths[pathIdx]
|
|
400
681
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
s.Require().NoError(err)
|
|
682
|
+
// Update Client
|
|
683
|
+
err = priorPath.EndpointA.UpdateClient()
|
|
684
|
+
s.Require().NoError(err)
|
|
405
685
|
|
|
406
|
-
|
|
407
|
-
|
|
686
|
+
// Prove the PFM packet's acknowledgement.
|
|
687
|
+
ackRes, err := acknowledgePacketWithResult(priorPath.EndpointA, ackedPacket, ack.Acknowledgement())
|
|
688
|
+
s.Require().NoError(err)
|
|
408
689
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
s.Require().NoError(err)
|
|
690
|
+
ackedPacket, err = ParsePacketFromFilteredEvents(ackRes.GetEvents(), channeltypes.EventTypeWriteAck)
|
|
691
|
+
s.Require().NoError(err)
|
|
412
692
|
|
|
413
|
-
|
|
414
|
-
|
|
693
|
+
ackData, err := ParseAckFromEvents(ackRes.GetEvents())
|
|
694
|
+
s.Require().NoError(err)
|
|
695
|
+
ack = vibctypes.NewRawAcknowledgement(ackData)
|
|
415
696
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
s.Require().NoError(err)
|
|
697
|
+
s.coordinator.CommitBlock(priorPath.EndpointA.Chain, priorPath.EndpointB.Chain)
|
|
698
|
+
}
|
|
419
699
|
|
|
420
|
-
|
|
700
|
+
// Update Client
|
|
701
|
+
err = paths[0].EndpointA.UpdateClient()
|
|
702
|
+
s.Require().NoError(err)
|
|
703
|
+
|
|
704
|
+
acknowledgementHeight := s.chainA.CurrentHeader.Height
|
|
705
|
+
acknowledgementTime := s.chainA.CurrentHeader.Time.Unix()
|
|
706
|
+
|
|
707
|
+
// Prove the initial packet's acknowledgement.
|
|
708
|
+
ackRes, err := acknowledgePacketWithResult(paths[0].EndpointA, ackedPacket, ack.Acknowledgement())
|
|
709
|
+
s.Require().NoError(err)
|
|
710
|
+
|
|
711
|
+
// Commit the block to finalize the acknowledgement.
|
|
712
|
+
s.coordinator.CommitBlock(s.chainA, s.chainB)
|
|
713
|
+
|
|
714
|
+
// Verify the resulting events.
|
|
715
|
+
gotEvents := 0
|
|
716
|
+
expectedEvents := 2
|
|
717
|
+
for _, event := range ackRes.GetEvents() {
|
|
718
|
+
if event.Type == ibctransfertypes.EventTypePacket {
|
|
719
|
+
gotEvents += 1
|
|
720
|
+
if gotEvents == 2 && len(event.Attributes) == 1 {
|
|
721
|
+
// We get a trailing event with a single "success" attribute.
|
|
722
|
+
s.Require().Equal(ibctransfertypes.AttributeKeyAckSuccess, string(event.Attributes[0].Key))
|
|
723
|
+
s.Require().Equal("\x01", string(event.Attributes[0].Value))
|
|
724
|
+
continue
|
|
725
|
+
}
|
|
726
|
+
expectedAttrs := 6
|
|
727
|
+
gotAttrs := 0
|
|
728
|
+
for _, attr := range event.Attributes {
|
|
729
|
+
switch string(attr.Key) {
|
|
730
|
+
case "module":
|
|
731
|
+
s.Require().Equal(ibctransfertypes.ModuleName, string(attr.Value))
|
|
732
|
+
gotAttrs += 1
|
|
733
|
+
case ibctransfertypes.AttributeKeyAckSuccess:
|
|
734
|
+
s.Require().Equal("\x01", string(attr.Value))
|
|
735
|
+
gotAttrs += 1
|
|
736
|
+
case ibctransfertypes.AttributeKeyMemo:
|
|
737
|
+
s.Require().Equal(transferData.Memo, string(attr.Value))
|
|
738
|
+
gotAttrs += 1
|
|
739
|
+
case ibctransfertypes.AttributeKeyReceiver:
|
|
740
|
+
s.Require().Equal(transferData.Receiver, string(attr.Value))
|
|
741
|
+
gotAttrs += 1
|
|
742
|
+
case "sender": // ibctransfertypes.AttributeKeySender:
|
|
743
|
+
s.Require().Equal(transferData.Sender, string(attr.Value))
|
|
744
|
+
gotAttrs += 1
|
|
745
|
+
case ibctransfertypes.AttributeKeyDenom:
|
|
746
|
+
s.Require().Equal(transferData.Denom, string(attr.Value))
|
|
747
|
+
gotAttrs += 1
|
|
748
|
+
case ibctransfertypes.AttributeKeyAmount:
|
|
749
|
+
s.Require().Equal(transferData.Amount, string(attr.Value))
|
|
750
|
+
gotAttrs += 1
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
s.Require().Equal(expectedAttrs, gotAttrs, `expected %d %s type attributes, got %d`, expectedAttrs, ibctransfertypes.EventTypePacket, gotAttrs)
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// The resulting IBC packet event should be what we expected.
|
|
758
|
+
s.Require().Equal(expectedEvents, gotEvents, `expected %d %s type events, got %d`, expectedEvents, ibctransfertypes.EventTypePacket, gotEvents)
|
|
421
759
|
|
|
422
|
-
{
|
|
423
|
-
expectedRecords := []swingsettypes.InboundQueueRecord{
|
|
424
760
|
{
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
761
|
+
// Undo the sender data override.
|
|
762
|
+
expectedPacket := ackedPacket
|
|
763
|
+
expectedPacket.Data = overriddenPacketData
|
|
764
|
+
|
|
765
|
+
expectedRecords := []swingsettypes.InboundQueueRecord{}
|
|
766
|
+
if tc.senderIsTarget {
|
|
767
|
+
expectedRecords = append(expectedRecords, swingsettypes.InboundQueueRecord{
|
|
768
|
+
Action: &vibckeeper.WriteAcknowledgementEvent{
|
|
769
|
+
ActionHeader: &vm.ActionHeader{
|
|
770
|
+
Type: "VTRANSFER_IBC_EVENT",
|
|
771
|
+
BlockHeight: acknowledgementHeight,
|
|
772
|
+
BlockTime: acknowledgementTime,
|
|
773
|
+
},
|
|
774
|
+
Event: "acknowledgementPacket",
|
|
775
|
+
Target: baseSender,
|
|
776
|
+
Packet: expectedPacket,
|
|
777
|
+
Acknowledgement: ack.Acknowledgement(),
|
|
778
|
+
Relayer: s.chainA.SenderAccount.GetAddress(),
|
|
779
|
+
},
|
|
780
|
+
Context: swingsettypes.ActionContext{
|
|
781
|
+
BlockHeight: acknowledgementHeight,
|
|
782
|
+
// TxHash is filled in below
|
|
783
|
+
MsgIdx: 0,
|
|
784
|
+
},
|
|
785
|
+
})
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
s.assertActionQueue(s.chainA, expectedRecords)
|
|
789
|
+
}
|
|
444
790
|
|
|
445
|
-
|
|
791
|
+
// Verify the resulting received coin balance.
|
|
792
|
+
req := &banktypes.QueryAllBalancesRequest{
|
|
793
|
+
Address: baseReceiver,
|
|
794
|
+
}
|
|
795
|
+
res, err := s.GetApp(s.chainB).BankKeeper.AllBalances(s.chainB.GetContext(), req)
|
|
796
|
+
s.Require().NoError(err)
|
|
797
|
+
|
|
798
|
+
amt, ok := sdk.NewIntFromString(transferData.Amount)
|
|
799
|
+
s.Require().True(ok)
|
|
800
|
+
|
|
801
|
+
// Decode the denom trace to get the denom hash.
|
|
802
|
+
hashReq := &ibctransfertypes.QueryDenomHashRequest{
|
|
803
|
+
Trace: denomTrace,
|
|
804
|
+
}
|
|
805
|
+
hashRes, err := s.GetApp(s.chainB).TransferKeeper.DenomHash(s.chainB.GetContext(), hashReq)
|
|
806
|
+
s.Require().NoError(err)
|
|
807
|
+
receivedDenom := `ibc/` + hashRes.Hash
|
|
808
|
+
|
|
809
|
+
coins := sdk.NewCoins(sdk.NewCoin(receivedDenom, amt))
|
|
810
|
+
s.Require().True(coins.IsEqual(res.Balances))
|
|
811
|
+
})
|
|
446
812
|
}
|
|
447
|
-
}
|
|
813
|
+
}
|
|
448
814
|
}
|