Refactor/api exits and authfeature/sql-semaphore1
@ -0,0 +1,68 @@ |
|||||
|
package api |
||||
|
|
||||
|
import ( |
||||
|
"errors" |
||||
|
"net/http" |
||||
|
"time" |
||||
|
|
||||
|
ethCommon "github.com/ethereum/go-ethereum/common" |
||||
|
"github.com/gin-gonic/gin" |
||||
|
"github.com/hermeznetwork/hermez-node/apitypes" |
||||
|
"github.com/hermeznetwork/hermez-node/common" |
||||
|
"github.com/iden3/go-iden3-crypto/babyjub" |
||||
|
) |
||||
|
|
||||
|
func postAccountCreationAuth(c *gin.Context) { |
||||
|
// Parse body
|
||||
|
var apiAuth receivedAuth |
||||
|
if err := c.ShouldBindJSON(&apiAuth); err != nil { |
||||
|
retBadReq(err, c) |
||||
|
return |
||||
|
} |
||||
|
// API to common + verify signature
|
||||
|
commonAuth := accountCreationAuthAPIToCommon(&apiAuth) |
||||
|
if !commonAuth.VerifySignature() { |
||||
|
retBadReq(errors.New("invalid signature"), c) |
||||
|
return |
||||
|
} |
||||
|
// Insert to DB
|
||||
|
if err := l2.AddAccountCreationAuth(commonAuth); err != nil { |
||||
|
retSQLErr(err, c) |
||||
|
return |
||||
|
} |
||||
|
// Return OK
|
||||
|
c.Status(http.StatusOK) |
||||
|
} |
||||
|
|
||||
|
func getAccountCreationAuth(c *gin.Context) { |
||||
|
// Get hezEthereumAddress
|
||||
|
addr, err := parseParamHezEthAddr(c) |
||||
|
if err != nil { |
||||
|
retBadReq(err, c) |
||||
|
return |
||||
|
} |
||||
|
// Fetch auth from l2DB
|
||||
|
auth, err := l2.GetAccountCreationAuthAPI(*addr) |
||||
|
if err != nil { |
||||
|
retSQLErr(err, c) |
||||
|
return |
||||
|
} |
||||
|
// Build succesfull response
|
||||
|
c.JSON(http.StatusOK, auth) |
||||
|
} |
||||
|
|
||||
|
type receivedAuth struct { |
||||
|
EthAddr apitypes.StrHezEthAddr `json:"hezEthereumAddress" binding:"required"` |
||||
|
BJJ apitypes.StrHezBJJ `json:"bjj" binding:"required"` |
||||
|
Signature apitypes.EthSignature `json:"signature" binding:"required"` |
||||
|
Timestamp time.Time `json:"timestamp"` |
||||
|
} |
||||
|
|
||||
|
func accountCreationAuthAPIToCommon(apiAuth *receivedAuth) *common.AccountCreationAuth { |
||||
|
return &common.AccountCreationAuth{ |
||||
|
EthAddr: ethCommon.Address(apiAuth.EthAddr), |
||||
|
BJJ: (*babyjub.PublicKey)(&apiAuth.BJJ), |
||||
|
Signature: []byte(apiAuth.Signature), |
||||
|
Timestamp: apiAuth.Timestamp, |
||||
|
} |
||||
|
} |
@ -0,0 +1,99 @@ |
|||||
|
package api |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"encoding/hex" |
||||
|
"encoding/json" |
||||
|
"fmt" |
||||
|
"math/big" |
||||
|
"testing" |
||||
|
"time" |
||||
|
|
||||
|
ethCommon "github.com/ethereum/go-ethereum/common" |
||||
|
"github.com/hermeznetwork/hermez-node/common" |
||||
|
"github.com/stretchr/testify/assert" |
||||
|
) |
||||
|
|
||||
|
type testAuth struct { |
||||
|
EthAddr string `json:"hezEthereumAddress" binding:"required"` |
||||
|
BJJ string `json:"bjj" binding:"required"` |
||||
|
Signature string `json:"signature" binding:"required"` |
||||
|
Timestamp time.Time `json:"timestamp"` |
||||
|
} |
||||
|
|
||||
|
func genTestAuths(auths []*common.AccountCreationAuth) []testAuth { |
||||
|
testAuths := []testAuth{} |
||||
|
for _, auth := range auths { |
||||
|
testAuths = append(testAuths, testAuth{ |
||||
|
EthAddr: ethAddrToHez(auth.EthAddr), |
||||
|
BJJ: bjjToString(auth.BJJ), |
||||
|
Signature: "0x" + hex.EncodeToString(auth.Signature), |
||||
|
Timestamp: auth.Timestamp, |
||||
|
}) |
||||
|
} |
||||
|
return testAuths |
||||
|
} |
||||
|
|
||||
|
func TestAccountCreationAuth(t *testing.T) { |
||||
|
// POST
|
||||
|
endpoint := apiURL + "account-creation-authorization" |
||||
|
for _, auth := range tc.auths { |
||||
|
jsonAuthBytes, err := json.Marshal(auth) |
||||
|
assert.NoError(t, err) |
||||
|
jsonAuthReader := bytes.NewReader(jsonAuthBytes) |
||||
|
fmt.Println(string(jsonAuthBytes)) |
||||
|
assert.NoError( |
||||
|
t, doGoodReq( |
||||
|
"POST", |
||||
|
endpoint, |
||||
|
jsonAuthReader, nil, |
||||
|
), |
||||
|
) |
||||
|
} |
||||
|
// GET
|
||||
|
endpoint += "/" |
||||
|
for _, auth := range tc.auths { |
||||
|
fetchedAuth := testAuth{} |
||||
|
assert.NoError( |
||||
|
t, doGoodReq( |
||||
|
"GET", |
||||
|
endpoint+auth.EthAddr, |
||||
|
nil, &fetchedAuth, |
||||
|
), |
||||
|
) |
||||
|
assertAuth(t, auth, fetchedAuth) |
||||
|
} |
||||
|
// POST
|
||||
|
// 400
|
||||
|
// Wrong addr
|
||||
|
badAuth := tc.auths[0] |
||||
|
badAuth.EthAddr = ethAddrToHez(ethCommon.BigToAddress(big.NewInt(1))) |
||||
|
jsonAuthBytes, err := json.Marshal(badAuth) |
||||
|
assert.NoError(t, err) |
||||
|
jsonAuthReader := bytes.NewReader(jsonAuthBytes) |
||||
|
err = doBadReq("POST", endpoint, jsonAuthReader, 400) |
||||
|
assert.NoError(t, err) |
||||
|
// Wrong signature
|
||||
|
badAuth = tc.auths[0] |
||||
|
badAuth.Signature = badAuth.Signature[:len(badAuth.Signature)-1] |
||||
|
badAuth.Signature += "F" |
||||
|
jsonAuthBytes, err = json.Marshal(badAuth) |
||||
|
assert.NoError(t, err) |
||||
|
jsonAuthReader = bytes.NewReader(jsonAuthBytes) |
||||
|
err = doBadReq("POST", endpoint, jsonAuthReader, 400) |
||||
|
assert.NoError(t, err) |
||||
|
// GET
|
||||
|
// 400
|
||||
|
err = doBadReq("GET", endpoint+"hez:0xFooBar", nil, 400) |
||||
|
assert.NoError(t, err) |
||||
|
// 404
|
||||
|
err = doBadReq("GET", endpoint+"hez:0x0000000000000000000000000000000000000001", nil, 404) |
||||
|
assert.NoError(t, err) |
||||
|
} |
||||
|
|
||||
|
func assertAuth(t *testing.T, expected, actual testAuth) { |
||||
|
// timestamp should be very close to now
|
||||
|
assert.Less(t, time.Now().UTC().Unix()-3, actual.Timestamp.Unix()) |
||||
|
expected.Timestamp = actual.Timestamp |
||||
|
assert.Equal(t, expected, actual) |
||||
|
} |
@ -0,0 +1,72 @@ |
|||||
|
package api |
||||
|
|
||||
|
import ( |
||||
|
"net/http" |
||||
|
|
||||
|
"github.com/gin-gonic/gin" |
||||
|
"github.com/hermeznetwork/hermez-node/db" |
||||
|
"github.com/hermeznetwork/hermez-node/db/historydb" |
||||
|
) |
||||
|
|
||||
|
func getExits(c *gin.Context) { |
||||
|
// Get query parameters
|
||||
|
// Account filters
|
||||
|
tokenID, addr, bjj, idx, err := parseAccountFilters(c) |
||||
|
if err != nil { |
||||
|
retBadReq(err, c) |
||||
|
return |
||||
|
} |
||||
|
// BatchNum
|
||||
|
batchNum, err := parseQueryUint("batchNum", nil, 0, maxUint32, c) |
||||
|
if err != nil { |
||||
|
retBadReq(err, c) |
||||
|
return |
||||
|
} |
||||
|
// Pagination
|
||||
|
fromItem, order, limit, err := parsePagination(c) |
||||
|
if err != nil { |
||||
|
retBadReq(err, c) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Fetch exits from historyDB
|
||||
|
exits, pagination, err := h.GetExitsAPI( |
||||
|
addr, bjj, tokenID, idx, batchNum, fromItem, limit, order, |
||||
|
) |
||||
|
if err != nil { |
||||
|
retSQLErr(err, c) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Build succesfull response
|
||||
|
type exitsResponse struct { |
||||
|
Exits []historydb.ExitAPI `json:"exits"` |
||||
|
Pagination *db.Pagination `json:"pagination"` |
||||
|
} |
||||
|
c.JSON(http.StatusOK, &exitsResponse{ |
||||
|
Exits: exits, |
||||
|
Pagination: pagination, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
func getExit(c *gin.Context) { |
||||
|
// Get batchNum and accountIndex
|
||||
|
batchNum, err := parseParamUint("batchNum", nil, 0, maxUint32, c) |
||||
|
if err != nil { |
||||
|
retBadReq(err, c) |
||||
|
return |
||||
|
} |
||||
|
idx, err := parseParamIdx(c) |
||||
|
if err != nil { |
||||
|
retBadReq(err, c) |
||||
|
return |
||||
|
} |
||||
|
// Fetch tx from historyDB
|
||||
|
exit, err := h.GetExitAPI(batchNum, idx) |
||||
|
if err != nil { |
||||
|
retSQLErr(err, c) |
||||
|
return |
||||
|
} |
||||
|
// Build succesfull response
|
||||
|
c.JSON(http.StatusOK, exit) |
||||
|
} |
@ -0,0 +1,276 @@ |
|||||
|
package api |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"testing" |
||||
|
|
||||
|
"github.com/hermeznetwork/hermez-node/common" |
||||
|
"github.com/hermeznetwork/hermez-node/db" |
||||
|
"github.com/hermeznetwork/hermez-node/db/historydb" |
||||
|
"github.com/mitchellh/copystructure" |
||||
|
"github.com/stretchr/testify/assert" |
||||
|
"github.com/stretchr/testify/require" |
||||
|
) |
||||
|
|
||||
|
type testCVP struct { |
||||
|
Root string |
||||
|
Siblings []string |
||||
|
OldKey string |
||||
|
OldValue string |
||||
|
IsOld0 bool |
||||
|
Key string |
||||
|
Value string |
||||
|
Fnc int |
||||
|
} |
||||
|
|
||||
|
type testExit struct { |
||||
|
ItemID int `json:"itemId"` |
||||
|
BatchNum common.BatchNum `json:"batchNum"` |
||||
|
AccountIdx string `json:"accountIndex"` |
||||
|
MerkleProof testCVP `json:"merkleProof"` |
||||
|
Balance string `json:"balance"` |
||||
|
InstantWithdrawn *int64 `json:"instantWithdrawn"` |
||||
|
DelayedWithdrawRequest *int64 `json:"delayedWithdrawRequest"` |
||||
|
DelayedWithdrawn *int64 `json:"delayedWithdrawn"` |
||||
|
Token historydb.TokenWithUSD `json:"token"` |
||||
|
} |
||||
|
|
||||
|
type testExitsResponse struct { |
||||
|
Exits []testExit `json:"exits"` |
||||
|
Pagination *db.Pagination `json:"pagination"` |
||||
|
} |
||||
|
|
||||
|
func (t *testExitsResponse) GetPagination() *db.Pagination { |
||||
|
if t.Exits[0].ItemID < t.Exits[len(t.Exits)-1].ItemID { |
||||
|
t.Pagination.FirstReturnedItem = t.Exits[0].ItemID |
||||
|
t.Pagination.LastReturnedItem = t.Exits[len(t.Exits)-1].ItemID |
||||
|
} else { |
||||
|
t.Pagination.LastReturnedItem = t.Exits[0].ItemID |
||||
|
t.Pagination.FirstReturnedItem = t.Exits[len(t.Exits)-1].ItemID |
||||
|
} |
||||
|
return t.Pagination |
||||
|
} |
||||
|
|
||||
|
func (t *testExitsResponse) Len() int { |
||||
|
return len(t.Exits) |
||||
|
} |
||||
|
|
||||
|
func genTestExits( |
||||
|
commonExits []common.ExitInfo, |
||||
|
tokens []historydb.TokenWithUSD, |
||||
|
accs []common.Account, |
||||
|
usrIdxs []string, |
||||
|
) (usrExits, allExits []testExit) { |
||||
|
allExits = []testExit{} |
||||
|
for _, exit := range commonExits { |
||||
|
token := getTokenByIdx(exit.AccountIdx, tokens, accs) |
||||
|
siblings := []string{} |
||||
|
for i := 0; i < len(exit.MerkleProof.Siblings); i++ { |
||||
|
siblings = append(siblings, exit.MerkleProof.Siblings[i].String()) |
||||
|
} |
||||
|
allExits = append(allExits, testExit{ |
||||
|
BatchNum: exit.BatchNum, |
||||
|
AccountIdx: idxToHez(exit.AccountIdx, token.Symbol), |
||||
|
MerkleProof: testCVP{ |
||||
|
Root: exit.MerkleProof.Root.String(), |
||||
|
Siblings: siblings, |
||||
|
OldKey: exit.MerkleProof.OldKey.String(), |
||||
|
OldValue: exit.MerkleProof.OldValue.String(), |
||||
|
IsOld0: exit.MerkleProof.IsOld0, |
||||
|
Key: exit.MerkleProof.Key.String(), |
||||
|
Value: exit.MerkleProof.Value.String(), |
||||
|
Fnc: exit.MerkleProof.Fnc, |
||||
|
}, |
||||
|
Balance: exit.Balance.String(), |
||||
|
InstantWithdrawn: exit.InstantWithdrawn, |
||||
|
DelayedWithdrawRequest: exit.DelayedWithdrawRequest, |
||||
|
DelayedWithdrawn: exit.DelayedWithdrawn, |
||||
|
Token: token, |
||||
|
}) |
||||
|
} |
||||
|
usrExits = []testExit{} |
||||
|
for _, exit := range allExits { |
||||
|
for _, idx := range usrIdxs { |
||||
|
if idx == exit.AccountIdx { |
||||
|
usrExits = append(usrExits, exit) |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return usrExits, allExits |
||||
|
} |
||||
|
|
||||
|
func TestGetExits(t *testing.T) { |
||||
|
endpoint := apiURL + "exits" |
||||
|
fetchedExits := []testExit{} |
||||
|
appendIter := func(intr interface{}) { |
||||
|
for i := 0; i < len(intr.(*testExitsResponse).Exits); i++ { |
||||
|
tmp, err := copystructure.Copy(intr.(*testExitsResponse).Exits[i]) |
||||
|
if err != nil { |
||||
|
panic(err) |
||||
|
} |
||||
|
fetchedExits = append(fetchedExits, tmp.(testExit)) |
||||
|
} |
||||
|
} |
||||
|
// Get all (no filters)
|
||||
|
limit := 8 |
||||
|
path := fmt.Sprintf("%s?limit=%d&fromItem=", endpoint, limit) |
||||
|
err := doGoodReqPaginated(path, historydb.OrderAsc, &testExitsResponse{}, appendIter) |
||||
|
assert.NoError(t, err) |
||||
|
assertExitAPIs(t, tc.exits, fetchedExits) |
||||
|
|
||||
|
// Get by ethAddr
|
||||
|
fetchedExits = []testExit{} |
||||
|
limit = 7 |
||||
|
path = fmt.Sprintf( |
||||
|
"%s?hermezEthereumAddress=%s&limit=%d&fromItem=", |
||||
|
endpoint, tc.usrAddr, limit, |
||||
|
) |
||||
|
err = doGoodReqPaginated(path, historydb.OrderAsc, &testExitsResponse{}, appendIter) |
||||
|
assert.NoError(t, err) |
||||
|
assertExitAPIs(t, tc.usrExits, fetchedExits) |
||||
|
// Get by bjj
|
||||
|
fetchedExits = []testExit{} |
||||
|
limit = 6 |
||||
|
path = fmt.Sprintf( |
||||
|
"%s?BJJ=%s&limit=%d&fromItem=", |
||||
|
endpoint, tc.usrBjj, limit, |
||||
|
) |
||||
|
err = doGoodReqPaginated(path, historydb.OrderAsc, &testExitsResponse{}, appendIter) |
||||
|
assert.NoError(t, err) |
||||
|
assertExitAPIs(t, tc.usrExits, fetchedExits) |
||||
|
// Get by tokenID
|
||||
|
fetchedExits = []testExit{} |
||||
|
limit = 5 |
||||
|
tokenID := tc.exits[0].Token.TokenID |
||||
|
path = fmt.Sprintf( |
||||
|
"%s?tokenId=%d&limit=%d&fromItem=", |
||||
|
endpoint, tokenID, limit, |
||||
|
) |
||||
|
err = doGoodReqPaginated(path, historydb.OrderAsc, &testExitsResponse{}, appendIter) |
||||
|
assert.NoError(t, err) |
||||
|
tokenIDExits := []testExit{} |
||||
|
for i := 0; i < len(tc.exits); i++ { |
||||
|
if tc.exits[i].Token.TokenID == tokenID { |
||||
|
tokenIDExits = append(tokenIDExits, tc.exits[i]) |
||||
|
} |
||||
|
} |
||||
|
assertExitAPIs(t, tokenIDExits, fetchedExits) |
||||
|
// idx
|
||||
|
fetchedExits = []testExit{} |
||||
|
limit = 4 |
||||
|
idx := tc.exits[0].AccountIdx |
||||
|
path = fmt.Sprintf( |
||||
|
"%s?accountIndex=%s&limit=%d&fromItem=", |
||||
|
endpoint, idx, limit, |
||||
|
) |
||||
|
err = doGoodReqPaginated(path, historydb.OrderAsc, &testExitsResponse{}, appendIter) |
||||
|
assert.NoError(t, err) |
||||
|
idxExits := []testExit{} |
||||
|
for i := 0; i < len(tc.exits); i++ { |
||||
|
if tc.exits[i].AccountIdx[6:] == idx[6:] { |
||||
|
idxExits = append(idxExits, tc.exits[i]) |
||||
|
} |
||||
|
} |
||||
|
assertExitAPIs(t, idxExits, fetchedExits) |
||||
|
// batchNum
|
||||
|
fetchedExits = []testExit{} |
||||
|
limit = 3 |
||||
|
batchNum := tc.exits[0].BatchNum |
||||
|
path = fmt.Sprintf( |
||||
|
"%s?batchNum=%d&limit=%d&fromItem=", |
||||
|
endpoint, batchNum, limit, |
||||
|
) |
||||
|
err = doGoodReqPaginated(path, historydb.OrderAsc, &testExitsResponse{}, appendIter) |
||||
|
assert.NoError(t, err) |
||||
|
batchNumExits := []testExit{} |
||||
|
for i := 0; i < len(tc.exits); i++ { |
||||
|
if tc.exits[i].BatchNum == batchNum { |
||||
|
batchNumExits = append(batchNumExits, tc.exits[i]) |
||||
|
} |
||||
|
} |
||||
|
assertExitAPIs(t, batchNumExits, fetchedExits) |
||||
|
// Multiple filters
|
||||
|
fetchedExits = []testExit{} |
||||
|
limit = 1 |
||||
|
path = fmt.Sprintf( |
||||
|
"%s?batchNum=%d&tokeId=%d&limit=%d&fromItem=", |
||||
|
endpoint, batchNum, tokenID, limit, |
||||
|
) |
||||
|
err = doGoodReqPaginated(path, historydb.OrderAsc, &testExitsResponse{}, appendIter) |
||||
|
assert.NoError(t, err) |
||||
|
mixedExits := []testExit{} |
||||
|
flipedExits := []testExit{} |
||||
|
for i := 0; i < len(tc.exits); i++ { |
||||
|
if tc.exits[i].BatchNum == batchNum && tc.exits[i].Token.TokenID == tokenID { |
||||
|
mixedExits = append(mixedExits, tc.exits[i]) |
||||
|
} |
||||
|
flipedExits = append(flipedExits, tc.exits[len(tc.exits)-1-i]) |
||||
|
} |
||||
|
assertExitAPIs(t, mixedExits, fetchedExits) |
||||
|
// All, in reverse order
|
||||
|
fetchedExits = []testExit{} |
||||
|
limit = 5 |
||||
|
path = fmt.Sprintf("%s?limit=%d&fromItem=", endpoint, limit) |
||||
|
err = doGoodReqPaginated(path, historydb.OrderDesc, &testExitsResponse{}, appendIter) |
||||
|
assert.NoError(t, err) |
||||
|
assertExitAPIs(t, flipedExits, fetchedExits) |
||||
|
// 400
|
||||
|
path = fmt.Sprintf( |
||||
|
"%s?accountIndex=%s&hermezEthereumAddress=%s", |
||||
|
endpoint, idx, tc.usrAddr, |
||||
|
) |
||||
|
err = doBadReq("GET", path, nil, 400) |
||||
|
assert.NoError(t, err) |
||||
|
path = fmt.Sprintf("%s?tokenId=X", endpoint) |
||||
|
err = doBadReq("GET", path, nil, 400) |
||||
|
assert.NoError(t, err) |
||||
|
// 404
|
||||
|
path = fmt.Sprintf("%s?batchNum=999999", endpoint) |
||||
|
err = doBadReq("GET", path, nil, 404) |
||||
|
assert.NoError(t, err) |
||||
|
path = fmt.Sprintf("%s?limit=1000&fromItem=999999", endpoint) |
||||
|
err = doBadReq("GET", path, nil, 404) |
||||
|
assert.NoError(t, err) |
||||
|
} |
||||
|
|
||||
|
func TestGetExit(t *testing.T) { |
||||
|
// Get all txs by their ID
|
||||
|
endpoint := apiURL + "exits/" |
||||
|
fetchedExits := []testExit{} |
||||
|
for _, exit := range tc.exits { |
||||
|
fetchedExit := testExit{} |
||||
|
assert.NoError( |
||||
|
t, doGoodReq( |
||||
|
"GET", |
||||
|
fmt.Sprintf("%s%d/%s", endpoint, exit.BatchNum, exit.AccountIdx), |
||||
|
nil, &fetchedExit, |
||||
|
), |
||||
|
) |
||||
|
fetchedExits = append(fetchedExits, fetchedExit) |
||||
|
} |
||||
|
assertExitAPIs(t, tc.exits, fetchedExits) |
||||
|
// 400
|
||||
|
err := doBadReq("GET", endpoint+"1/haz:BOOM:1", nil, 400) |
||||
|
assert.NoError(t, err) |
||||
|
err = doBadReq("GET", endpoint+"-1/hez:BOOM:1", nil, 400) |
||||
|
assert.NoError(t, err) |
||||
|
// 404
|
||||
|
err = doBadReq("GET", endpoint+"494/hez:XXX:1", nil, 404) |
||||
|
assert.NoError(t, err) |
||||
|
} |
||||
|
|
||||
|
func assertExitAPIs(t *testing.T, expected, actual []testExit) { |
||||
|
require.Equal(t, len(expected), len(actual)) |
||||
|
for i := 0; i < len(actual); i++ { //nolint len(actual) won't change within the loop
|
||||
|
actual[i].ItemID = 0 |
||||
|
actual[i].Token.ItemID = 0 |
||||
|
if expected[i].Token.USDUpdate == nil { |
||||
|
assert.Equal(t, expected[i].Token.USDUpdate, actual[i].Token.USDUpdate) |
||||
|
} else { |
||||
|
assert.Equal(t, expected[i].Token.USDUpdate.Unix(), actual[i].Token.USDUpdate.Unix()) |
||||
|
expected[i].Token.USDUpdate = actual[i].Token.USDUpdate |
||||
|
} |
||||
|
assert.Equal(t, expected[i], actual[i]) |
||||
|
} |
||||
|
} |