Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(EIP712): deduce primaryType if not provided #1632

Merged
merged 9 commits into from
Jan 9, 2025
76 changes: 6 additions & 70 deletions packages/aws-kms-adapter/src/KMSVeChainSigner.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { bytesToHex, concatBytes } from '@noble/curves/abstract/utils';
import { type SignatureType } from '@noble/curves/abstract/weierstrass';
import { secp256k1 } from '@noble/curves/secp256k1';
import { Address, Hex, Keccak256, Transaction, Txt } from '@vechain/sdk-core';
import { Address, Hex, Transaction } from '@vechain/sdk-core';
import { JSONRPCInvalidParams, SignerMethodError } from '@vechain/sdk-errors';
import {
type AvailableVeChainProviders,
Expand All @@ -11,13 +11,7 @@ import {
VeChainAbstractSigner
} from '@vechain/sdk-network';
import { BitString, ObjectIdentifier, Sequence, verifySchema } from 'asn1js';
import {
hashTypedData,
recoverPublicKey,
toHex,
type TypedDataDomain,
type TypedDataParameter
} from 'viem';
import { recoverPublicKey, toHex } from 'viem';
import { KMSVeChainProvider } from './KMSVeChainProvider';

class KMSVeChainSigner extends VeChainAbstractSigner {
Expand Down Expand Up @@ -157,7 +151,7 @@ class KMSVeChainSigner extends VeChainAbstractSigner {
}

/**
* It builds a VeChain signature from a bytes payload.
* It builds a VeChain signature from a bytes' payload.
* @param {Uint8Array} payload to sign.
* @param {KMSVeChainProvider} kmsProvider The provider to sign the payload.
* @returns {Uint8Array} The signature following the VeChain format.
Expand Down Expand Up @@ -188,12 +182,10 @@ class KMSVeChainSigner extends VeChainAbstractSigner {
kmsProvider
);

const decodedSignature = concatBytes(
return concatBytes(
decodedSignatureWithoutRecoveryBit.toCompactRawBytes(),
new Uint8Array([recoveryBit])
);

return decodedSignature;
}

/**
Expand Down Expand Up @@ -314,7 +306,7 @@ class KMSVeChainSigner extends VeChainAbstractSigner {

/**
* Submits a signed transaction to the network.
* @param transactionToSend Transaction to by signed and sent to the network.
* @param transactionToSend Transaction to be signed and sent to the network.
* @returns {string} The transaction ID.
*/
public async sendTransaction(
Expand Down Expand Up @@ -345,69 +337,13 @@ class KMSVeChainSigner extends VeChainAbstractSigner {
* @param {Uint8Array} payload in bytes to sign.
* @returns {string} The VeChain signature in hexadecimal format.
*/
private async signPayload(payload: Uint8Array): Promise<string> {
public async signPayload(payload: Uint8Array): Promise<string> {
const veChainSignature =
await this.buildVeChainSignatureFromPayload(payload);
// SCP256K1 encodes the recovery flag in the last byte. EIP-191 adds 27 to it.
veChainSignature[veChainSignature.length - 1] += 27;
return Hex.of(veChainSignature).toString();
}

/**
* Signs a message returning the VeChain signature in hexadecimal format.
* @param {string | Uint8Array} message to sign.
* @returns {string} The VeChain signature in hexadecimal format.
*/
public async signMessage(message: string | Uint8Array): Promise<string> {
try {
const payload =
typeof message === 'string' ? Txt.of(message).bytes : message;
const payloadHashed = Keccak256.of(
concatBytes(
this.MESSAGE_PREFIX,
Txt.of(payload.length).bytes,
payload
)
).bytes;
return await this.signPayload(payloadHashed);
} catch (error) {
throw new SignerMethodError(
'KMSVeChainSigner.signMessage',
'The message could not be signed.',
{ message },
error
);
}
}

/**
* Signs a typed data returning the VeChain signature in hexadecimal format.
* @param {TypedDataDomain} domain to hash as typed data.
* @param {Record<string, TypedDataField[]>} types to hash as typed data.
* @param {Record<string, unknown>} value to hash as typed data.
* @returns {string} The VeChain signature in hexadecimal format.
*/
public async signTypedData(
domain: TypedDataDomain,
types: Record<string, TypedDataParameter[]>,
primaryType: string,
message: Record<string, unknown>
): Promise<string> {
try {
const payload = Hex.of(
hashTypedData({ domain, types, primaryType, message })
).bytes;

return await this.signPayload(payload);
} catch (error) {
throw new SignerMethodError(
'KMSVeChainSigner.signTypedData',
'The typed data could not be signed.',
{ domain, types, primaryType, message },
error
);
}
}
}

export { KMSVeChainSigner };
21 changes: 18 additions & 3 deletions packages/aws-kms-adapter/tests/KMSVeChainSigner.solo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,12 +299,27 @@ describe('KMSVeChainSigner - Thor Solo', () => {
const signature = await signer.signTypedData(
typedData.domain,
typedData.types,
typedData.primaryType,
typedData.data
typedData.data,
typedData.primaryType
);
expect(signature).toBeDefined();
// 64-bytes hex string
expect(signature.length).toBe(132);
expect(signature).toMatch(/^0x[A-Fa-f0-9]{130}$/);

const signatureWithoutPrimaryType = await signer.signTypedData(
typedData.domain,
typedData.types,
typedData.data
);
expect(signatureWithoutPrimaryType).toBeDefined();
// 64-bytes hex string
expect(signatureWithoutPrimaryType).toMatch(/^0x[A-Fa-f0-9]{130}$/);

// Not checking directly the signatures since there is an issue in LocalStack:
// https://github.com/localstack/localstack/issues/11678
// Looks like, regardless the configuration, a new SECP256r1 key is generated
// meaning that the signature will be different every time.
// However both hashes have been checked and they match, + tests in the other implementation.
});
});
});
13 changes: 7 additions & 6 deletions packages/aws-kms-adapter/tests/KMSVeChainSigner.unit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { JSONRPCInvalidParams, SignerMethodError } from '@vechain/sdk-errors';
import {
VeChainProvider,
type ThorClient,
type TransactionRequestInput
type TransactionRequestInput,
type TypedDataDomain,
type TypedDataParameter
} from '@vechain/sdk-network';
import { type TypedDataDomain, type TypedDataParameter } from 'viem';
import { KMSVeChainProvider, KMSVeChainSigner } from '../src';
import { EIP712_CONTRACT, EIP712_FROM, EIP712_TO } from './fixture';
jest.mock('asn1js', () => ({
Expand Down Expand Up @@ -136,7 +137,6 @@ describe('KMSVeChainSigner', () => {
}
]
},
'Mail',
{
from: {
name: 'Cow',
Expand All @@ -147,7 +147,8 @@ describe('KMSVeChainSigner', () => {
wallet: EIP712_TO
},
contents: 'Hello, Bob!'
}
},
'Mail'
)
).rejects.toThrow(SignerMethodError);
});
Expand All @@ -161,8 +162,8 @@ describe('KMSVeChainSigner', () => {
signer.signTypedData(
{} as unknown as TypedDataDomain,
{} as unknown as Record<string, TypedDataParameter[]>,
'primaryType',
{} as unknown as Record<string, unknown>
{} as unknown as Record<string, unknown>,
'primaryType'
)
).rejects.toThrow(SignerMethodError);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import {
JSONRPCInvalidParams,
stringifyData
} from '@vechain/sdk-errors';
import type { TypedDataDomain, TypedDataParameter } from 'viem';
import type { VeChainSigner } from '../../../../../signer/signers';
import type {
TypedDataDomain,
TypedDataParameter,
VeChainSigner
} from '../../../../../signer/signers';
import type { ThorClient } from '../../../../../thor-client';
import type { VeChainProvider } from '../../../../providers/vechain-provider';

Expand Down Expand Up @@ -74,8 +77,8 @@ const ethSignTypedDataV4 = async (
return await signer.signTypedData(
typedData.domain,
typedData.types,
typedData.primaryType,
typedData.message
typedData.message,
typedData.primaryType
);
} catch (error) {
throw new JSONRPCInternalError(
Expand Down
16 changes: 14 additions & 2 deletions packages/network/src/signer/signers/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { type TransactionClause } from '@vechain/sdk-core';
import { type TypedDataDomain, type TypedDataParameter } from 'viem';
import type {
TypedDataDomain as viemTypedDataDomain,
TypedDataParameter as viemTypedDataParameter
} from 'viem';
import {
type HardhatVeChainProvider,
type VeChainProvider
Expand All @@ -13,6 +16,13 @@ import {
*/
type AvailableVeChainProviders = VeChainProvider | HardhatVeChainProvider;

/**
* EIP-712 types in case we change the provider (viem as of now)
*/

type TypedDataDomain = viemTypedDataDomain;
type TypedDataParameter = viemTypedDataParameter;

/**
* Type for transaction input
*
Expand Down Expand Up @@ -371,8 +381,8 @@ interface VeChainSigner {
signTypedData: (
domain: TypedDataDomain,
types: Record<string, TypedDataParameter[]>,
primaryType: string,
message: Record<string, unknown>,
primaryType?: string,
options?: SignTypedDataOptions
) => Promise<string>;

Expand All @@ -384,6 +394,8 @@ interface VeChainSigner {

export {
type AvailableVeChainProviders,
type TypedDataDomain,
type TypedDataParameter,
type SignTypedDataOptions,
type TransactionRequestInput,
type VeChainSigner
Expand Down
Loading
Loading