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

Add provider for Avalanche Account #86

Merged
merged 4 commits into from
Nov 21, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
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