Skip to content
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: 10 additions & 8 deletions __mocks__/typedData/example_baseTypes.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@
{ "name": "n2", "type": "string" },
{ "name": "n3", "type": "selector" },
{ "name": "n4", "type": "u128" },
{ "name": "n5", "type": "ContractAddress" },
{ "name": "n6", "type": "ClassHash" },
{ "name": "n7", "type": "timestamp" },
{ "name": "n8", "type": "shortstring" }
{ "name": "n5", "type": "i128" },
{ "name": "n6", "type": "ContractAddress" },
{ "name": "n7", "type": "ClassHash" },
{ "name": "n8", "type": "timestamp" },
{ "name": "n9", "type": "shortstring" }
]
},
"primaryType": "Example",
Expand All @@ -30,10 +31,11 @@
"n1": true,
"n2": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
"n3": "transfer",
"n4": "0x3e8",
"n5": "0x3e8",
"n4": 10,
"n5": -10,
"n6": "0x3e8",
"n7": 1000,
"n8": "transfer"
"n7": "0x3e8",
"n8": 1000,
"n9": "transfer"
}
}
35 changes: 29 additions & 6 deletions __tests__/utils/typedData.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import exampleEnum from '../../__mocks__/typedData/example_enum.json';
import examplePresetTypes from '../../__mocks__/typedData/example_presetTypes.json';
import typedDataStructArrayExample from '../../__mocks__/typedData/mail_StructArray.json';
import typedDataSessionExample from '../../__mocks__/typedData/session_MerkleTree.json';
import { BigNumberish, StarkNetDomain, num } from '../../src';
import { BigNumberish, StarknetDomain, num } from '../../src';
import { PRIME } from '../../src/constants';
import { getSelectorFromName } from '../../src/utils/hash';
import { MerkleTree } from '../../src/utils/merkle';
import {
Expand Down Expand Up @@ -43,7 +44,7 @@ describe('typedData', () => {
);
encoded = encodeType(exampleBaseTypes.types, 'Example', TypedDataRevision.Active);
expect(encoded).toMatchInlineSnapshot(
`"\\"Example\\"(\\"n0\\":\\"felt\\",\\"n1\\":\\"bool\\",\\"n2\\":\\"string\\",\\"n3\\":\\"selector\\",\\"n4\\":\\"u128\\",\\"n5\\":\\"ContractAddress\\",\\"n6\\":\\"ClassHash\\",\\"n7\\":\\"timestamp\\",\\"n8\\":\\"shortstring\\")"`
`"\\"Example\\"(\\"n0\\":\\"felt\\",\\"n1\\":\\"bool\\",\\"n2\\":\\"string\\",\\"n3\\":\\"selector\\",\\"n4\\":\\"u128\\",\\"n5\\":\\"i128\\",\\"n6\\":\\"ContractAddress\\",\\"n7\\":\\"ClassHash\\",\\"n8\\":\\"timestamp\\",\\"n9\\":\\"shortstring\\")"`
);
encoded = encodeType(examplePresetTypes.types, 'Example', TypedDataRevision.Active);
expect(encoded).toMatchInlineSnapshot(
Expand Down Expand Up @@ -83,7 +84,7 @@ describe('typedData', () => {
);
typeHash = getTypeHash(exampleBaseTypes.types, 'Example', TypedDataRevision.Active);
expect(typeHash).toMatchInlineSnapshot(
`"0x2e5b7e12ca4388c49b4ceb305d853b8f7bf5f36525fea5e4255346b80153249"`
`"0x1f94cd0be8b4097a41486170fdf09a4cd23aefbc74bb2344718562994c2c111"`
);
typeHash = getTypeHash(examplePresetTypes.types, 'Example', TypedDataRevision.Active);
expect(typeHash).toMatchInlineSnapshot(
Expand Down Expand Up @@ -169,7 +170,7 @@ describe('typedData', () => {
const hash = getStructHash(
typedDataExample.types,
'StarkNetDomain',
typedDataExample.domain as StarkNetDomain
typedDataExample.domain as StarknetDomain
);
expect(hash).toMatchInlineSnapshot(
`"0x54833b121883a3e3aebff48ec08a962f5742e5f7b973469c1f8f4f55d470b07"`
Expand All @@ -180,7 +181,7 @@ describe('typedData', () => {
const hash = getStructHash(
exampleBaseTypes.types,
'StarknetDomain',
exampleBaseTypes.domain as StarkNetDomain,
exampleBaseTypes.domain as StarknetDomain,
TypedDataRevision.Active
);
expect(hash).toMatchInlineSnapshot(
Expand Down Expand Up @@ -274,7 +275,7 @@ describe('typedData', () => {
let messageHash: string;
messageHash = getMessageHash(exampleBaseTypes, exampleAddress);
expect(messageHash).toMatchInlineSnapshot(
`"0x790d9fa99cf9ad91c515aaff9465fcb1c87784d9cfb27271ed193675cd06f9c"`
`"0xdb7829db8909c0c5496f5952bcfc4fc894341ce01842537fc4f448743480b6"`
);

messageHash = getMessageHash(examplePresetTypes, exampleAddress);
Expand All @@ -292,4 +293,26 @@ describe('typedData', () => {
spyPedersen.mockRestore();
spyPoseidon.mockRestore();
});

describe('should fail validation', () => {
const baseTypes = (type: string, value: any = PRIME) => {
const copy = JSON.parse(JSON.stringify(exampleBaseTypes)) as typeof exampleBaseTypes;
const property = copy.types.Example.find((e) => e.type === type)!.name;
(copy.message as any)[property] = value;
return copy;
};

test.each([
{ type: 'felt' },
{ type: 'bool' },
{ type: 'u128' },
{ type: 'i128' },
{ type: 'ContractAddress' },
{ type: 'ClassHash' },
{ type: 'timestamp' },
{ type: 'shortstring' },
])('out of bounds - $type', ({ type }) => {
expect(() => getMessageHash(baseTypes(type), exampleAddress)).toThrow(RegExp(type));
});
});
});
6 changes: 6 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,17 @@ export { ETransactionVersion as TRANSACTION_VERSION };
export const ZERO = 0n;
export const MASK_250 = 2n ** 250n - 1n; // 2 ** 250 - 1
export const API_VERSION = ZERO;
export const PRIME = 2n ** 251n + 17n * 2n ** 192n + 1n;

// based on: https://github.com/starkware-libs/cairo-lang/blob/v0.12.3/src/starkware/starknet/common/storage.cairo#L3
export const MAX_STORAGE_ITEM_SIZE = 256n;
export const ADDR_BOUND = 2n ** 251n - MAX_STORAGE_ITEM_SIZE;

const range = (min: bigint, max: bigint) => ({ min, max }) as const;
export const RANGE_FELT = range(ZERO, PRIME - 1n);
export const RANGE_I128 = range(-(2n ** 127n), 2n ** 127n - 1n);
export const RANGE_U128 = range(ZERO, 2n ** 128n - 1n);

export enum BaseUrl {
SN_MAIN = 'https://alpha-mainnet.starknet.io',
SN_GOERLI = 'https://alpha4.starknet.io',
Expand Down
6 changes: 3 additions & 3 deletions src/types/api/rpcspec_0_6/methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ type ReadMethods = {
errors: Errors.BLOCK_NOT_FOUND;
};

// Call a StarkNet function without creating a StarkNet transaction
// Call a Starknet function without creating a Starknet transaction
starknet_call: {
params: {
request: FUNCTION_CALL;
Expand All @@ -172,7 +172,7 @@ type ReadMethods = {
errors: Errors.CONTRACT_NOT_FOUND | Errors.CONTRACT_ERROR | Errors.BLOCK_NOT_FOUND;
};

// Estimate the fee for StarkNet transactions
// Estimate the fee for Starknet transactions
starknet_estimateFee: {
params: {
request: BROADCASTED_TXN[];
Expand Down Expand Up @@ -207,7 +207,7 @@ type ReadMethods = {
errors: Errors.NO_BLOCKS;
};

// Return the currently configured StarkNet chain id
// Return the currently configured Starknet chain id
starknet_chainId: {
params: [];
result: CHAIN_ID;
Expand Down
6 changes: 3 additions & 3 deletions src/types/api/rpcspec_0_7/methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ type ReadMethods = {
errors: Errors.BLOCK_NOT_FOUND;
};

// Call a StarkNet function without creating a StarkNet transaction
// Call a Starknet function without creating a Starknet transaction
starknet_call: {
params: {
request: FUNCTION_CALL;
Expand All @@ -182,7 +182,7 @@ type ReadMethods = {
errors: Errors.CONTRACT_NOT_FOUND | Errors.CONTRACT_ERROR | Errors.BLOCK_NOT_FOUND;
};

// Estimate the fee for StarkNet transactions
// Estimate the fee for Starknet transactions
starknet_estimateFee: {
params: {
request: BROADCASTED_TXN[];
Expand Down Expand Up @@ -217,7 +217,7 @@ type ReadMethods = {
errors: Errors.NO_BLOCKS;
};

// Return the currently configured StarkNet chain id
// Return the currently configured Starknet chain id
starknet_chainId: {
params: [];
result: CHAIN_ID;
Expand Down
18 changes: 8 additions & 10 deletions src/types/typedData.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
// TODO: adjust starknet casing in v6

export enum TypedDataRevision {
Active = '1',
Legacy = '0',
}

export type StarkNetEnumType = {
export type StarknetEnumType = {
name: string;
type: 'enum';
contains: string;
};

export type StarkNetMerkleType = {
export type StarknetMerkleType = {
name: string;
type: 'merkletree';
contains: string;
Expand All @@ -23,18 +21,18 @@ export type StarkNetMerkleType = {
* Note that the `uint` and `int` aliases like in Solidity, and fixed point numbers are not supported by the EIP-712
* standard.
*/
export type StarkNetType =
export type StarknetType =
| {
name: string;
type: string;
}
| StarkNetEnumType
| StarkNetMerkleType;
| StarknetEnumType
| StarknetMerkleType;

/**
* The EIP712 domain struct. Any of these fields are optional, but it must contain at least one field.
*/
export interface StarkNetDomain extends Record<string, unknown> {
export interface StarknetDomain extends Record<string, unknown> {
name?: string;
version?: string;
chainId?: string | number;
Expand All @@ -45,8 +43,8 @@ export interface StarkNetDomain extends Record<string, unknown> {
* The complete typed data, with all the structs, domain data, primary type of the message, and the message itself.
*/
export interface TypedData {
types: Record<string, StarkNetType[]>;
types: Record<string, StarknetType[]>;
primaryType: string;
domain: StarkNetDomain;
domain: StarknetDomain;
message: Record<string, unknown>;
}
62 changes: 48 additions & 14 deletions src/utils/typedData.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
/* eslint-disable no-param-reassign */
import { PRIME, RANGE_FELT, RANGE_I128, RANGE_U128 } from '../constants';
import {
BigNumberish,
TypedDataRevision as Revision,
StarkNetEnumType,
StarkNetMerkleType,
StarkNetType,
StarknetEnumType,
StarknetMerkleType,
StarknetType,
TypedData,
} from '../types';
import assert from './assert';
import { byteArrayFromString } from './calldata/byteArray';
import {
computePedersenHash,
Expand Down Expand Up @@ -62,6 +64,11 @@ const revisionConfiguration: Record<Revision, Configuration> = {
},
};

function assertRange(data: unknown, type: string, { min, max }: { min: bigint; max: bigint }) {
const value = BigInt(data as string);
assert(value >= min && value <= max, `${value} (${type}) is out of bounds [${min}, ${max}]`);
}

function identifyRevision({ types, domain }: TypedData) {
if (revisionConfiguration[Revision.Active].domain in types && domain.revision === Revision.Active)
return Revision.Active;
Expand Down Expand Up @@ -100,7 +107,7 @@ export function prepareSelector(selector: string): string {
return isHex(selector) ? selector : getSelectorFromName(selector);
}

export function isMerkleTreeType(type: StarkNetType): type is StarkNetMerkleType {
export function isMerkleTreeType(type: StarknetType): type is StarknetMerkleType {
return type.type === 'merkletree';
}

Expand Down Expand Up @@ -135,7 +142,7 @@ export function getDependencies(

return [
type,
...(types[type] as StarkNetEnumType[]).reduce<string[]>(
...(types[type] as StarknetEnumType[]).reduce<string[]>(
(previous, t) => [
...previous,
...getDependencies(types, t.type, previous, t.contains, revision).filter(
Expand Down Expand Up @@ -191,7 +198,7 @@ export function encodeType(
const dependencyElements = allTypes[dependency].map((t) => {
const targetType =
t.type === 'enum' && revision === Revision.Active
? (t as StarkNetEnumType).contains
? (t as StarknetEnumType).contains
: t.type;
// parentheses handling for enum variant types
const typeString = targetType.match(/^\(.*\)$/)
Expand Down Expand Up @@ -258,9 +265,9 @@ export function encodeValue(
if (revision === Revision.Active) {
const [variantKey, variantData] = Object.entries(data as TypedData['message'])[0];

const parentType = types[ctx.parent as string][0] as StarkNetEnumType;
const parentType = types[ctx.parent as string][0] as StarknetEnumType;
const enumType = types[parentType.contains];
const variantType = enumType.find((t) => t.name === variantKey) as StarkNetType;
const variantType = enumType.find((t) => t.name === variantKey) as StarknetType;
const variantIndex = enumType.indexOf(variantType);

const encodedSubtypes = variantType.type
Expand Down Expand Up @@ -305,15 +312,42 @@ export function encodeValue(
} // else fall through to default
return [type, getHex(data as string)];
}
case 'i128': {
if (revision === Revision.Active) {
const value = BigInt(data as string);
assertRange(value, type, RANGE_I128);
return [type, getHex(value < 0n ? PRIME + value : value)];
} // else fall through to default
return [type, getHex(data as string)];
}
case 'timestamp':
case 'u128': {
if (revision === Revision.Active) {
assertRange(data, type, RANGE_U128);
} // else fall through to default
return [type, getHex(data as string)];
}
case 'felt':
case 'bool':
case 'u128':
case 'i128':
case 'ContractAddress':
case 'shortstring': {
// TODO: should 'shortstring' diverge into directly using encodeShortString()?
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to draw attention to the approach taken with the shortstring encoding. It was implemented by reusing the existing felt encoding.

As a basic example, a provided value "hello" will be encoded as 0x68656c6c6f, this I believe to not be contentious. The part where I believe there might be issues with is the encoding of numerical strings, the existing felt encoding treats both numbers and numerical strings as the same value and such behaviour might be undesirable/unexpected for the shortstring type. As an example "2" will be encoded as 0x2 while someone coming from a Cairo background will probably assume it to be encoded as 0x32.

if (revision === Revision.Active) {
assertRange(getHex(data as string), type, RANGE_FELT);
} // else fall through to default
return [type, getHex(data as string)];
}
case 'ClassHash':
case 'timestamp':
case 'shortstring':
case 'ContractAddress': {
if (revision === Revision.Active) {
assertRange(data, type, RANGE_FELT);
} // else fall through to default
return [type, getHex(data as string)];
}
case 'bool': {
if (revision === Revision.Active) {
assert(typeof data === 'boolean', `Type mismatch for ${type} ${data}`);
} // else fall through to default
return [type, getHex(data as string)];
}
default: {
if (revision === Revision.Active) {
throw new Error(`Unsupported type: ${type}`);
Expand Down