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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"fixtures:generate": "node scripts/generate-tx-fixtures.cjs"
},
"dependencies": {
"@casper-ecosystem/casper-eip-712": "1.2.1",
"@noble/hashes": "^1.8.0",
"apisauce": "^3.2.2",
"casper-js-sdk": "5.0.12",
Expand Down
65 changes: 65 additions & 0 deletions src/data/repositories/eip712/eip712.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { PrivateKey, KeyAlgorithm } from 'casper-js-sdk';
import { PermitTypes, buildDomain } from '@casper-ecosystem/casper-eip-712';
import { EIP712Repository } from './index';
import { EIP712Error, isEIP712Error } from '../../../domain';

const DOMAIN = buildDomain('CasperSwap', '1', 'casper', '0x' + '01'.repeat(32));
const MESSAGE = {
owner: '0x00' + '02'.repeat(32),
spender: '0x01' + '03'.repeat(32),
value: 1000n,
nonce: 0n,
deadline: 1999999999n,
};
const TYPED_DATA = { domain: DOMAIN, types: PermitTypes, primaryType: 'Permit', message: MESSAGE };
const EXPECTED_DIGEST = '0x545a8088d6365ada6ef282f4d5979c655cd428b4115cbf65f561bcd65767a98c';

describe('EIP712Repository', () => {
const repo = new EIP712Repository();

it('computeDigest delegates to the digest util', () => {
expect(repo.computeDigest(TYPED_DATA).digest).toBe(EXPECTED_DIGEST);
});

it('buildDisplayModel returns rows', () => {
const model = repo.buildDisplayModel(TYPED_DATA);
expect(model.primaryType).toBe('Permit');
expect(model.messageRows).toHaveLength(5);
});

it('signDigest produces a 02-prefixed secp256k1 signature', () => {
const key = PrivateKey.fromHex('11'.repeat(32), KeyAlgorithm.SECP256K1);
const result = repo.signDigest({ privateKey: key, digest: EXPECTED_DIGEST });
expect(result.signature.startsWith('02')).toBe(true);
expect(result.digest).toBe(EXPECTED_DIGEST);
});

it('signTypedData computes then signs', () => {
const key = PrivateKey.fromHex('11'.repeat(32), KeyAlgorithm.SECP256K1);
const result = repo.signTypedData({ typedData: TYPED_DATA, privateKey: key });
expect(result.digest).toBe(EXPECTED_DIGEST);
});

it('preserves an already-typed EIP712Error (pass-through)', () => {
const broken = { ...TYPED_DATA, primaryType: 'Missing' };
let caught: unknown;
try {
repo.computeDigest(broken);
} catch (e) {
caught = e;
}
expect(isEIP712Error(caught)).toBe(true);
});

it('wraps a non-EIP712 lib failure as EIP712Error with type computeDigest', () => {
const malformed = { ...TYPED_DATA, message: { ...MESSAGE, owner: '0x1234' } };
let caught: unknown;
try {
repo.computeDigest(malformed);
} catch (e) {
caught = e;
}
expect(isEIP712Error(caught)).toBe(true);
expect((caught as EIP712Error).type).toBe('computeDigest');
});
});
68 changes: 68 additions & 0 deletions src/data/repositories/eip712/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {
EIP712Error,
IEIP712Digest,
IEIP712DisplayModel,
IEIP712RecoverSignerParams,
IEIP712Repository,
IEIP712SignDigestParams,
IEIP712SignResult,
IEIP712SignTypedDataOptions,
IEIP712SignTypedDataParams,
IEIP712TypedData,
IEIP712VerifySignatureParams,
isEIP712Error,
} from '../../../domain';
import {
buildTypedDataDisplayModel,
computeTypedDataDigest,
recoverTypedDataSignerAddress,
signTypedData as signTypedDataUtil,
signTypedDataDigestWithKey,
verifyTypedDataSignature,
} from '../../../utils';

/**
* Synchronous repository: EIP-712 work is pure CPU (validation, hashing, signing) with no network
* or I/O, so — unlike the HTTP-backed repositories in this package — these methods are not async.
*
* `computeDigest`, `signDigest` and `signTypedData` wrap unexpected failures in {@link EIP712Error}.
* `recoverSigner` and `verifySignature` are thin secp256k1 wrappers that surface raw library errors
* as-is (they are not wrapped in EIP712Error).
*/
export class EIP712Repository implements IEIP712Repository {
computeDigest(typedData: IEIP712TypedData, options?: IEIP712SignTypedDataOptions): IEIP712Digest {
try {
return computeTypedDataDigest(typedData, options);
} catch (e) {
throw isEIP712Error(e) ? e : new EIP712Error(e, 'computeDigest');
}
}

buildDisplayModel(typedData: IEIP712TypedData): IEIP712DisplayModel {
return buildTypedDataDisplayModel(typedData);
}

signDigest({ privateKey, digest }: IEIP712SignDigestParams): IEIP712SignResult {
try {
return signTypedDataDigestWithKey(privateKey, digest);
} catch (e) {
throw isEIP712Error(e) ? e : new EIP712Error(e, 'signDigest');
}
}

signTypedData({ typedData, privateKey, options }: IEIP712SignTypedDataParams): IEIP712SignResult {
try {
return signTypedDataUtil(typedData, privateKey, options);
} catch (e) {
throw isEIP712Error(e) ? e : new EIP712Error(e, 'signTypedData');
}
}

recoverSigner(params: IEIP712RecoverSignerParams): string {
return recoverTypedDataSignerAddress(params);
}

verifySignature(params: IEIP712VerifySignatureParams): boolean {
return verifyTypedDataSignature(params);
}
}
1 change: 1 addition & 0 deletions src/data/repositories/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from './accountInfo';
export * from './appEvents';
export * from './txSignatureRequest';
export * from './contractPackage';
export * from './eip712';
60 changes: 60 additions & 0 deletions src/domain/eip712/entities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Maybe } from '../../typings';

export interface IEIP712Field {
name: string;
type: string;
}

export type IEIP712Types = Record<string, IEIP712Field[]>;

export interface IEIP712TypedData {
domain: Record<string, unknown>;
types: IEIP712Types;
primaryType: string;
message: Record<string, unknown>;
}

export interface IEIP712SignTypedDataOptions {
domainTypes?: IEIP712Field[];
returnHashArtifacts?: boolean;
rejectUnknownFields?: boolean;
}

export interface IEIP712HashArtifacts {
domainTypeString: string;
domain: Record<string, unknown>;
domainSeparator: string;
structHash: string;
canonicalTypeString: string;
typeHash: string;
}

export interface IEIP712Digest {
digest: string;
resolvedDomainTypes: IEIP712Field[];
hashArtifacts?: IEIP712HashArtifacts;
}

export interface IEIP712DisplayRow {
label: string;
value: string;
displayValue: string;
isAddress: boolean;
copyValue: Maybe<string>;
type: string;
}

export interface IEIP712DisplayModel {
domainRows: IEIP712DisplayRow[];
messageRows: IEIP712DisplayRow[];
primaryType: string;
}

export type EIP712SignatureScheme = 'ed25519' | 'secp256k1';

export interface IEIP712SignResult {
signature: string;
digest: string;
publicKey: string;
hashArtifacts?: IEIP712HashArtifacts;
}
23 changes: 23 additions & 0 deletions src/domain/eip712/errors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { EIP712Error, isEIP712Error, SignTypedDataErrorCodes } from './errors';

describe('EIP712Error', () => {
it('carries type, errorCode and a public name', () => {
const err = new EIP712Error(
new Error('boom'),
'validateTypedData',
SignTypedDataErrorCodes.INVALID_PARAMS,
);

expect(err.message).toBe('boom');
expect(err.type).toBe('validateTypedData');
expect(err.errorCode).toBe('INVALID_PARAMS');
expect(err.name).toBe('EIP712Error');
expect(err.traceable).toBe(true);
});

it('isEIP712Error narrows correctly', () => {
const err = new EIP712Error(new Error('x'), 'computeDigest');
expect(isEIP712Error(err)).toBe(true);
expect(isEIP712Error(new Error('plain'))).toBe(false);
});
});
45 changes: 45 additions & 0 deletions src/domain/eip712/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { IDomainError, isDomainError, isError } from '../common';

export const SignTypedDataErrorCodes = {
INVALID_PARAMS: 'INVALID_PARAMS',
DOMAIN_TYPES_REQUIRED: 'DOMAIN_TYPES_REQUIRED',
UNSUPPORTED_TYPE: 'UNSUPPORTED_TYPE',
} as const;
export type SignTypedDataErrorCode =
(typeof SignTypedDataErrorCodes)[keyof typeof SignTypedDataErrorCodes];

export type EIP712ErrorType =
| 'validateTypedData'
| 'resolveDomainTypes'
| 'computeDigest'
| 'signDigest'
| 'signTypedData';

export type IEIP712Error = IDomainError<EIP712ErrorType> & {
errorCode?: SignTypedDataErrorCode;
};

export function isEIP712Error(error: unknown | IEIP712Error): error is IEIP712Error {
return error instanceof EIP712Error && (<EIP712Error>error).name === 'EIP712Error';
}

export class EIP712Error extends Error implements IEIP712Error {
constructor(error: Error | unknown, type: EIP712ErrorType, errorCode?: SignTypedDataErrorCode) {
if (isError(error)) {
super(error.message);
this.stack = error.stack;
this.traceable = isDomainError(error) ? Boolean(error.traceable) : true;
} else {
super(JSON.stringify(error));
this.traceable = true;
}

this.name = 'EIP712Error';
this.type = type;
this.errorCode = errorCode;
}

type: EIP712ErrorType;
traceable: boolean;
errorCode?: SignTypedDataErrorCode;
}
3 changes: 3 additions & 0 deletions src/domain/eip712/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './entities';
export * from './errors';
export * from './repository';
47 changes: 47 additions & 0 deletions src/domain/eip712/repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { PrivateKey } from 'casper-js-sdk';
import {
IEIP712Digest,
IEIP712DisplayModel,
IEIP712SignResult,
IEIP712SignTypedDataOptions,
IEIP712TypedData,
} from './entities';

export interface IEIP712SignDigestParams {
privateKey: PrivateKey;
digest: string;
}

export interface IEIP712SignTypedDataParams {
typedData: IEIP712TypedData;
privateKey: PrivateKey;
options?: IEIP712SignTypedDataOptions;
}

export interface IEIP712RecoverSignerParams {
typedData: IEIP712TypedData;
/** Raw 65-byte recoverable secp256k1 signature (r‖s‖v). Not the wallet's `02`-prefixed format. */
signature: Uint8Array;
options?: IEIP712SignTypedDataOptions;
}

export interface IEIP712VerifySignatureParams {
digest: string;
/** Raw 65-byte recoverable secp256k1 signature (r‖s‖v). */
signature: Uint8Array;
/** 0x-prefixed 20-byte Ethereum-style address (as returned by recoverSigner). */
expectedAddress: string;
}

/**
* All methods are synchronous: EIP-712 operations are pure CPU work (validation, hashing, signing)
* with no network or I/O, unlike the async HTTP-backed repositories in this package.
*/
export interface IEIP712Repository {
computeDigest(typedData: IEIP712TypedData, options?: IEIP712SignTypedDataOptions): IEIP712Digest;
buildDisplayModel(typedData: IEIP712TypedData): IEIP712DisplayModel;
signDigest(params: IEIP712SignDigestParams): IEIP712SignResult;
signTypedData(params: IEIP712SignTypedDataParams): IEIP712SignResult;
recoverSigner(params: IEIP712RecoverSignerParams): string;
verifySignature(params: IEIP712VerifySignatureParams): boolean;
}
1 change: 1 addition & 0 deletions src/domain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from './accountInfo';
export * from './appEvents';
export * from './tx-signature-request';
export * from './contractPackage';
export * from './eip712';
3 changes: 2 additions & 1 deletion src/setup.integration.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { setupRepositories } from './setup';

describe('setupRepositories (integration)', () => {
it('wires all 9 repositories', () => {
it('wires all 10 repositories', () => {
const repos = setupRepositories();

expect(repos.accountInfoRepository).toBeDefined();
Expand All @@ -13,6 +13,7 @@ describe('setupRepositories (integration)', () => {
expect(repos.appEventsRepository).toBeDefined();
expect(repos.txSignatureRequestRepository).toBeDefined();
expect(repos.contractPackageRepository).toBeDefined();
expect(repos.eip712Repository).toBeDefined();
});

it('honors debug flag (logger is wired)', () => {
Expand Down
3 changes: 3 additions & 0 deletions src/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
AppEventsRepository,
TxSignatureRequestRepository,
ContractPackageRepository,
EIP712Repository,
} from './data/repositories';
import { Logger } from './utils';
import {
Expand Down Expand Up @@ -75,6 +76,7 @@ export const setupRepositories = ({
grpcUrl,
httpAuthorizationHeader,
);
const eip712Repository = new EIP712Repository();

return {
accountInfoRepository,
Expand All @@ -86,5 +88,6 @@ export const setupRepositories = ({
appEventsRepository,
txSignatureRequestRepository,
contractPackageRepository,
eip712Repository,
};
};
Loading
Loading