Skip to content

Commit

Permalink
feat(account): initial multicall support
Browse files Browse the repository at this point in the history
  • Loading branch information
delaaxe committed Mar 1, 2022
1 parent d55006a commit e2e3d61
Show file tree
Hide file tree
Showing 10 changed files with 68,677 additions and 52,063 deletions.
120,492 changes: 68,548 additions & 51,944 deletions __mocks__/ArgentAccount.json

Large diffs are not rendered by default.

35 changes: 21 additions & 14 deletions __tests__/utils/ellipticalCurve.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ec, getKeyPair, getStarkKey, sign, verify } from '../../src/utils/ellipticCurve';
import { removeHexPrefix } from '../../src/utils/encode';
import { hashCalldata, hashMessage, pedersen } from '../../src/utils/hash';
import { computeHashOnElements, hashMulticall, pedersen } from '../../src/utils/hash';
import { toBN, toHex } from '../../src/utils/number';

test('getKeyPair()', () => {
Expand All @@ -17,35 +17,42 @@ test('pedersen()', () => {
expect(own).toMatchSnapshot();
});

test('hashCalldata()', () => {
test('computeHashOnElements()', () => {
const array = ['1', '2', '3', '4'];
expect(hashCalldata(array)).toBe(
expect(computeHashOnElements(array)).toBe(
'0x66bd4335902683054d08a0572747ea78ebd9e531536fb43125424ca9f902084'
);
expect(array).toStrictEqual(['1', '2', '3', '4']);

expect(hashCalldata(['1', '2'])).toBe(
expect(computeHashOnElements(['1', '2'])).toBe(
'0x501a3a8e6cd4f5241c639c74052aaa34557aafa84dd4ba983d6443c590ab7df'
);
});

test('hashMessage()', () => {
const privateKey = '0x019800ea6a9a73f94aee6a3d2edf018fc770443e90c7ba121e8303ec6b349279';
const hashMsg = hashMessage(
'0x33f45f07e1bd1a51b45fc24ec8c8c9908db9e42191be9e169bfcac0c0d99745',
'5',
'6',
['1', '2'],
'2'
const account = '2007067565103695475819120104515800035851923905855118399071773059478896040938';
const transactions = [
{
contractAddress:
'3290661298119599979891444342541795905081168856323302956721669397616389152866',
entrypoint: 'set_number',
calldata: ['47'],
},
];
const nonce = '3';
const maxFee = '0';
const hash = hashMulticall(account, transactions, nonce, maxFee);
expect(hash).toBe(
toHex(toBN('1608351043472325350463069815257733118091727529101532499046754244230898025592'))
);
expect(hashMsg).toBe('0x7f15c38ea577a26f4f553282fcfe4f1feeb8ecfaad8f221ae41abf8224cbddd');
const keyPair = getKeyPair(privateKey);
const [r, s] = sign(keyPair, removeHexPrefix(hashMsg));
const [r, s] = sign(keyPair, removeHexPrefix(hash));
expect(r.toString()).toStrictEqual(
toBN('2458502865976494910213617956670505342647705497324144349552978333078363662855').toString()
toBN('1079537730825246752292590270213864261175133133352510235773017189606850691611').toString()
);
expect(s.toString()).toStrictEqual(
toBN('3439514492576562277095748549117516048613512930236865921315982886313695689433').toString()
toBN('2904560423220491364719171767721067837294296476624248675613584621502231297000').toString()
);
});

Expand Down
77 changes: 38 additions & 39 deletions src/account/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import { Signer, SignerInterface } from '../signer';
import {
Abi,
AddTransactionResponse,
ExecuteInvocation,
Call,
InvocationsDetails,
KeyPair,
Signature,
} from '../types';
import { getSelectorFromName } from '../utils/hash';
import { BigNumberish, bigNumberishArrayToDecimalStringArray, toBN, toHex } from '../utils/number';
import { compileCalldata, getSelectorFromName } from '../utils/stark';
import { compileCalldata } from '../utils/stark';
import { TypedData, getMessageHash } from '../utils/typedData';
import { AccountInterface } from './interface';

Expand Down Expand Up @@ -41,52 +42,50 @@ export class Account extends Provider implements AccountInterface {
* @returns a confirmation of invoking a function on the starknet contract
*/
public async execute(
transactions: ExecuteInvocation | ExecuteInvocation[],
calls: Call | Call[],
abis: Abi[] = [],
transactionsDetail: InvocationsDetails = {}
): Promise<AddTransactionResponse> {
if (Array.isArray(transactions) && transactions.length !== 1) {
throw new Error('Only one transaction at a time is currently supported');
}
const transactions = Array.isArray(calls) ? calls : [calls];

const {
contractAddress,
calldata = [],
entrypoint,
...invocation
} = Array.isArray(transactions) ? transactions[0] : transactions;
const { nonce } = transactionsDetail;
const signerDetails = {
walletAddress: this.address,
nonce: toBN(transactionsDetail.nonce ?? (await this.getNonce())),
maxFee: toBN(transactionsDetail.maxFee ?? '0'),
};

const nonceBn = toBN(nonce ?? (await this.getNonce()));
const calldataDecimal = bigNumberishArrayToDecimalStringArray(calldata);
const signature = await this.signer.signTransaction(transactions, signerDetails, abis);

const signature = await this.signer.signTransaction(
[
{
...invocation,
contractAddress,
calldata: calldataDecimal,
entrypoint,
},
],
{ walletAddress: this.address, nonce: nonceBn },
abis
);
const calldata = this.fromCallsToCallArray(transactions);

const entrypointSelector = getSelectorFromName(entrypoint);
return this.fetchEndpoint('add_transaction', undefined, {
type: 'INVOKE_FUNCTION',
contract_address: this.address,
entry_point_selector: getSelectorFromName('__execute__'),
calldata: [...calldata, signerDetails.nonce.toString()],
signature: bigNumberishArrayToDecimalStringArray(signature),
});
}

return super.invokeFunction({
contractAddress: this.address,
entrypoint: 'execute',
calldata: [
contractAddress,
entrypointSelector,
calldataDecimal.length.toString(),
...calldataDecimal,
nonceBn.toString(),
],
signature,
private fromCallsToCallArray(calls: Call[]): string[] {
const callArray: string[] = [];
const calldata: BigNumberish[] = [];
calls.forEach((call) => {
const data = call.calldata || [];
callArray.push(
call.contractAddress,
getSelectorFromName(call.entrypoint),
calldata.length.toString(),
data.length.toString()
);
calldata.push(...data);
});
return [
callArray.length.toString(),
...bigNumberishArrayToDecimalStringArray(callArray),
calldata.length.toString(),
...bigNumberishArrayToDecimalStringArray(calldata),
];
}

/**
Expand Down
4 changes: 2 additions & 2 deletions src/account/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { ProviderInterface } from '../provider';
import {
Abi,
AddTransactionResponse,
Call,
DeployContractPayload,
ExecuteInvocation,
InvocationsDetails,
Signature,
} from '../types';
Expand Down Expand Up @@ -41,7 +41,7 @@ export abstract class AccountInterface extends ProviderInterface {
* @returns response from addTransaction
*/
public abstract execute(
transactions: ExecuteInvocation | ExecuteInvocation[],
transactions: Call | Call[],
abis?: Abi[],
transactionsDetail?: InvocationsDetails
): Promise<AddTransactionResponse>;
Expand Down
26 changes: 17 additions & 9 deletions src/provider/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ import {
Invocation,
TransactionReceipt,
} from '../types';
import { getSelectorFromName } from '../utils/hash';
import { parse, stringify } from '../utils/json';
import { BigNumberish, bigNumberishArrayToDecimalStringArray, toBN, toHex } from '../utils/number';
import { compressProgram, getSelectorFromName, randomAddress } from '../utils/stark';
import { compressProgram, randomAddress } from '../utils/stark';
import { ProviderInterface } from './interface';
import { BlockIdentifier, getFormattedBlockIdentifier, txIdentifier } from './utils';

Expand Down Expand Up @@ -133,14 +134,21 @@ export class Provider implements ProviderInterface {
const queryString = this.getQueryString(query);
const headers = this.getHeaders(method);

const { data } = await axios.request<Endpoints[T]['RESPONSE']>({
method,
url: urljoin(baseUrl, endpoint, queryString),
data: stringify(request),
headers,
});

return data;
try {
const { data } = await axios.request<Endpoints[T]['RESPONSE']>({
method,
url: urljoin(baseUrl, endpoint, queryString),
data: stringify(request),
headers,
});
return data;
} catch (error: any) {
const data = error?.response?.data;
if (data?.message) {
throw new Error(`${data.code}: ${data.message}`);
}
throw error;
}
}

/**
Expand Down
32 changes: 8 additions & 24 deletions src/signer/default.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { Abi, Invocation, InvocationsSignerDetails, KeyPair, Signature } from '../types';
import { getStarkKey, sign } from '../utils/ellipticCurve';
import { addHexPrefix } from '../utils/encode';
import { hashMessage } from '../utils/hash';
import { bigNumberishArrayToDecimalStringArray, toBN } from '../utils/number';
import { getSelectorFromName } from '../utils/stark';
import { hashMulticall } from '../utils/hash';
import { TypedData, getMessageHash } from '../utils/typedData';
import { SignerInterface } from './interface';

Expand All @@ -23,32 +20,19 @@ export class Signer implements SignerInterface {
transactionsDetail: InvocationsSignerDetails,
abis: Abi[] = []
): Promise<Signature> {
if (transactions.length !== 1) {
throw new Error('Only one transaction at a time is currently supported by this signer');
}
if (abis?.length !== 0 && abis.length !== transactions.length) {
if (abis && abis.length !== transactions.length) {
throw new Error('ABI must be provided for each transaction or no transaction');
}
// now use abi to display decoded data somewhere, but as this signer is headless, we can't do that

const { contractAddress, entrypoint, calldata = [] } = transactions[0];
const { nonce, walletAddress } = transactionsDetail;

const nonceBn = toBN(nonce);
const entrypointSelector = getSelectorFromName(entrypoint);
const calldataDecimal = bigNumberishArrayToDecimalStringArray(calldata);

const msgHash = addHexPrefix(
hashMessage(
walletAddress,
contractAddress,
entrypointSelector,
calldataDecimal,
nonceBn.toString()
)
const hash = hashMulticall(
transactionsDetail.walletAddress,
transactions,
transactionsDetail.nonce.toString(),
transactionsDetail.maxFee.toString()
);

return sign(this.keyPair, msgHash);
return sign(this.keyPair, hash);
}

public async signMessage(typedData: TypedData, walletAddress: string): Promise<Signature> {
Expand Down
5 changes: 2 additions & 3 deletions src/types/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,13 @@ export type Invocation = {
signature?: Signature;
};

export type ExecuteInvocation = Omit<Invocation, 'signature'>;
export type Call = Omit<Invocation, 'signature'>;

export type InvocationsDetails = {
nonce?: BigNumberish;
maxFee?: BigNumberish;
};

export type Call = Omit<Invocation, 'signature' | 'nonce'>;

export type Status =
| 'NOT_RECEIVED'
| 'RECEIVED'
Expand Down
51 changes: 39 additions & 12 deletions src/utils/hash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ import { keccak256 } from 'ethereum-cryptography/keccak';
import assert from 'minimalistic-assert';

import { CONSTANT_POINTS, FIELD_PRIME, MASK_250, ONE, ZERO } from '../constants';
import { Call } from '../types';
import { ec } from './ellipticCurve';
import { addHexPrefix, buf2hex, utf8ToArray } from './encode';
import { BigNumberish, toBN } from './number';
import { BigNumberish, bigNumberishArrayToDecimalStringArray, toBN, toHex } from './number';
import { encodeShortString } from './shortString';

export const transactionPrefix = encodeShortString('StarkNet Transaction');
export const transactionVersion = 0;

function keccakHex(value: string): string {
return addHexPrefix(buf2hex(keccak256(utf8ToArray(value))));
Expand All @@ -22,6 +27,18 @@ export function starknetKeccak(value: string): BN {
return toBN(keccakHex(value)).and(MASK_250);
}

/**
* Function to get the hex selector from a given function name
*
* [Reference](https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/starknet/public/abi.py#L25-L26)
* @param funcName - selectors abi function name
* @returns hex selector of given abi function name
*/
export function getSelectorFromName(funcName: string) {
// sometimes BigInteger pads the hex string with zeros, which isnt allowed in the starknet api
return toHex(starknetKeccak(funcName));
}

const constantPoints = CONSTANT_POINTS.map((coords: string[]) =>
ec.curve.point(coords[0], coords[1])
);
Expand All @@ -47,17 +64,27 @@ export function computeHashOnElements(data: BigNumberish[]) {
return [...data, data.length].reduce((x, y) => pedersen([x, y]), 0).toString();
}

export function hashCalldata(calldata: string[]): string {
return computeHashOnElements(calldata);
}

export function hashMessage(
export function hashMulticall(
account: string,
to: string,
selector: string,
calldata: string[],
nonce: string
transactions: Call[],
nonce: string,
maxFee: string
) {
const calldataHash = hashCalldata(calldata);
return computeHashOnElements([account, to, selector, calldataHash, nonce]);
const hashArray = transactions
.map(({ contractAddress, entrypoint, calldata }) => [
contractAddress,
getSelectorFromName(entrypoint),
computeHashOnElements(calldata || []),
])
.map(bigNumberishArrayToDecimalStringArray)
.map(computeHashOnElements);

return computeHashOnElements([
transactionPrefix,
account,
computeHashOnElements(hashArray),
nonce,
maxFee,
transactionVersion,
]);
}
Loading

0 comments on commit e2e3d61

Please sign in to comment.