Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support Solana wallet address in zetacore for SOL withdrawals #2518

Merged
merged 5 commits into from
Jul 22, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
* [2372](https://github.com/zeta-chain/node/pull/2372) - add queries for tss fund migration info
* [2416](https://github.com/zeta-chain/node/pull/2416) - add Solana chain information
* [2465](https://github.com/zeta-chain/node/pull/2465) - add Solana inbound SOL token observation
* [2518](https://github.com/zeta-chain/node/pull/2518) - add support for Solana address in zetacore

### Refactor

Expand Down
19 changes: 18 additions & 1 deletion pkg/chains/address.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package chains

import (
"errors"
"fmt"
"strings"

"github.com/btcsuite/btcutil"
eth "github.com/ethereum/go-ethereum/common"
"github.com/gagliardetto/solana-go"
"github.com/pkg/errors"
)

type Address string
Expand Down Expand Up @@ -87,6 +88,22 @@ func DecodeBtcAddress(inputAddress string, chainID int64) (address btcutil.Addre
return
}

// DecodeSolanaWalletAddress decodes a Solana wallet address from a given string
func DecodeSolanaWalletAddress(inputAddress string) (pk solana.PublicKey, err error) {
ws4charlie marked this conversation as resolved.
Show resolved Hide resolved
// decode the Base58 encoded address
pk, err = solana.PublicKeyFromBase58(inputAddress)
if err != nil {
return solana.PublicKey{}, errors.Wrapf(err, "error decoding solana wallet address %s", inputAddress)
}

// accept address that is generated from keypair
// reject off-curve address such as program derived address from 'findProgramAddress'
ws4charlie marked this conversation as resolved.
Show resolved Hide resolved
if !pk.IsOnCurve() {
return solana.PublicKey{}, fmt.Errorf("address %s is not on ed25519 curve", inputAddress)
}
return
}

// IsBtcAddressSupported returns true if the given BTC address is supported
func IsBtcAddressSupported(addr btcutil.Address) bool {
switch addr.(type) {
Expand Down
52 changes: 52 additions & 0 deletions pkg/chains/address_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,58 @@ func TestDecodeBtcAddress(t *testing.T) {
})
}

func Test_DecodeSolanaWalletAddress(t *testing.T) {
tests := []struct {
name string
address string
want string
fail bool
ws4charlie marked this conversation as resolved.
Show resolved Hide resolved
message string
}{
{
name: "should decode a valid Solana wallet address",
address: "DCAK36VfExkPdAkYUQg6ewgxyinvcEyPLyHjRbmveKFw",
want: "DCAK36VfExkPdAkYUQg6ewgxyinvcEyPLyHjRbmveKFw",
fail: false,
},
{
name: "should fail to decode an address with invalid length",
address: "DCAK36VfExkPdAkYUQg6ewgxyinvcEyPLyHjRbmveK",
want: "",
fail: true,
message: "error decoding solana wallet address",
},
{
name: "should fail to decode an invalid Base58 address",
address: "DCAK36VfExkPdAkYUQg6ewgxyinvcEyPLyHjRbmveKFl", // contains invalid character 'l'
want: "",
fail: true,
message: "error decoding solana wallet address",
},
{
name: "should fail to decode a program derived address (not on ed25519 curve)",
address: "9dcAyYG4bawApZocwZSyJBi9Mynf5EuKAJfifXdfkqik",
want: "",
fail: true,
message: "is not on ed25519 curve",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pk, err := DecodeSolanaWalletAddress(tt.address)
if tt.fail {
require.Error(t, err)
require.Contains(t, err.Error(), tt.message)
return
}

require.NoError(t, err)
require.Equal(t, tt.want, pk.String())
})
}
}

func Test_IsBtcAddressSupported_P2TR(t *testing.T) {
tests := []struct {
name string
Expand Down
16 changes: 12 additions & 4 deletions pkg/chains/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,14 @@ func (chain Chain) IsExternalChain() bool {
// on EVM chain, it is 20Bytes
// on Bitcoin chain, it is P2WPKH address, []byte(bech32 encoded string)
func (chain Chain) EncodeAddress(b []byte) (string, error) {
if chain.Consensus == Consensus_ethereum {
switch chain.Consensus {
case Consensus_ethereum:
addr := ethcommon.BytesToAddress(b)
if addr == (ethcommon.Address{}) {
return "", fmt.Errorf("invalid EVM address")
}
return addr.Hex(), nil
} else if chain.Consensus == Consensus_bitcoin {
case Consensus_bitcoin:
addrStr := string(b)
chainParams, err := GetBTCChainParams(chain.ChainId)
if err != nil {
Expand All @@ -72,8 +73,15 @@ func (chain Chain) EncodeAddress(b []byte) (string, error) {
return "", fmt.Errorf("address is not for network %s", chainParams.Name)
}
return addrStr, nil
case Consensus_solana_consensus:
pk, err := DecodeSolanaWalletAddress(string(b))
if err != nil {
return "", err
}
return pk.String(), nil
default:
return "", fmt.Errorf("chain (%d) not supported", chain.ChainId)
ws4charlie marked this conversation as resolved.
Show resolved Hide resolved
}
return "", fmt.Errorf("chain (%d) not supported", chain.ChainId)
}

func (chain Chain) IsEVMChain() bool {
Expand Down Expand Up @@ -111,7 +119,7 @@ func IsEVMChain(chainID int64, additionalChains []Chain) bool {
// additionalChains is a list of additional chains to search from
// in practice, it is used in the protocol to dynamically support new chains without doing an upgrade
func IsBitcoinChain(chainID int64, additionalChains []Chain) bool {
return ChainIDInChainList(chainID, ChainListByConsensus(Consensus_bitcoin, additionalChains))
return ChainIDInChainList(chainID, ChainListByNetwork(Network_btc, additionalChains))
}

// IsSolanaChain returns true if the chain is a Solana chain
Expand Down
53 changes: 30 additions & 23 deletions pkg/chains/chain_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package chains_test

import (
"github.com/zeta-chain/zetacore/testutil/sample"
"testing"

"github.com/zeta-chain/zetacore/testutil/sample"

"github.com/btcsuite/btcd/chaincfg"
ethcommon "github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -146,43 +147,43 @@ func TestChain_EncodeAddress(t *testing.T) {
wantErr bool
}{
{
name: "should error if b is not a valid address on the bitcoin network",
chain: chains.Chain{
ChainName: chains.ChainName_btc_testnet,
ChainId: 18332,
Consensus: chains.Consensus_bitcoin,
},
name: "should error if b is not a valid address on the bitcoin network",
chain: chains.BitcoinTestnet,
b: []byte("bc1qk0cc73p8m7hswn8y2q080xa4e5pxapnqgp7h9c"),
want: "",
wantErr: true,
},
{
name: "should pass if b is a valid address on the network",
chain: chains.Chain{
ChainName: chains.ChainName_btc_mainnet,
ChainId: 8332,
Consensus: chains.Consensus_bitcoin,
},
name: "should pass if b is a valid address on the network",
chain: chains.BitcoinMainnet,
b: []byte("bc1qk0cc73p8m7hswn8y2q080xa4e5pxapnqgp7h9c"),
want: "bc1qk0cc73p8m7hswn8y2q080xa4e5pxapnqgp7h9c",
wantErr: false,
},
{
name: "should error if b is not a valid address on the evm network",
chain: chains.Chain{
ChainName: chains.ChainName_goerli_testnet,
ChainId: 5,
},
name: "should pass if b is a valid wallet address on the solana network",
chain: chains.SolanaMainnet,
b: []byte("DCAK36VfExkPdAkYUQg6ewgxyinvcEyPLyHjRbmveKFw"),
want: "DCAK36VfExkPdAkYUQg6ewgxyinvcEyPLyHjRbmveKFw",
wantErr: false,
},
{
name: "should error if b is not a valid Base58 address",
chain: chains.SolanaMainnet,
b: []byte("9G0P8HkKqegZ7B6cE2hGvkZjHjSH14WZXDNZQmwYLokAc"), // contains invalid digit '0'
want: "",
wantErr: true,
},
{
name: "should error if b is not a valid address on the evm network",
chain: chains.Ethereum,
b: ethcommon.Hex2Bytes("0x321"),
want: "",
wantErr: true,
},
{
name: "should pass if b is a valid address on the evm network",
chain: chains.Chain{
ChainName: chains.ChainName_goerli_testnet,
ChainId: 5,
},
name: "should pass if b is a valid address on the evm network",
chain: chains.Ethereum,
b: []byte("0x321"),
want: "0x0000000000000000000000000000003078333231",
wantErr: false,
Expand Down Expand Up @@ -294,6 +295,12 @@ func TestDecodeAddressFromChainID(t *testing.T) {
addr: "bc1qk0cc73p8m7hswn8y2q080xa4e5pxapnqgp7h9c",
want: []byte("bc1qk0cc73p8m7hswn8y2q080xa4e5pxapnqgp7h9c"),
},
{
name: "Solana",
chainID: chains.SolanaMainnet.ChainId,
addr: "DCAK36VfExkPdAkYUQg6ewgxyinvcEyPLyHjRbmveKFw",
want: []byte("DCAK36VfExkPdAkYUQg6ewgxyinvcEyPLyHjRbmveKFw"),
},
{
name: "Non-supported chain",
chainID: 9999,
Expand Down
Loading