Skip to content

Commit

Permalink
Add provider for Avalanche Account (#86)
Browse files Browse the repository at this point in the history
Feat: Providers are not handled by Avalanche account

Solution: Add metamask support and tests
  • Loading branch information
Rgascoin authored Nov 21, 2022
1 parent 3fb16b0 commit 76a9570
Show file tree
Hide file tree
Showing 8 changed files with 186 additions and 31 deletions.
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ This is the list of currently supported Account types. For each of them you can:

On top of that some accounts allow you to **encrypt** messages, retrieve an account from a **browser based** wallet (ex: Metamask), or from a **Ledger** wallet.

| Chain | Encryption | Wallet | Ledger |
| --------- | ------------------ | ------------------ | ------ |
| Avalanche | :heavy_check_mark: | :x: | :x: |
| Cosmos | :x: | :x: | :x: |
| Ethereum | :heavy_check_mark: | :heavy_check_mark: | :x: |
| NULS2 | :heavy_check_mark: | :x: | :x: |
| Solana | :x: | :heavy_check_mark: | :x: |
| Substrate | :heavy_check_mark: | :x: | :x: |
| Tezos | :x: | :heavy_check_mark: | :x: |
| Chain | Encryption | Wallet | Ledger |
| --------- | ------------------ |---------------------| ------ |
| Avalanche | :heavy_check_mark: | :heavy_check_mark: | :x: |
| Cosmos | :x: | :x: | :x: |
| Ethereum | :heavy_check_mark: | :heavy_check_mark: | :x: |
| NULS2 | :heavy_check_mark: | :x: | :x: |
| Solana | :x: | :heavy_check_mark: | :x: |
| Substrate | :heavy_check_mark: | :x: | :x: |
| Tezos | :x: | :heavy_check_mark: | :x: |

## Running from source

Expand Down
1 change: 1 addition & 0 deletions examples/toolshed/src/components/SelectProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const availableKeypairs: Option[] = [
]

export const availableWallets: Option[] = [
{ label: 'Avalanche (via Metamask)', value: WalletChains.Avalanche },
{ label: 'Ethereum (via Metamask)', value: WalletChains.Ethereum },
{ label: 'Solana (via Phantom)', value: WalletChains.Solana },
]
Expand Down
5 changes: 3 additions & 2 deletions examples/toolshed/src/components/WalletConfig.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { solana, ethereum } from '../../../../src/accounts'
import { solana, ethereum, avalanche } from '../../../../src/accounts'
import { WalletChains } from '../model/chains'
import { dispatchAndConsume } from '../model/componentProps'
import { Actions } from '../reducer'


function WalletConfig({ dispatch, state } : dispatchAndConsume) {
const getAccountClass = () => (
state.selectedChain === WalletChains.Ethereum ? [ethereum, window.ethereum]
state.selectedChain === WalletChains.Avalanche ? [avalanche, window.ethereum]
: state.selectedChain === WalletChains.Ethereum ? [ethereum, window.ethereum]
: state.selectedChain === WalletChains.Solana ? [solana, window.phantom?.solana]
: [null, null]
)
Expand Down
1 change: 1 addition & 0 deletions examples/toolshed/src/model/chains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export enum KeypairChains {
}

export enum WalletChains {
Avalanche = "AVAX",
Ethereum = "ETH",
Solana = "SOL",
}
Expand Down
76 changes: 58 additions & 18 deletions src/accounts/avalanche.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,59 @@ import { BaseMessage, Chain } from "../messages/message";
import { decrypt as secp256k1_decrypt, encrypt as secp256k1_encrypt } from "eciesjs";
import { KeyPair } from "avalanche/dist/apis/avm";
import { Avalanche, BinTools, Buffer as AvaBuff } from "avalanche";
import { JsonRPCWallet, RpcChainType } from "../providers/JsonRPCWallet";
import { BaseProviderWallet } from "../providers/BaseProviderWallet";
import { providers } from "ethers";

/**
* AvalancheAccount implements the Account class for the Avalanche protocol.
* It is used to represent an Avalanche account when publishing a message on the Aleph network.
*/
export class AvalancheAccount extends Account {
private signer;
private signer?: KeyPair;
private provider?: BaseProviderWallet;

constructor(signer: KeyPair) {
super(signer.getAddressString());
this.signer = signer;
constructor(signerOrProvider: KeyPair | BaseProviderWallet, address: string) {
super(address);

if (signerOrProvider instanceof KeyPair) this.signer = signerOrProvider;
if (signerOrProvider instanceof BaseProviderWallet) this.provider = signerOrProvider;
}

override GetChain(): Chain {
return Chain.AVAX;
if (this.signer) return Chain.AVAX;
if (this.provider) return Chain.ETH;

throw new Error("Cannot determine chain");
}

/**
* Encrypt a content using the user's public key from the keypair
*
* @param content The content to encrypt.
*/
encrypt(content: Buffer): Buffer {
const publicKey = this.signer.getPublicKey().toString("hex");
return secp256k1_encrypt(publicKey, content);
async encrypt(content: Buffer): Promise<Buffer> {
const publicKey = this.signer?.getPublicKey().toString("hex") || (await this.provider?.getPublicKey());
if (publicKey) return secp256k1_encrypt(publicKey, content);

throw new Error("Cannot encrypt content");
}

/**
* Decrypt a given content using the private key from the keypair.
*
* @param encryptedContent The encrypted content to decrypt.
*/
decrypt(encryptedContent: Buffer): Buffer {
const secret = this.signer.getPrivateKey().toString("hex");
return secp256k1_decrypt(secret, encryptedContent);
async decrypt(encryptedContent: Buffer): Promise<Buffer> {
if (this.signer) {
const secret = this.signer.getPrivateKey().toString("hex");
return secp256k1_decrypt(secret, encryptedContent);
}
if (this.provider) {
const decrypted = await this.provider.decrypt(encryptedContent);
return Buffer.from(decrypted);
}
throw new Error("Cannot encrypt content");
}

private async digestMessage(message: Buffer) {
Expand All @@ -63,13 +81,18 @@ export class AvalancheAccount extends Account {
const buffer = GetVerificationBuffer(message);
const digest = await this.digestMessage(buffer);

const digestHex = digest.toString("hex");
const digestBuff = AvaBuff.from(digestHex, "hex");
if (this.signer) {
const digestHex = digest.toString("hex");
const digestBuff = AvaBuff.from(digestHex, "hex");
const signatureBuffer = this.signer?.sign(digestBuff);

const signatureBuffer = this.signer.sign(digestBuff);
const bintools = BinTools.getInstance();
const bintools = BinTools.getInstance();
return bintools.cb58Encode(signatureBuffer);
} else if (this.provider) {
return await this.provider.signMessage(buffer);
}

return bintools.cb58Encode(signatureBuffer);
throw new Error("Cannot sign message");
}
}

Expand Down Expand Up @@ -107,7 +130,24 @@ export async function getKeyPair(privateKey?: string): Promise<KeyPair> {
*/
export async function ImportAccountFromPrivateKey(privateKey: string): Promise<AvalancheAccount> {
const keyPair = await getKeyPair(privateKey);
return new AvalancheAccount(keyPair);
return new AvalancheAccount(keyPair, keyPair.getAddressString());
}

/**
* Get an account from a Web3 provider (ex: Metamask)
*
* @param {providers.ExternalProvider} provider from metamask
*/
export async function GetAccountFromProvider(provider: providers.ExternalProvider): Promise<AvalancheAccount> {
const avaxProvider = new providers.Web3Provider(provider);
const jrw = new JsonRPCWallet(avaxProvider);
await jrw.changeNetwork(RpcChainType.AVAX);

await jrw.connect();
if (jrw.address) {
return new AvalancheAccount(jrw, jrw.address);
}
throw new Error("Insufficient permissions");
}

/**
Expand All @@ -118,5 +158,5 @@ export async function NewAccount(): Promise<{ account: AvalancheAccount; private
const keypair = await getKeyPair();
const privateKey = keypair.getPrivateKey().toString("hex");

return { account: new AvalancheAccount(keypair), privateKey };
return { account: new AvalancheAccount(keypair, keypair.getAddressString()), privateKey };
}
36 changes: 36 additions & 0 deletions src/providers/JsonRPCWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,36 @@ import { BaseProviderWallet } from "./BaseProviderWallet";
const RPC_WARNING = `DEPRECATION WARNING:
Encryption/Decryption features may become obsolete, for more information: https://github.com/aleph-im/aleph-sdk-ts/issues/37`;

export enum RpcChainType {
ETH,
AVAX,
}

const ChainData = {
[RpcChainType.AVAX]: {
chainId: "0xA86A",
rpcUrls: ["https://api.avax.network/ext/bc/C/rpc"],
chainName: "Avalanche Mainnet",
nativeCurrency: {
name: "AVAX",
symbol: "AVAX",
decimals: 18,
},
blockExplorerUrls: ["https://snowtrace.io"],
},
[RpcChainType.ETH]: {
chainId: "0x1",
rpcUrls: ["https://mainnet.infura.io/v3/"],
chainName: "Ethereum Mainnet",
nativeCurrency: {
name: "ETH",
symbol: "ETH",
decimals: 18,
},
blockExplorerUrls: ["https://etherscan.io"],
},
};

/**
* Wrapper for JSON RPC Providers (ex: Metamask)
*/
Expand Down Expand Up @@ -51,4 +81,10 @@ export class JsonRPCWallet extends BaseProviderWallet {
if (!this.signer) throw new Error("Wallet not connected");
return this.signer.signMessage(data);
}

public async changeNetwork(chain: RpcChainType = RpcChainType.ETH): Promise<void> {
if (chain === RpcChainType.ETH) {
await this.provider.send("wallet_switchEthereumChain", [{ chainId: "0x1" }]);
} else await this.provider.send("wallet_addEthereumChain", [ChainData[chain]]);
}
}
72 changes: 70 additions & 2 deletions tests/accounts/avalanche.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { avalanche, post } from "../index";
import { DEFAULT_API_V2 } from "../../src/global";
import { ItemType } from "../../src/messages/message";
import { EthereumProvider } from "../providers/ethereumProvider";

describe("Avalanche accounts", () => {
const providerAddress = "0xB98bD7C7f656290071E52D1aA617D9cB4467Fd6D";
const providerPrivateKey = "de926db3012af759b4f24b5a51ef6afa397f04670f634aa4f48d4480417007f3";

it("should retrieved an avalanche keypair from an hexadecimal private key", async () => {
const { account, privateKey } = await avalanche.NewAccount();

Expand Down Expand Up @@ -32,17 +36,81 @@ describe("Avalanche accounts", () => {
expect(fromHex.address).toBe(fromCb58.address);
});

it("should import an ethereum accounts using a provider", async () => {
const provider = new EthereumProvider({
address: providerAddress,
privateKey: providerPrivateKey,
networkVersion: 31,
});

const accountFromProvider = await avalanche.GetAccountFromProvider(provider);
expect(accountFromProvider.address).toStrictEqual(providerAddress);
});

it("Should encrypt and decrypt some data with an Avalanche keypair", async () => {
const { account } = await avalanche.NewAccount();
const msg = Buffer.from("Laŭ Ludoviko Zamenhof bongustas freŝa ĉeĥa manĝaĵo kun spicoj");

const c = account.encrypt(msg);
const d = account.decrypt(c);
const c = await account.encrypt(msg);
const d = await account.decrypt(c);

expect(c).not.toBe(msg);
expect(d).toStrictEqual(msg);
});

it("Should encrypt and decrypt some data with an Avalanche account from provider", async () => {
const provider = new EthereumProvider({
address: providerAddress,
privateKey: providerPrivateKey,
networkVersion: 31,
});
const accountFromProvider = await avalanche.GetAccountFromProvider(provider);
const msg = Buffer.from("Laŭ Ludoviko Zamenhof bongustas freŝa ĉeĥa manĝaĵo kun spicoj");

const c = await accountFromProvider.encrypt(msg);
const d = await accountFromProvider.decrypt(c);

expect(c).not.toBe(msg);
expect(d).toStrictEqual(msg);
});

it("should publish a post message correctly with an account from a provider", async () => {
const provider = new EthereumProvider({
address: providerAddress,
privateKey: providerPrivateKey,
networkVersion: 31,
});
const accountFromProvider = await avalanche.GetAccountFromProvider(provider);
const content: { body: string } = {
body: "This message was posted from the typescript-SDK test suite",
};

const msg = await post.Publish({
APIServer: DEFAULT_API_V2,
channel: "TEST",
inlineRequested: true,
storageEngine: ItemType.ipfs,
account: accountFromProvider,
postType: "avalanche",
content: content,
});

expect(msg.item_hash).not.toBeUndefined();
setTimeout(async () => {
const amends = await post.Get({
types: "avalanche",
APIServer: DEFAULT_API_V2,
pagination: 200,
page: 1,
refs: [],
addresses: [],
tags: [],
hashes: [msg.item_hash],
});
expect(amends.posts[0].content).toStrictEqual(content);
});
});

it("should publish a post message correctly", async () => {
const { account } = await avalanche.NewAccount();
const content: { body: string } = {
Expand Down
8 changes: 8 additions & 0 deletions tests/providers/ethereumProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,14 @@ export class EthereumProvider implements IMockProvider {
return Promise.resolve(decrypted);
}

case "wallet_addEthereumChain": {
return Promise.resolve();
}

case "wallet_switchEthereumChain": {
return Promise.resolve();
}

default:
this.log(`resquesting missing method ${method}`);
// eslint-disable-next-line prefer-promise-reject-errors
Expand Down

0 comments on commit 76a9570

Please sign in to comment.