Skip to content

Commit

Permalink
feat: support eip712 for starknet
Browse files Browse the repository at this point in the history
implemented as described in argentlabs/argent-x#14
  • Loading branch information
janek26 committed Dec 8, 2021
1 parent 3837e72 commit d597082
Show file tree
Hide file tree
Showing 8 changed files with 339 additions and 2 deletions.
24 changes: 24 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@types/jest": "^27.0.2",
"@types/json-bigint": "^1.0.1",
"@types/minimalistic-assert": "^1.0.1",
"@types/object-hash": "^2.2.1",
"@types/pako": "^1.0.2",
"@types/url-join": "^4.0.1",
"@typescript-eslint/eslint-plugin": "^5.0.0",
Expand Down Expand Up @@ -67,6 +68,7 @@
"json-bigint": "^1.0.0",
"minimalistic-assert": "^1.0.1",
"pako": "^2.0.4",
"superstruct": "^0.15.3",
"url-join": "^4.0.1"
},
"lint-staged": {
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ export * as stark from './utils/stark';
export * as ec from './utils/ellipticCurve';
export * as uint256 from './utils/uint256';
export * as shortString from './utils/shortString';
export * as eip712 from './utils/eip712';
25 changes: 24 additions & 1 deletion src/signer/default.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import assert from 'minimalistic-assert';

import { Provider } from '../provider';
import { AddTransactionResponse, KeyPair, Transaction } from '../types';
import { AddTransactionResponse, KeyPair, Signature, Transaction } from '../types';
import { TypedData, getMessageHash } from '../utils/eip712';
import { sign } from '../utils/ellipticCurve';
import { addHexPrefix } from '../utils/encode';
import { hashMessage } from '../utils/hash';
Expand Down Expand Up @@ -69,4 +70,26 @@ export class Signer extends Provider implements SignerInterface {
signature: [r, s],
});
}

/**
* Sign an JSON object with the starknet private key and return the signature
*
* @param json - JSON object to be signed
* @returns the signature of the JSON object
* @throws {Error} if the JSON object is not a valid JSON
*/
public async sign(typedData: TypedData): Promise<Signature> {
return sign(this.keyPair, await this.hash(typedData));
}

/**
* Hash a JSON object with pederson hash and return the hash
*
* @param json - JSON object to be hashed
* @returns the hash of the JSON object
* @throws {Error} if the JSON object is not a valid JSON
*/
public async hash(typedData: TypedData): Promise<string> {
return getMessageHash(typedData, this.address);
}
}
21 changes: 20 additions & 1 deletion src/signer/interface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Provider } from '../provider';
import { AddTransactionResponse, Transaction } from '../types';
import { AddTransactionResponse, Signature, Transaction } from '../types';
import { TypedData } from '../utils/eip712/types';

export abstract class SignerInterface extends Provider {
public abstract address: string;
Expand All @@ -14,4 +15,22 @@ export abstract class SignerInterface extends Provider {
public abstract override addTransaction(
transaction: Transaction
): Promise<AddTransactionResponse>;

/**
* Sign an JSON object with the starknet private key and return the signature
*
* @param json - JSON object to be signed
* @returns the signature of the JSON object
* @throws {Error} if the JSON object is not a valid JSON
*/
public abstract sign(typedData: TypedData): Promise<Signature>;

/**
* Hash a JSON object with pederson hash and return the hash
*
* @param json - JSON object to be hashed
* @returns the hash of the JSON object
* @throws {Error} if the JSON object is not a valid JSON
*/
public abstract hash(typedData: TypedData): Promise<string>;
}
169 changes: 169 additions & 0 deletions src/utils/eip712/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { computeHashOnElements } from '../hash';
import { BigNumberish } from '../number';
import { encodeShortString } from '../shortString';
import { getSelectorFromName } from '../stark';
import { TypedData } from './types';
import { validateTypedData } from './utils';

export * from './types';

/**
* Get the dependencies of a struct type. If a struct has the same dependency multiple times, it's only included once
* in the resulting array.
*
* @param {TypedData} typedData
* @param {string} type
* @param {string[]} [dependencies]
* @return {string[]}
*/
export const getDependencies = (
typedData: TypedData,
type: string,
dependencies: string[] = []
): string[] => {
// `getDependencies` is called by most other functions, so we validate the JSON schema here
if (!validateTypedData(typedData)) {
throw new Error('Typed data does not match JSON schema');
}

if (dependencies.includes(type)) {
return dependencies;
}

if (!typedData.types[type]) {
return dependencies;
}

return [
type,
...typedData.types[type].reduce<string[]>(
(previous, t) => [
...previous,
...getDependencies(typedData, t.type, previous).filter(
(dependency) => !previous.includes(dependency)
),
],
[]
),
];
};

/**
* Encode a type to a string. All dependant types are alphabetically sorted.
*
* @param {TypedData} typedData
* @param {string} type
* @return {string}
*/
export const encodeType = (typedData: TypedData, type: string): string => {
const [primary, ...dependencies] = getDependencies(typedData, type);
const types = [primary, ...dependencies.sort()];

return types
.map((dependency) => {
return `${dependency}(${typedData.types[dependency].map((t) => `${t.type} ${t.name}`)})`;
})
.join('');
};

/**
* Get a type string as hash.
*
* @param {TypedData} typedData
* @param {string} type
* @return {string}
*/
export const getTypeHash = (typedData: TypedData, type: string): string => {
return getSelectorFromName(encodeType(typedData, type));
};

/**
* Encodes a single value to an ABI serialisable string, number or Buffer. Returns the data as tuple, which consists of
* an array of ABI compatible types, and an array of corresponding values.
*
* @param {TypedData} typedData
* @param {string} type
* @param {any} data
* @returns {[string, string]}
*/
const encodeValue = (typedData: TypedData, type: string, data: unknown): [string, string] => {
if (typedData.types[type]) {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
return ['felt', getStructHash(typedData, type, data as Record<string, unknown>)];
}

if (type === 'shortString') {
return ['felt', encodeShortString(data as string)];
}

if (type === 'felt*') {
return ['felt', computeHashOnElements(data as string[])];
}

return [type, data as string];
};

/**
* Encode the data to an ABI encoded Buffer. The data should be a key -> value object with all the required values. All
* dependant types are automatically encoded.
*
* @param {TypedData} typedData
* @param {string} type
* @param {Record<string, any>} data
*/
export const encodeData = <T extends TypedData>(typedData: T, type: string, data: T['message']) => {
const [types, values] = typedData.types[type].reduce<[string[], string[]]>(
([ts, vs], field) => {
if (data[field.name] === undefined || data[field.name] === null) {
throw new Error(`Cannot encode data: missing data for '${field.name}'`);
}

const value = data[field.name];
const [t, encodedValue] = encodeValue(typedData, field.type, value);

return [
[...ts, t],
[...vs, encodedValue],
];
},
[['felt'], [getTypeHash(typedData, type)]]
);

return [types, values];
};

/**
* Get encoded data as a hash. The data should be a key -> value object with all the required values. All dependant
* types are automatically encoded.
*
* @param {TypedData} typedData
* @param {string} type
* @param {Record<string, any>} data
* @return {Buffer}
*/
export const getStructHash = <T extends TypedData>(
typedData: T,
type: string,
data: T['message']
) => {
return computeHashOnElements(encodeData(typedData, type, data)[1]);
};

/**
* Get the EIP-191 encoded message to sign, from the typedData object. If `hash` is enabled, the message will be hashed
* with Keccak256.
*
* @param {TypedData} typedData
* @param {boolean} hash
* @return {string}
*/
export const getMessageHash = (typedData: TypedData, account: BigNumberish): string => {
const message = [
encodeShortString('StarkNet Message'),
getStructHash(typedData, 'EIP712Domain', typedData.domain),
account,
getStructHash(typedData, typedData.primaryType, typedData.message),
];

return computeHashOnElements(message);
};
Loading

0 comments on commit d597082

Please sign in to comment.