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

Automatic account artifacts fetching #72

Merged
merged 5 commits into from
Mar 8, 2022
Merged
Show file tree
Hide file tree
Changes from 4 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
111 changes: 111 additions & 0 deletions src/account-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { Numeric } from "./types";
import { hash } from "starknet";
import { BigNumberish, toBN } from "starknet/utils/number";
import * as ellipticCurve from "starknet/utils/ellipticCurve";
import { ec } from "elliptic";
import { HardhatRuntimeEnvironment } from "hardhat/types";
import * as fs from "fs";
import path from "path";
import {
ABI_SUFFIX,
ACCOUNT_ARTIFACTS_VERSION,
ACCOUNT_CONTRACT_ARTIFACTS_ROOT_PATH,
GITHUB_ACCOUNT_ARTIFACTS_URL
} from "./constants";
import axios from "axios";

/*
* Helper cryptography functions for Key generation and message signing
*/

export function generateRandomStarkPrivateKey(length = 63) {
const characters = "0123456789ABCDEF";
let result = "";
for (let i = 0; i < length; ++i) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
return toBN(result, "hex");
}

/**
* Returns a signature which is the result of signing a message
*
* @param keyPair
* @param accountAddress
* @param nonce
* @param functionSelector
* @param toAddress
* @param calldata
* @returns the signature
*/
export function sign(
keyPair: ec.KeyPair,
accountAddress: string,
nonce: BigNumberish,
functionSelector: string,
toAddress: string,
calldata: BigNumberish[]
): Numeric[] {
const msgHash = hash.computeHashOnElements([
toBN(accountAddress.substring(2), "hex"),
toBN(toAddress.substring(2), "hex"),
functionSelector,
toBN(hash.computeHashOnElements(calldata).substring(2), "hex"),
nonce
]);

const signedMessage = ellipticCurve.sign(keyPair, BigInt(msgHash).toString(16));
const signature = [
BigInt("0x" + signedMessage[0].toString(16)),
BigInt("0x" + signedMessage[1].toString(16))
];
return signature;
}

export async function handleAccountContractArtifacts(
accountType: string,
artifactsName: string,
hre: HardhatRuntimeEnvironment
) {
// Name of the artifacts' parent folder
const targetPath = artifactsName + ".cairo";

// Full path to where the artifacts will be saved
const artifactsTargetPath = path.join(
hre.config.paths.starknetArtifacts,
ACCOUNT_CONTRACT_ARTIFACTS_ROOT_PATH,
ACCOUNT_ARTIFACTS_VERSION,
targetPath
);

if (!fs.existsSync(artifactsTargetPath)) {
const baseArtifactsPath = path.join(
hre.config.paths.starknetArtifacts,
ACCOUNT_CONTRACT_ARTIFACTS_ROOT_PATH
);
if (fs.existsSync(baseArtifactsPath)) {
fs.rmSync(baseArtifactsPath, { recursive: true, force: true });
FabijanC marked this conversation as resolved.
Show resolved Hide resolved
}

const jsonArtifact = artifactsName + ".json";
const abiArtifact = artifactsName + ABI_SUFFIX;

fs.mkdirSync(artifactsTargetPath, { recursive: true });

const fileLocationUrl = GITHUB_ACCOUNT_ARTIFACTS_URL.concat("/", accountType, "/", targetPath, "/");
FabijanC marked this conversation as resolved.
Show resolved Hide resolved

await downloadArtifact(jsonArtifact, artifactsTargetPath, fileLocationUrl);
await downloadArtifact(abiArtifact, artifactsTargetPath, fileLocationUrl);
}
}
async function downloadArtifact(artifact: string, artifactsTargetPath: string, fileLocationUrl: string) {
FabijanC marked this conversation as resolved.
Show resolved Hide resolved
const rawFileURL = fileLocationUrl.concat(artifact);
const response = await axios.get(rawFileURL, {
transformResponse: (res) => {
return res;
},
responseType: "json"
});

fs.writeFileSync(path.join(artifactsTargetPath, artifact), response.data);
}
86 changes: 28 additions & 58 deletions src/account.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { Choice, Numeric, StarknetContract, StringMap } from "./types";
import { Choice, StarknetContract, StringMap } from "./types";
import { PLUGIN_NAME } from "./constants";
import { HardhatPluginError } from "hardhat/plugins";
import { HardhatRuntimeEnvironment } from "hardhat/types";
import { hash } from "starknet";
import * as ellipticCurve from "starknet/utils/ellipticCurve";
import { BigNumberish, toBN } from "starknet/utils/number";
import { toBN } from "starknet/utils/number";
import { ec } from "elliptic";
import {
generateRandomStarkPrivateKey,
handleAccountContractArtifacts,
sign
} from "./account-utils";

/**
* Representation of an Account.
Expand Down Expand Up @@ -51,6 +56,8 @@ export abstract class Account {
*/
export class OpenZeppelinAccount extends Account {
static readonly EXECUTION_FUNCTION_NAME = "execute";
static readonly ACCOUNT_TYPE_NAME = "OpenZeppelinAccount";
static readonly ACCOUNT_ARTIFACTS_NAME = "Account";

constructor(
starknetContract: StarknetContract,
Expand Down Expand Up @@ -124,28 +131,39 @@ export class OpenZeppelinAccount extends Account {
);
}

static async deployFromABI(
accountContract: string,
hre: HardhatRuntimeEnvironment
): Promise<Account> {
static async deployFromABI(hre: HardhatRuntimeEnvironment): Promise<Account> {
await handleAccountContractArtifacts(
OpenZeppelinAccount.ACCOUNT_TYPE_NAME,
OpenZeppelinAccount.ACCOUNT_ARTIFACTS_NAME,
hre
);

const starkPrivateKey = generateRandomStarkPrivateKey();
const keyPair = ellipticCurve.getKeyPair(starkPrivateKey);
const publicKey = ellipticCurve.getStarkKey(keyPair);

const contractFactory = await hre.starknet.getContractFactory(accountContract);
const contractFactory = await hre.starknet.getContractFactory(
OpenZeppelinAccount.ACCOUNT_ARTIFACTS_NAME
);
const contract = await contractFactory.deploy({ _public_key: BigInt(publicKey) });
const privateKey = "0x" + starkPrivateKey.toString(16);

return new OpenZeppelinAccount(contract, privateKey, publicKey, keyPair);
}

static async getAccountFromAddress(
accountContract: string,
address: string,
privateKey: string,
hre: HardhatRuntimeEnvironment
): Promise<Account> {
const contractFactory = await hre.starknet.getContractFactory(accountContract);
await handleAccountContractArtifacts(
OpenZeppelinAccount.ACCOUNT_TYPE_NAME,
OpenZeppelinAccount.ACCOUNT_ARTIFACTS_NAME,
hre
);

const contractFactory = await hre.starknet.getContractFactory(
OpenZeppelinAccount.ACCOUNT_ARTIFACTS_NAME
);
const contract = contractFactory.getContractAt(address);

const { res: expectedPubKey } = await contract.call("get_public_key");
Expand All @@ -163,51 +181,3 @@ export class OpenZeppelinAccount extends Account {
return new OpenZeppelinAccount(contract, privateKey, publicKey, keyPair);
}
}

/*
* Helper cryptography functions for Key generation and message signing
*/

function generateRandomStarkPrivateKey(length = 63) {
const characters = "0123456789ABCDEF";
let result = "";
for (let i = 0; i < length; ++i) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
}
return toBN(result, "hex");
}

/**
* Returns a signature which is the result of signing a message
*
* @param keyPair
* @param accountAddress
* @param nonce
* @param functionSelector
* @param toAddress
* @param calldata
* @returns the signature
*/
function sign(
keyPair: ec.KeyPair,
accountAddress: string,
nonce: BigNumberish,
functionSelector: string,
toAddress: string,
calldata: BigNumberish[]
): Numeric[] {
const msgHash = hash.computeHashOnElements([
toBN(accountAddress.substring(2), "hex"),
toBN(toAddress.substring(2), "hex"),
functionSelector,
toBN(hash.computeHashOnElements(calldata).substring(2), "hex"),
nonce
]);

const signedMessage = ellipticCurve.sign(keyPair, BigInt(msgHash).toString(16));
const signature = [
BigInt("0x" + signedMessage[0].toString(16)),
BigInt("0x" + signedMessage[1].toString(16))
];
return signature;
}
4 changes: 4 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ export const DEFAULT_STARKNET_ACCOUNT_PATH = "~/.starknet_accounts";
export const DOCKER_REPOSITORY = "shardlabs/cairo-cli";
export const DEFAULT_DOCKER_IMAGE_TAG = "0.7.1";

export const ACCOUNT_CONTRACT_ARTIFACTS_ROOT_PATH = "account-contract-artifacts";
export const ACCOUNT_ARTIFACTS_VERSION = "0.1.0";
export const GITHUB_ACCOUNT_ARTIFACTS_URL = `https://raw.githubusercontent.com/Shard-Labs/starknet-hardhat-example/plugin/account-contract-artifacts/${ACCOUNT_ARTIFACTS_VERSION}`;
FabijanC marked this conversation as resolved.
Show resolved Hide resolved

export const ALPHA_TESTNET = "alpha-goerli";
export const ALPHA_TESTNET_INTERNALLY = "alpha";
export const ALPHA_MAINNET = "alpha-mainnet";
Expand Down
6 changes: 2 additions & 4 deletions src/extend-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export async function getContractFactoryUtil(
`${contractPath}.cairo`,
`${path.basename(contractPath)}.json`
);

const metadataPath = await findPath(artifactsPath, metadataSearchTarget);
if (!metadataPath) {
throw new HardhatPluginError(PLUGIN_NAME, `Could not find metadata for ${contractPath}`);
Expand Down Expand Up @@ -95,14 +96,13 @@ export function getWalletUtil(name: string, hre: HardhatRuntimeEnvironment) {
}

export async function deployAccountFromABIUtil(
accountContract: string,
accountType: AccountImplementationType,
hre: HardhatRuntimeEnvironment
): Promise<Account> {
let account: Account;
switch (accountType) {
case "OpenZeppelin":
account = await OpenZeppelinAccount.deployFromABI(accountContract, hre);
FabijanC marked this conversation as resolved.
Show resolved Hide resolved
account = await OpenZeppelinAccount.deployFromABI(hre);
break;
default:
throw new HardhatPluginError(PLUGIN_NAME, "Invalid account type requested.");
Expand All @@ -112,7 +112,6 @@ export async function deployAccountFromABIUtil(
}

export async function getAccountFromAddressUtil(
accountContract: string,
address: string,
privateKey: string,
accountType: AccountImplementationType,
Expand All @@ -122,7 +121,6 @@ export async function getAccountFromAddressUtil(
switch (accountType) {
case "OpenZeppelin":
account = await OpenZeppelinAccount.getAccountFromAddress(
accountContract,
address,
privateKey,
hre
Expand Down
7 changes: 3 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,14 +181,13 @@ extendEnvironment((hre) => {

devnet: lazyObject(() => new DevnetUtils(hre)),

deployAccountFromABI: async (accountContract, accountType) => {
const account = await deployAccountFromABIUtil(accountContract, accountType, hre);
deployAccountFromABI: async (accountType) => {
const account = await deployAccountFromABIUtil(accountType, hre);
return account;
},

getAccountFromAddress: async (accountContract, address, privateKey, accountType) => {
getAccountFromAddress: async (address, privateKey, accountType) => {
const account = await getAccountFromAddressUtil(
accountContract,
address,
privateKey,
accountType,
Expand Down
2 changes: 0 additions & 2 deletions src/type-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,6 @@ declare module "hardhat/types/runtime" {
* @returns an Account object
*/
deployAccountFromABI: (
accountContract: string,
accountType: AccountImplementationType
) => Promise<Account>;

Expand All @@ -155,7 +154,6 @@ declare module "hardhat/types/runtime" {
* @returns an Account object
*/
getAccountFromAddress: (
accountContract: string,
address: string,
privateKey: string,
accountType: AccountImplementationType
Expand Down
12 changes: 11 additions & 1 deletion test/general-tests/account-test/check.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
#!/bin/bash
set -e

npx hardhat starknet-compile contracts/contract.cairo contracts/Account.cairo
DUMMY_DIR=starknet-artifacts/account-contract-artifacts/0.0.0/Account.cairo

mkdir "$DUMMY_DIR"
FabijanC marked this conversation as resolved.
Show resolved Hide resolved

npx hardhat starknet-compile contracts/contract.cairo

npx hardhat test --no-compile test/account-test.ts

echo "Testing removal of dummy directory"
if [ -d "$DUMMY_DIR" ]; then
exit 1
FabijanC marked this conversation as resolved.
Show resolved Hide resolved
fi
echo "Success"