Skip to content

Commit

Permalink
signTypedData support
Browse files Browse the repository at this point in the history
  • Loading branch information
mattschoch committed Mar 28, 2024
1 parent 7a82d43 commit 8acfb3c
Show file tree
Hide file tree
Showing 10 changed files with 263 additions and 130 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ArgumentsHost, HttpStatus } from '@nestjs/common'
import { ArgumentsHost, HttpStatus, Logger } from '@nestjs/common'
import { HttpArgumentsHost } from '@nestjs/common/interfaces'
import { ConfigService } from '@nestjs/config'
import { Response } from 'express'
Expand Down Expand Up @@ -46,6 +46,15 @@ describe(ApplicationExceptionFilter.name, () => {
})

describe('catch', () => {
// Silence the logger in these tests so we don't spam our console w/ errors that are "expected"
beforeAll(() => {
Logger.overrideLogger([])
})

afterAll(() => {
Logger.overrideLogger(new Logger())
})

describe('when environment is production', () => {
it('responds with exception status and short message', () => {
const filter = new ApplicationExceptionFilter(buildConfigServiceMock(Env.PRODUCTION))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Action, Request } from '@narval/policy-engine-shared'
import { Action, Eip712TypedData, Request } from '@narval/policy-engine-shared'
import { Test } from '@nestjs/testing'
import {
Hex,
Expand All @@ -7,7 +7,8 @@ import {
parseTransaction,
serializeTransaction,
toHex,
verifyMessage
verifyMessage,
verifyTypedData
} from 'viem'
import { Wallet } from '../../../../../shared/type/domain.type'
import { WalletRepository } from '../../../../persistence/repository/wallet.repository'
Expand Down Expand Up @@ -139,5 +140,74 @@ describe('SigningService', () => {
expect(result).toEqual(expectedSignature)
expect(isVerified).toEqual(true)
})

it('signs EIP712 Typed Data', async () => {
const typedData: Eip712TypedData = {
domain: {
chainId: 137,
name: 'Crypto Unicorns Authentication',
version: '1'
},
message: {
contents: 'UNICOOOORN :)',
wallet: '0xdd4d43575a5eff17ec814da6ea810a0cc39ff23e',
nonce: '0e01c9bd-94a0-4ba1-925d-ab02688e65de'
},
primaryType: 'Validator',
types: {
EIP712Domain: [
{
name: 'name',
type: 'string'
},
{
name: 'version',
type: 'string'
},
{
name: 'chainId',
type: 'uint256'
}
],
Validator: [
{
name: 'contents',
type: 'string'
},
{
name: 'wallet',
type: 'address'
},
{
name: 'nonce',
type: 'string'
}
]
}
}
const tenantId = 'tenantId'
const typedDataRequest: Request = {
action: Action.SIGN_TYPED_DATA,
nonce: 'random-nonce-111',
resourceId: 'eip155:eoa:0x2c4895215973CbBd778C32c456C074b99daF8Bf1',
typedData
}

const expectedSignature =
'0x1f6b8ebbd066c5a849e37fc890c1f2f1b6b0a91e3dd3e8279c646948e8f14b030a13a532fd04c6b5d92e11e008558b0b60b6d061c8f34483af7deab0591317da1b'

// Call the sign method
const result = await signingService.sign(tenantId, typedDataRequest)

const isVerified = await verifyTypedData({
address: wallet.address,
signature: result,
...typedData
})

// Assert the result
expect(isVerified).toEqual(true)
expect(result).toEqual(expectedSignature)
})
})
})
48 changes: 28 additions & 20 deletions apps/vault/src/vault/core/service/signing.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { Action, Hex, Request, SignMessageAction, SignTransactionAction } from '@narval/policy-engine-shared'
import {
Action,
Hex,
Request,
SignMessageAction,
SignTransactionAction,
SignTypedDataAction
} from '@narval/policy-engine-shared'
import { HttpStatus, Injectable } from '@nestjs/common'
import {
TransactionRequest,
Expand All @@ -23,13 +30,14 @@ export class SigningService {
return this.signTransaction(tenantId, request)
} else if (request.action === Action.SIGN_MESSAGE) {
return this.signMessage(tenantId, request)
} else if (request.action === Action.SIGN_TYPED_DATA) {
return this.signTypedData(tenantId, request)
}

throw new Error('Action not supported')
}

async signTransaction(tenantId: string, action: SignTransactionAction): Promise<Hex> {
const { transactionRequest, resourceId } = action
async #buildClient(tenantId: string, resourceId: string, chainId?: number) {
const wallet = await this.walletRepository.findById(tenantId, resourceId)
if (!wallet) {
throw new ApplicationException({
Expand All @@ -42,7 +50,7 @@ export class SigningService {
const account = privateKeyToAccount(wallet.privateKey)
const chain = extractChain<chains.Chain[], number>({
chains: Object.values(chains),
id: transactionRequest.chainId
id: chainId || 1
})

const client = createWalletClient({
Expand All @@ -51,8 +59,15 @@ export class SigningService {
transport: http('') // clear the RPC so we don't call any chain stuff here.
})

return client
}

async signTransaction(tenantId: string, action: SignTransactionAction): Promise<Hex> {
const { transactionRequest, resourceId } = action
const client = await this.#buildClient(tenantId, resourceId, transactionRequest.chainId)

const txRequest: TransactionRequest = {
from: checksumAddress(account.address),
from: checksumAddress(client.account.address),
to: transactionRequest.to,
nonce: transactionRequest.nonce,
data: transactionRequest.data,
Expand Down Expand Up @@ -83,24 +98,17 @@ export class SigningService {

async signMessage(tenantId: string, action: SignMessageAction): Promise<Hex> {
const { message, resourceId } = action
const wallet = await this.walletRepository.findById(tenantId, resourceId)
if (!wallet) {
throw new ApplicationException({
message: 'Wallet not found',
suggestedHttpStatusCode: HttpStatus.BAD_REQUEST,
context: { clientId: tenantId, resourceId }
})
}
const client = await this.#buildClient(tenantId, resourceId)

const account = privateKeyToAccount(wallet.privateKey)
const signature = await client.signMessage({ message })
return signature
}

const client = createWalletClient({
account,
chain: chains.mainnet,
transport: http('') // clear the RPC so we don't call any chain stuff here.
})
async signTypedData(tenantId: string, action: SignTypedDataAction): Promise<Hex> {
const { typedData, resourceId } = action
const client = await this.#buildClient(tenantId, resourceId)

const signature = await client.signMessage({ message })
const signature = await client.signTypedData(typedData)
return signature
}
}
14 changes: 7 additions & 7 deletions apps/vault/src/vault/http/rest/controller/sign.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@ import { Request } from '@narval/policy-engine-shared'
import { Body, Controller, Post, UseGuards } from '@nestjs/common'
import { createZodDto } from 'nestjs-zod'
import {
SignMessageActionSchema,
SignTransactionActionSchema,
SignTypedDataActionSchema
} from 'packages/policy-engine-shared/src/lib/schema/action.schema'
SignMessageAction,
SignTransactionAction,
SignTypedDataAction
} from 'packages/policy-engine-shared/src/lib/type/action.type'
import { z } from 'zod'
import { ClientId } from '../../../../shared/decorator/client-id.decorator'
import { AuthorizationGuard } from '../../../../shared/guard/authorization.guard'
import { SigningService } from '../../../core/service/signing.service'

const SignRequestSchema = z.object({
request: z.union([SignTransactionActionSchema, SignMessageActionSchema, SignTypedDataActionSchema])
const SignRequest = z.object({
request: z.union([SignTransactionAction, SignMessageAction, SignTypedDataAction])
})

class SignRequestDto extends createZodDto(SignRequestSchema) {}
class SignRequestDto extends createZodDto(SignRequest) {}
@Controller('/sign')
@UseGuards(AuthorizationGuard)
export class SignController {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { toHex } from 'viem'
import { Action, SignTypedDataAction } from '../../type/action.type'

describe('SignTypedDataAction', () => {
const typedData = {
domain: {
name: 'Ether Mail',
version: '1',
chainId: 1,
verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC'
},
types: {
Person: [
{ name: 'name', type: 'string' },
{ name: 'wallet', type: 'address' }
],
Mail: [
{ name: 'from', type: 'Person' },
{ name: 'to', type: 'Person' },
{ name: 'contents', type: 'string' }
]
},
primaryType: 'Mail',
message: {
from: {
name: 'Cow',
wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826'
},
to: {
name: 'Bob',
wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB'
},
contents: 'Hello, Bob!'
}
}

it('should validate a valid SignTypedDataAction object', () => {
const validAction = {
action: Action.SIGN_TYPED_DATA,
nonce: 'xxx',
resourceId: 'resourceId',
typedData
}

const result = SignTypedDataAction.safeParse(validAction)

expect(result).toEqual({
success: true,
data: expect.any(Object)
})
})

it('should validate a valid typedData as a string', () => {
const validAction = {
action: Action.SIGN_TYPED_DATA,
nonce: 'xxx',
resourceId: 'resourceId',
typedData: JSON.stringify(typedData)
}

const result = SignTypedDataAction.safeParse(validAction)

expect(result.success).toEqual(true)
})

it('should validate a valid typedData as a hex-encoded stringified json object', () => {
const validAction = {
action: Action.SIGN_TYPED_DATA,
nonce: 'xxx',
resourceId: 'resourceId',
typedData: toHex(JSON.stringify(typedData))
}

const result = SignTypedDataAction.safeParse(validAction)

expect(result.success).toEqual(true)
})

it('should not validate an invalid SignTypedDataAction object with invalid JSON string', () => {
const invalidAction = {
action: Action.SIGN_TYPED_DATA,
nonce: 'xxx',
resourceId: 'resourceId',
typedData: 'invalidJSON'
}

const result = SignTypedDataAction.safeParse(invalidAction)

expect(result.success).toEqual(false)
})
})
74 changes: 0 additions & 74 deletions packages/policy-engine-shared/src/lib/schema/action.schema.ts

This file was deleted.

Loading

0 comments on commit 8acfb3c

Please sign in to comment.