package e2e import ( "context" "encoding/base64" "encoding/json" "fmt" "io/ioutil" "math" "math/big" "net/http" "net/url" "testing" "time" "github.com/certusone/wormhole/bridge/pkg/devnet" "github.com/certusone/wormhole/bridge/pkg/ethereum" "github.com/certusone/wormhole/bridge/pkg/ethereum/erc20" "github.com/certusone/wormhole/bridge/pkg/vaa" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/tendermint/tendermint/libs/rand" "github.com/terra-project/terra.go/client" "github.com/terra-project/terra.go/key" "github.com/terra-project/terra.go/msg" "github.com/terra-project/terra.go/tx" "github.com/tidwall/gjson" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/kubernetes" ) type lockAssetsMsg struct { Params lockAssetsParams `json:"lock_assets"` } type increaseAllowanceMsg struct { Params increaseAllowanceParams `json:"increase_allowance"` } type lockAssetsParams struct { Asset string `json:"asset"` Amount string `json:"amount"` Recipient []byte `json:"recipient"` TargetChain uint8 `json:"target_chain"` Nonce uint32 `json:"nonce"` } type increaseAllowanceParams struct { Spender string `json:"spender"` Amount string `json:"amount"` } // TerraClient encapsulates Terra LCD client and fee payer signing address type TerraClient struct { lcdClient client.LCDClient address msg.AccAddress } const ( feeAmount = 10000 feeDenomination = "uluna" ) func (tc TerraClient) lockAssets(t *testing.T, ctx context.Context, token string, amount *big.Int, recipient [32]byte, targetChain uint8, nonce uint32) (*client.TxResponse, error) { bridgeContract, err := msg.AccAddressFromBech32(devnet.TerraBridgeAddress) if err != nil { return nil, err } tokenContract, err := msg.AccAddressFromBech32(token) if err != nil { return nil, err } // Create tx increaseAllowanceCall, err := json.Marshal(increaseAllowanceMsg{ Params: increaseAllowanceParams{ Spender: devnet.TerraBridgeAddress, Amount: amount.String(), }}) if err != nil { return nil, err } lockAssetsCall, err := json.Marshal(lockAssetsMsg{ Params: lockAssetsParams{ Asset: token, Amount: amount.String(), Recipient: recipient[:], TargetChain: targetChain, Nonce: nonce, }}) if err != nil { return nil, err } t.Logf("increaseAllowanceCall\n %s", increaseAllowanceCall) t.Logf("lockAssetsCall\n %s", lockAssetsCall) executeIncreaseAllowance := msg.NewExecuteContract(tc.address, tokenContract, increaseAllowanceCall, msg.NewCoins()) executeLockAssets := msg.NewExecuteContract(tc.address, bridgeContract, lockAssetsCall, msg.NewCoins(msg.NewInt64Coin(feeDenomination, feeAmount))) transaction, err := tc.lcdClient.CreateAndSignTx(ctx, client.CreateTxOptions{ Msgs: []msg.Msg{ executeIncreaseAllowance, executeLockAssets, }, Fee: tx.StdFee{ Gas: msg.NewInt(0), Amount: msg.NewCoins(), }, }) if err != nil { return nil, err } // Broadcast return tc.lcdClient.Broadcast(ctx, transaction) } // NewTerraClient creates new TerraClient instance to work func NewTerraClient() (*TerraClient, error) { // Derive Raw Private Key privKey, err := key.DerivePrivKey(devnet.TerraFeePayerKey, key.CreateHDPath(0, 0)) if err != nil { return nil, err } // Generate StdPrivKey tmKey, err := key.StdPrivKeyGen(privKey) if err != nil { return nil, err } // Generate Address from Public Key address := msg.AccAddress(tmKey.PubKey().Address()) // Terra client lcdClient := client.NewLCDClient( devnet.TerraLCDURL, devnet.TerraChainID, msg.NewDecCoinFromDec("uusd", msg.NewDecFromIntWithPrec(msg.NewInt(15), 2)), // 0.15uusd msg.NewDecFromIntWithPrec(msg.NewInt(15), 1), tmKey, time.Second*15, ) return &TerraClient{ lcdClient: *lcdClient, address: address, }, nil } func getTerraBalance(ctx context.Context, token string) (*big.Int, error) { json, err := terraQuery(ctx, token, fmt.Sprintf("{\"balance\":{\"address\":\"%s\"}}", devnet.TerraMainTestAddress)) if err != nil { return nil, err } balance := gjson.Get(json, "result.balance").String() parsed, success := new(big.Int).SetString(balance, 10) if !success { return nil, fmt.Errorf("cannot parse balance: %s", balance) } return parsed, nil } func getAssetAddress(ctx context.Context, contract string, chain uint8, asset []byte) (string, error) { json, err := terraQuery(ctx, contract, fmt.Sprintf("{\"wrapped_registry\":{\"chain\":%d,\"address\":\"%s\"}}", chain, base64.StdEncoding.EncodeToString(asset))) if err != nil { return "", err } return gjson.Get(json, "result.address").String(), nil } func terraQuery(ctx context.Context, contract string, query string) (string, error) { requestURL := fmt.Sprintf("%s/wasm/contracts/%s/store?query_msg=%s", devnet.TerraLCDURL, contract, url.QueryEscape(query)) req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil) if err != nil { return "", fmt.Errorf("http request error: %w", err) } client := &http.Client{ Timeout: time.Second * 15, } resp, err := client.Do(req) if err != nil { return "", fmt.Errorf("http execution error: %w", err) } body, err := ioutil.ReadAll(resp.Body) if err != nil { return "", fmt.Errorf("http read error: %w", err) } return string(body), nil } // waitTerraAsset waits for asset contract to be deployed on terra func waitTerraAsset(t *testing.T, ctx context.Context, contract string, chain uint8, asset []byte) (string, error) { ctx, cancel := context.WithTimeout(ctx, 90*time.Second) defer cancel() assetAddress := "" err := wait.PollUntil(3*time.Second, func() (bool, error) { address, err := getAssetAddress(ctx, contract, chain, asset) if err != nil { t.Log(err) return false, nil } // Check the case if request was successful, but asset address is not yet in the registry if address == "" { return false, nil } t.Logf("Returning asset: %s", address) assetAddress = address return true, nil }, ctx.Done()) if err != nil { t.Error(err) } return assetAddress, err } // waitTerraBalance waits for target account before to increase. func waitTerraBalance(t *testing.T, ctx context.Context, token string, before *big.Int, target int64) { ctx, cancel := context.WithTimeout(ctx, 60*time.Second) defer cancel() err := wait.PollUntil(1*time.Second, func() (bool, error) { after, err := getTerraBalance(ctx, token) if err != nil { return false, err } d := new(big.Int).Sub(after, before) t.Logf("CW20 balance after: %d -> %d, delta %d", before, after, d) if after.Cmp(before) != 0 { if d.Cmp(new(big.Int).SetInt64(target)) != 0 { t.Errorf("expected CW20 delta of %v, got: %v", target, d) } return true, nil } return false, nil }, ctx.Done()) if err != nil { t.Error(err) } } func waitTerraUnknownBalance(t *testing.T, ctx context.Context, contract string, chain uint8, asset []byte, before *big.Int, target int64) { token, err := waitTerraAsset(t, ctx, contract, chain, asset) if err != nil { return } ctx, cancel := context.WithTimeout(ctx, 90*time.Second) defer cancel() err = wait.PollUntil(3*time.Second, func() (bool, error) { after, err := getTerraBalance(ctx, token) if err != nil { return false, err } d := new(big.Int).Sub(after, before) t.Logf("CW20 balance after: %d -> %d, delta %d", before, after, d) if after.Cmp(before) != 0 { if d.Cmp(new(big.Int).SetInt64(target)) != 0 { t.Errorf("expected CW20 delta of %v, got: %v", target, d) } return true, nil } return false, nil }, ctx.Done()) if err != nil { t.Error(err) } } func testTerraLockup(t *testing.T, ctx context.Context, tc *TerraClient, c *kubernetes.Clientset, token string, destination string, amount int64, precisionLoss int) { // Store balance of source CW20 token beforeCw20, err := getTerraBalance(ctx, token) if err != nil { t.Log(err) // account may not yet exist, defaults to 0 } t.Logf("CW20 balance: %v", beforeCw20) // Store balance of destination SPL token beforeSPL, err := getSPLBalance(ctx, c, destination) if err != nil { t.Fatal(err) } t.Logf("SPL balance: %d", beforeSPL) // Send lockup tx, err := tc.lockAssets( t, ctx, // asset address token, // token amount new(big.Int).SetInt64(amount), // recipient address on target chain devnet.MustBase58ToEthAddress(destination), // target chain vaa.ChainIDSolana, // random nonce rand.Uint32(), ) if err != nil { t.Error(err) } t.Logf("sent lockup tx: %s", tx.TxHash) // Destination account increases by full amount. waitSPLBalance(t, ctx, c, destination, beforeSPL, int64(float64(amount)/math.Pow10(precisionLoss))) // Source account decreases by the full amount. waitTerraBalance(t, ctx, token, beforeCw20, -int64(amount)) } func testTerraToEthLockup(t *testing.T, ctx context.Context, tc *TerraClient, ec *ethclient.Client, tokenAddr string, destination common.Address, amount int64, precisionGain int) { token, err := erc20.NewErc20(destination, ec) if err != nil { panic(err) } // Store balance of source CW20 token beforeCw20, err := getTerraBalance(ctx, tokenAddr) if err != nil { t.Log(err) // account may not yet exist, defaults to 0 beforeCw20 = new(big.Int) } t.Logf("CW20 balance: %v", beforeCw20) /// Store balance of wrapped destination token beforeErc20, err := token.BalanceOf(nil, devnet.GanacheClientDefaultAccountAddress) if err != nil { t.Log(err) // account may not yet exist, defaults to 0 beforeErc20 = new(big.Int) } t.Logf("ERC20 balance: %v", beforeErc20) // Send lockup tx, err := tc.lockAssets( t, ctx, // asset address tokenAddr, // token amount new(big.Int).SetInt64(amount), // recipient address on target chain ethereum.PadAddress(devnet.GanacheClientDefaultAccountAddress), // target chain vaa.ChainIDEthereum, // random nonce rand.Uint32(), ) if err != nil { t.Error(err) } t.Logf("sent lockup tx: %s", tx.TxHash) // Destination account increases by full amount. waitEthBalance(t, ctx, token, beforeErc20, int64(float64(amount)*math.Pow10(precisionGain))) // Source account decreases by the full amount. waitTerraBalance(t, ctx, tokenAddr, beforeCw20, -int64(amount)) }