Skip to content

Commit

Permalink
Adapt Account to OpenZeppelin 0.1.0 and Argent 0.2.1 (#108)
Browse files Browse the repository at this point in the history
* Use enum for chain id

* Store account artifacts in plugin itself
  • Loading branch information
FabijanC authored May 26, 2022
1 parent c53f810 commit a8eb495
Show file tree
Hide file tree
Showing 11 changed files with 276 additions and 188 deletions.
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -490,12 +490,12 @@ module.exports = {

### Wallet

To configure a wallet for your project, specify it by using `wallets["walletName"]`.
To configure a wallet for your project, specify it by adding an entry to `wallets` in your hardhat config file.
You can specify multiple wallets/accounts.

The parameters for the wallet are:

- `accountName`: The name to give the account. If omitted, the default value `__default__ ` will be used;
- `accountName`: The name to give the account. If omitted, the default value `__default__` will be used;
- `modulePath`: The python module and wallet class of your chosen wallet provider;
- `accountPath`: The path where your wallet information will be saved.

Expand Down Expand Up @@ -545,8 +545,8 @@ function deployAccount(accountType: AccountImplementationType, options?: DeployA
```

- `accountType` - the implementation of the Account that you want to use; currently supported implementations:
- `"OpenZeppelin"`
- `"Argent"`
- `"OpenZeppelin"`- [v0.1.0](https://github.com/OpenZeppelin/cairo-contracts/releases/tag/v0.1.0)
- `"Argent"` - [v0.2.1](https://github.com/argentlabs/argent-contracts-starknet/releases/tag/v0.2.1)
- `options` - optional deployment parameters:
- `salt` - for fixing the account address
- `privateKey` - if you don't provide one, it will be randomly generated
Expand Down Expand Up @@ -610,8 +610,8 @@ const txHash = await account.multiInvoke(interactionArray);
const results = await account.multiCall(interactionArray);
```

OpenZeppelin and Argent account implementations work pretty much the same way, however Argent's has the additional signature verifications of a Guardian.
A key pair is generated for the Guardian the same way it is for the Signer, however if you want to change it, you must cast the `account` object to `ArgentAccount`
OpenZeppelin and Argent account implementation work almost the same: Argent implementation has the additional Guardian signature verification.
A key pair is generated for the Guardian the same way it is for the Signer, however if you want to change it, you must cast the `account` object to `ArgentAccount`.

```typescript
import { ArgentAccount } from "@shardlabs/starknet-hardhat-plugin/dist/account";
Expand Down
143 changes: 24 additions & 119 deletions src/account-utils.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
import { Numeric, StarknetContract, StringMap } from "./types";
import { Call, hash, RawCalldata } from "starknet";
import { BigNumberish, toBN } from "starknet/utils/number";
import { StarknetContract, StringMap } from "./types";
import { 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";
import { ABI_SUFFIX, ACCOUNT_ARTIFACTS_DIR } from "./constants";
import { flattenStringMap } from "./utils";

export type CallParameters = {
Expand All @@ -21,13 +14,6 @@ export type CallParameters = {
calldata?: StringMap;
};

type executeCallParameters = {
to: bigint;
selector: BigNumberish;
data_offset: number;
data_len: number;
};

type KeysType = {
publicKey: string;
privateKey: string;
Expand Down Expand Up @@ -58,142 +44,61 @@ export function signMultiCall(
return ellipticCurve.sign(keyPair, BigInt(messageHash).toString(16)).map(BigInt);
}

/**
* Prepares the calldata and hashes the message for the multicall execution
*
* @param accountAddress address of the account contract
* @param callParameters array witht the call parameters
* @param nonce current nonce
* @returns the message hash for the multicall and the arguments to execute it with
*/
export function handleMultiCall(
accountAddress: string,
callParameters: CallParameters[],
nonce: Numeric,
maxFee: Numeric,
version: Numeric
) {
// Transform a CallParameters array into Call array, so it can be used by the hash functions
const callArray: Call[] = callParameters.map((callParameters) => {
return {
contractAddress: callParameters.toContract.address,
entrypoint: callParameters.functionName,
calldata: callParameters.toContract.adaptInput(
callParameters.functionName,
callParameters.calldata
)
};
});

const executeCallArray: executeCallParameters[] = [];
let rawCalldata: RawCalldata = [];

// Parse the Call array to create the objects which will be accepted by the contract
callArray.forEach((call) => {
executeCallArray.push({
to: BigInt(call.contractAddress),
selector: hash.starknetKeccak(call.entrypoint),
data_offset: rawCalldata.length,
data_len: call.calldata.length
});
rawCalldata = rawCalldata.concat(call.calldata);
});

const adaptedNonce = nonce.toString();
const adaptedMaxFee = "0x" + maxFee.toString(16);
const adaptedVersion = "0x" + version.toString(16);
const messageHash = hash.hashMulticall(
accountAddress,
callArray,
adaptedNonce,
adaptedMaxFee,
adaptedVersion
);

const args = {
call_array: executeCallArray,
calldata: rawCalldata,
nonce: adaptedNonce
};

return { messageHash, args };
}

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

const baseArtifactsPath = path.join(
hre.config.paths.starknetArtifacts,
ACCOUNT_CONTRACT_ARTIFACTS_ROOT_PATH
);

// Remove old versions from the path
if (fs.existsSync(baseArtifactsPath)) {
const contents = fs.readdirSync(baseArtifactsPath);
contents
.filter((content) => content !== ACCOUNT_ARTIFACTS_VERSION)
.forEach((content) => {
fs.rmSync(path.join(baseArtifactsPath, content), {
recursive: true,
force: true
});
});
}
const baseArtifactsPath = path.join(hre.config.paths.starknetArtifacts, ACCOUNT_ARTIFACTS_DIR);

// Full path to where the artifacts will be saved
const artifactsTargetPath = path.join(
baseArtifactsPath,
ACCOUNT_ARTIFACTS_VERSION,
accountType,
artifactsVersion,
artifactsBase
);

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

const artifactLocationUrl = GITHUB_ACCOUNT_ARTIFACTS_URL.concat(
const artifactsSourcePath = path.join(
__dirname,
"..", // necessary since artifact dir is in the root, not in src
ACCOUNT_ARTIFACTS_DIR,
accountType,
"/",
artifactsBase,
"/"
artifactsVersion,
artifactsBase
);

await ensureArtifact(jsonArtifact, artifactsTargetPath, artifactLocationUrl);
await ensureArtifact(abiArtifact, artifactsTargetPath, artifactLocationUrl);
await ensureArtifact(jsonArtifact, artifactsTargetPath, artifactsSourcePath);
await ensureArtifact(abiArtifact, artifactsTargetPath, artifactsSourcePath);

return artifactsTargetPath;
}

/**
* Checks if the provided artifact exists in the project's artifacts folder.
* If it doesen't, downloads it from the GitHub repository "https://github.com/Shard-Labs/starknet-hardhat-example"
* @param artifact artifact file to download. E.g. "Account.json" or "Account_abi.json"
* If it doesn't exist, it is downloaded from the GitHub repository.
* @param fileName artifact file to download. E.g. "Account.json" or "Account_abi.json"
* @param artifactsTargetPath folder to where the artifacts will be downloaded. E.g. "project/starknet-artifacts/Account.cairo"
* @param artifactLocationUrl url to the github folder where the artifacts are stored
* @param artifactSourcePath path to the folder where the artifacts are stored
*/
async function ensureArtifact(
artifact: string,
fileName: string,
artifactsTargetPath: string,
artifactLocationUrl: string
artifactSourcePath: string
) {
// Download artifact if it doesen't exist
if (!fs.existsSync(path.join(artifactsTargetPath, artifact))) {
const finalTargetPath = path.join(artifactsTargetPath, fileName);
if (!fs.existsSync(finalTargetPath)) {
fs.mkdirSync(artifactsTargetPath, { recursive: true });

const rawFileURL = artifactLocationUrl.concat(artifact);

const response = await axios.get(rawFileURL, {
transformResponse: (res) => {
return res;
},
responseType: "json"
});

fs.writeFileSync(path.join(artifactsTargetPath, artifact), response.data);
const finalSourcePath = path.join(artifactSourcePath, fileName);
fs.copyFileSync(finalSourcePath, finalTargetPath);
}
}

Expand Down
Loading

0 comments on commit a8eb495

Please sign in to comment.