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: initial support for zerodev #578

Merged
merged 7 commits into from
Oct 19, 2023
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
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