Skip to content

Commit

Permalink
Merge pull request #578 from nevermined-io/feat/zerodev
Browse files Browse the repository at this point in the history
feat: initial support for zerodev
  • Loading branch information
r-marques authored Oct 19, 2023
2 parents afbfae6 + dc2e7ae commit bb26f8a
Show file tree
Hide file tree
Showing 8 changed files with 413 additions and 24 deletions.
215 changes: 215 additions & 0 deletions integration/external/Zerodev.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
import { ethers } from 'ethers'
import { ZeroDevEthersProvider } from '@zerodev/sdk'
import { verifyMessage } from '@ambire/signature-validator'
import * as fs from 'fs'
import {
Account,
AssetAttributes,
AssetPrice,
DDO,
MetaData,
Nevermined,
convertEthersV6SignerToAccountSigner,
} from '../../src'
import { assert } from 'chai'
import { decodeJwt } from 'jose'
import { config } from '../config'
import { getMetadata } from '../utils'

describe('Nevermined sdk with zerodev', () => {
let nevermined: Nevermined

before(async () => {
nevermined = await Nevermined.getInstance(config)
})

describe('Test zerodev signatures and login', () => {
let zerodevProvider: ZeroDevEthersProvider<'ECDSA'>
let clientAssertion: string

before(async () => {
const projectId = process.env.PROJECT_ID!
const owner = ethers.Wallet.createRandom()

zerodevProvider = await ZeroDevEthersProvider.init('ECDSA', {
projectId,
owner: convertEthersV6SignerToAccountSigner(owner),
})
})

it('should produce a valid EIP-6492 signature', async () => {
const signer = zerodevProvider.getAccountSigner()

const signature = await signer.signMessageWith6492('nevermined')
const isValidSignature = await verifyMessage({
signer: await signer.getAddress(),
message: 'nevermined',
signature: signature,
provider: zerodevProvider,
})

assert.isTrue(isValidSignature)
})

it('should provide a valid EIP-6492 typed signature', async () => {
const domain = {
name: 'Nevermined',
version: '1',
chainId: 80001,
}
const types = {
Nevermined: [{ name: 'message', type: 'string' }],
}
const message = {
message: 'nevermined',
}

const signer = zerodevProvider.getAccountSigner()
const signature = await signer.signTypedDataWith6492({
domain,
types,
message,
primaryType: '',
})

const isValidSignature = await verifyMessage({
signer: await signer.getAddress(),
signature: signature,
typedData: {
types,
domain,
message,
},
provider: zerodevProvider,
})

assert.isTrue(isValidSignature)
})

it('should generate a client assertion with a zerodev signer', async () => {
const signer = zerodevProvider.getAccountSigner()
const account = await Account.fromZeroDevSigner(signer)

clientAssertion = await nevermined.utils.jwt.generateClientAssertion(account, 'hello world')
assert.isDefined(clientAssertion)

const jwtPayload = decodeJwt(clientAssertion)
assert.equal(jwtPayload.iss, await signer.getAddress())
})

it('should login to the marketplace api', async () => {
const accessToken = await nevermined.services.marketplace.login(clientAssertion)
assert.isDefined(accessToken)

const jwtPayload = decodeJwt(accessToken)
const signer = zerodevProvider.getAccountSigner()
assert.equal(jwtPayload.iss, await signer.getAddress())
assert.isDefined(jwtPayload.sub)
})
})

describe('E2E Asset flow with zerodev', () => {
let zerodevProviderPublisher: ZeroDevEthersProvider<'ECDSA'>
let zerodevProviderConsumer: ZeroDevEthersProvider<'ECDSA'>
let metadata: MetaData
let ddo: DDO
let agreementId: string

before(async () => {
const projectId = process.env.PROJECT_ID!
const publisher = ethers.Wallet.createRandom()
const consumer = ethers.Wallet.createRandom()

zerodevProviderPublisher = await ZeroDevEthersProvider.init('ECDSA', {
projectId,
owner: convertEthersV6SignerToAccountSigner(publisher),
})

zerodevProviderConsumer = await ZeroDevEthersProvider.init('ECDSA', {
projectId,
owner: convertEthersV6SignerToAccountSigner(consumer),
})

const signerPublisher = zerodevProviderPublisher.getAccountSigner()
const accountPublisher = await Account.fromZeroDevSigner(signerPublisher)
const clientAssertion = await nevermined.utils.jwt.generateClientAssertion(accountPublisher)

const accessToken = await nevermined.services.marketplace.login(clientAssertion)
const payload = decodeJwt(accessToken)

metadata = getMetadata()
metadata.userId = payload.sub
})

it('should register an asset with a zerodev account', async () => {
const assetAttributes = AssetAttributes.getInstance({
metadata,
services: [
{
serviceType: 'access',
price: new AssetPrice(),
},
],
providers: [config.neverminedNodeAddress],
})

const signerPublisher = zerodevProviderPublisher.getAccountSigner()
const publisher = await Account.fromZeroDevSigner(signerPublisher)
ddo = await nevermined.assets.create(assetAttributes, publisher, undefined, {
zeroDevSigner: signerPublisher,
})

assert.isDefined(ddo)
assert.equal(ddo.publicKey[0].owner, await signerPublisher.getAddress())
assert.equal(ddo.proof.creator, await signerPublisher.getAddress())
})

it('owner should be able to download the asset', async () => {
const signerPublisher = zerodevProviderPublisher.getAccountSigner()
const publisher = await Account.fromZeroDevSigner(signerPublisher)
const folder = '/tmp/nevermined/sdk-js'

const path = (await nevermined.assets.download(ddo.id, publisher, folder, -1)) as string
const files = await new Promise<string[]>((resolve) => {
fs.readdir(path, (e, fileList) => {
resolve(fileList)
})
})

assert.deepEqual(files, ['README.md', 'ddo-example.json'])
})

it('consumer should be able to order the asset with a zerodev account', async () => {
const signerConsumer = zerodevProviderConsumer.getAccountSigner()
const consumer = await Account.fromZeroDevSigner(signerConsumer)
agreementId = await nevermined.assets.order(ddo.id, 'access', consumer, {
zeroDevSigner: signerConsumer,
})

assert.isDefined(agreementId)
})

it('consumer should be able to access ordered assets with zerodev account', async () => {
const signerConsumer = zerodevProviderConsumer.getAccountSigner()
const consumer = await Account.fromZeroDevSigner(signerConsumer)
const folder = '/tmp/nevermined/sdk-js'

const path = (await nevermined.assets.access(
agreementId,
ddo.id,
'access',
consumer,
folder,
-1,
)) as string

const files = await new Promise<string[]>((resolve) => {
fs.readdir(path, (e, fileList) => {
resolve(fileList)
})
})

assert.deepEqual(files, ['README.md', 'ddo-example.json'])
})
})
})
2 changes: 1 addition & 1 deletion integration/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"moduleResolution": "NodeNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"lib": ["es6", "es7", "dom", "ES2020"],
"target": "ES2020",
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,17 @@
},
"homepage": "https://github.com/nevermined-io/sdk-js#readme",
"dependencies": {
"@alchemy/aa-core": "0.1.0",
"@apollo/client": "^3.7.16",
"@zerodev/sdk": "4.0.30",
"assert": "^2.0.0",
"cross-fetch": "^4.0.0",
"crypto-browserify": "^3.12.0",
"deprecated-decorator": "^0.1.6",
"ethers": "^6.7.1",
"form-data": "^4.0.0",
"graphql": "^16.7.1",
"https-browserify": "^1.0.0",
"form-data": "^4.0.0",
"jose": "^4.5.1",
"js-file-download": "^0.4.12",
"lodash": "^4.17.21",
Expand All @@ -75,6 +77,7 @@
"whatwg-url": "^13.0.0"
},
"devDependencies": {
"@ambire/signature-validator": "^1.3.1",
"@commitlint/cli": "^17.4.2",
"@commitlint/config-conventional": "^17.4.2",
"@faker-js/faker": "^6.3.1",
Expand Down
83 changes: 83 additions & 0 deletions src/keeper/contracts/ContractBase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import {
ContractTransactionReceipt,
ContractTransactionResponse,
FunctionFragment,
TransactionReceipt,
ethers,
} from 'ethers'
import { jsonReplacer, parseUnits } from '../../sdk'
import { ZeroDevAccountSigner } from '@zerodev/sdk'
export interface TxParameters {
value?: string
gasLimit?: bigint
Expand All @@ -17,6 +19,7 @@ export interface TxParameters {
maxPriorityFeePerGas?: string
maxFeePerGas?: string
signer?: ethers.Signer
zeroDevSigner?: ZeroDevAccountSigner<'ECDSA'>
nonce?: number
progress?: (data: any) => void
}
Expand Down Expand Up @@ -162,12 +165,92 @@ export abstract class ContractBase extends Instantiable {
return transactionReceipt
}

private async internalSendZeroDev(
name: string,
from: string,
args: any[],
txparams: any,
contract: ethers.BaseContract,
progress: (data: any) => void,
): Promise<ContractTransactionReceipt> {
const methodSignature = this.getSignatureOfMethod(name, args)
// Uncomment to debug contract calls
// console.debug(`Making contract call ....: ${name} - ${from}`)
// console.debug(`With args - ${JSON.stringify(args)}`)
// console.debug(`And signature - ${methodSignature}`)

const { gasLimit, value } = txparams
// make the call
if (progress) {
progress({
stage: 'sending',
args: this.searchMethodInputs(name, args),
method: name,
from,
value,
contractName: this.contractName,
contractAddress: this.address,
gasLimit,
})
}

const transactionResponse: ContractTransactionResponse = await contract[methodSignature](
...args,
txparams,
)
if (progress) {
progress({
stage: 'sent',
args: this.searchMethodInputs(name, args),
transactionResponse,
method: name,
from,
value,
contractName: this.contractName,
contractAddress: this.address,
gasLimit,
})
}

const transactionReceipt: TransactionReceipt =
await transactionResponse.provider.waitForTransaction(transactionResponse.hash)

if (progress) {
progress({
stage: 'receipt',
args: this.searchMethodInputs(name, args),
transactionReceipt,
method: name,
from,
value,
contractName: this.contractName,
contractAddress: this.address,
gasLimit,
})
}

return transactionReceipt as ContractTransactionReceipt
}

public async send(
name: string,
from: string,
args: any[],
params: TxParameters = {},
): Promise<ContractTransactionReceipt> {
if (params.zeroDevSigner) {
const paramsFixed = { ...params, signer: undefined }
const contract = this.contract.connect(params.zeroDevSigner as any)
return await this.internalSendZeroDev(
name,
from,
args,
paramsFixed,
contract,
params.progress,
)
}

if (params.signer) {
const paramsFixed = { ...params, signer: undefined }
const contract = this.contract.connect(params.signer)
Expand Down
28 changes: 27 additions & 1 deletion src/keeper/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ethers } from 'ethers'
import { Signer, TypedDataField, Wallet, ethers } from 'ethers'
import { SmartAccountSigner, SignTypedDataParams, Hex } from '@alchemy/aa-core'
import { KeeperError } from '../errors'

export async function getNetworkName(networkId: number): Promise<string> {
Expand Down Expand Up @@ -91,3 +92,28 @@ export class Web3ProviderWrapper {
.then((result) => callback(null, { jsonrpc: payload.jsonrpc, id, result }))
}
}

const isWalletEthersV6 = (signer: any): signer is Wallet =>
signer && signer.signTypedData !== undefined

// zerodev ethersV6 compatibility
export const convertEthersV6SignerToAccountSigner = (
signer: Signer | Wallet,
): SmartAccountSigner => {
return {
signerType: '',
getAddress: async () => Promise.resolve((await signer.getAddress()) as `0x${string}`),
signMessage: async (msg: Uint8Array | string) =>
(await signer.signMessage(msg)) as `0x${string}`,
signTypedData: async (params: SignTypedDataParams) => {
if (!isWalletEthersV6(signer)) {
throw Error('signTypedData method not implemented in signer')
}
return (await signer.signTypedData(
params.domain!,
params.types as unknown as Record<string, TypedDataField[]>,
params.message,
)) as Hex
},
}
}
Loading

0 comments on commit bb26f8a

Please sign in to comment.